From 5dcb535555531d3f0645985076a6bcec79c9a028 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 21:48:15 +0200 Subject: [PATCH] test(document): cover TimelineControls and TimelineXAxis branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TimelineControls: empty render when neither flag is set, reset button gated on isZoomed, clear button gated on hasSelection, both-on, both callback wirings. TimelineXAxis: empty filled → no ticks, populated → ticks render, omit-year branch when all buckets share a year, show-year branch across multiple years, length-4 bucket-string fallback. 11 tests across two timeline primitives. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../document/TimelineControls.svelte.test.ts | 84 +++++++++++++++++++ .../lib/document/TimelineXAxis.svelte.test.ts | 54 ++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 frontend/src/lib/document/TimelineControls.svelte.test.ts create mode 100644 frontend/src/lib/document/TimelineXAxis.svelte.test.ts diff --git a/frontend/src/lib/document/TimelineControls.svelte.test.ts b/frontend/src/lib/document/TimelineControls.svelte.test.ts new file mode 100644 index 00000000..760404b4 --- /dev/null +++ b/frontend/src/lib/document/TimelineControls.svelte.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import TimelineControls from './TimelineControls.svelte'; + +afterEach(cleanup); + +describe('TimelineControls', () => { + it('renders neither button when not zoomed and no selection', async () => { + render(TimelineControls, { + props: { + isZoomed: false, + hasSelection: false, + onresetzoom: () => {}, + onclearselection: () => {} + } + }); + + const buttons = document.querySelectorAll('button'); + expect(buttons.length).toBe(0); + }); + + it('renders the reset-zoom button when isZoomed is true', async () => { + render(TimelineControls, { + props: { + isZoomed: true, + hasSelection: false, + onresetzoom: () => {}, + onclearselection: () => {} + } + }); + + await expect.element(page.getByRole('button', { name: /zur übersicht/i })).toBeVisible(); + }); + + it('renders the clear-selection button when hasSelection is true', async () => { + render(TimelineControls, { + props: { + isZoomed: false, + hasSelection: true, + onresetzoom: () => {}, + onclearselection: () => {} + } + }); + + await expect.element(page.getByRole('button', { name: /auswahl zurücksetzen/i })).toBeVisible(); + }); + + it('renders both buttons when both flags are true', async () => { + render(TimelineControls, { + props: { + isZoomed: true, + hasSelection: true, + onresetzoom: () => {}, + onclearselection: () => {} + } + }); + + const buttons = document.querySelectorAll('button'); + expect(buttons.length).toBe(2); + }); + + it('calls onresetzoom when the reset button is clicked', async () => { + const onresetzoom = vi.fn(); + render(TimelineControls, { + props: { isZoomed: true, hasSelection: false, onresetzoom, onclearselection: () => {} } + }); + + await page.getByRole('button', { name: /zur übersicht/i }).click(); + + expect(onresetzoom).toHaveBeenCalledOnce(); + }); + + it('calls onclearselection when the clear button is clicked', async () => { + const onclearselection = vi.fn(); + render(TimelineControls, { + props: { isZoomed: false, hasSelection: true, onresetzoom: () => {}, onclearselection } + }); + + await page.getByRole('button', { name: /auswahl zurücksetzen/i }).click(); + + expect(onclearselection).toHaveBeenCalledOnce(); + }); +}); diff --git a/frontend/src/lib/document/TimelineXAxis.svelte.test.ts b/frontend/src/lib/document/TimelineXAxis.svelte.test.ts new file mode 100644 index 00000000..a43b1850 --- /dev/null +++ b/frontend/src/lib/document/TimelineXAxis.svelte.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import TimelineXAxis from './TimelineXAxis.svelte'; + +afterEach(cleanup); + +const bucket = (month: string, count = 1) => ({ month, count }); + +describe('TimelineXAxis', () => { + it('renders no ticks when filled is empty', async () => { + render(TimelineXAxis, { props: { filled: [] } }); + + const ticks = document.querySelectorAll('[data-testid="timeline-x-tick"]'); + expect(ticks.length).toBe(0); + }); + + it('renders tick marks when filled buckets are present', async () => { + const filled = Array.from({ length: 12 }, (_, i) => + bucket(`1923-${String(i + 1).padStart(2, '0')}`) + ); + render(TimelineXAxis, { props: { filled } }); + + const ticks = document.querySelectorAll('[data-testid="timeline-x-tick"]'); + expect(ticks.length).toBeGreaterThan(0); + }); + + it('omits the year when all visible buckets share the same year', async () => { + const filled = Array.from({ length: 12 }, (_, i) => + bucket(`1923-${String(i + 1).padStart(2, '0')}`) + ); + render(TimelineXAxis, { props: { filled } }); + + const ticks = Array.from(document.querySelectorAll('[data-testid="timeline-x-tick"]')); + const allText = ticks.map((t) => t.textContent ?? '').join(' '); + expect(allText).not.toContain('1923'); + }); + + it('shows the year when buckets span multiple years', async () => { + const filled = [bucket('1923-01'), bucket('1924-06'), bucket('1925-12')]; + render(TimelineXAxis, { props: { filled } }); + + const ticks = Array.from(document.querySelectorAll('[data-testid="timeline-x-tick"]')); + const allText = ticks.map((t) => t.textContent ?? '').join(' '); + expect(allText).toMatch(/19\d{2}/); + }); + + it('handles single-year (length-4) bucket month strings without omitting the year', async () => { + const filled = [bucket('1923'), bucket('1924')]; + render(TimelineXAxis, { props: { filled } }); + + const ticks = document.querySelectorAll('[data-testid="timeline-x-tick"]'); + expect(ticks.length).toBeGreaterThan(0); + }); +});