From 3d4bd3fc383acf79396b91a502c0fa432dcc2cff Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 9 May 2026 21:55:58 +0200 Subject: [PATCH] test(timetable): row-height helper + StageHeaderCell prop seam (B4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B4 — jsdom-runnable assertions for the structural pieces of B2/B3. apps/app/tests/unit/lib/timetable/row-height.test.ts (4 tests): - laneCount=0 → 52px (Math.max(1, 0) fallback path) - laneCount=1 → 52px (single-lane stage row) - laneCount=3 → 148px - laneCount=10 → 484px (10 × 48 + 4) apps/app/tests/component/StageHeaderCell.test.ts (4 tests): - row-height-px prop applies as inline blockSize on the root - prop omitted → no inline blockSize set (legacy `block-size: 100%` CSS path takes over for any caller still relying on parent-driven sizing) - 484px for laneCount=10 round-trips through the prop without truncation - conflict badge renders only when conflictCount > 0 (existing behavior; locked in as part of touching this surface) Visual scroll/alignment proof (sticky-left freeze pane, sticky-top axis, horizontal scroll cohesion across 14 stages, diagonal trackpad scroll, pixel-perfect header↔row alignment) is deferred to TEST-VISUAL-001 explicitly: jsdom does not compute position:sticky offsets, scrollbar visibility, layout overflow chains, or scroll containment ancestry. This is a known limitation of jsdom-based component testing — not a test gap in this branch. The sticky behavior, z-index ladder, and DOM structure are all in place per E1-E4; their validation requires a real browser, which is exactly what the Playwright CT migration on TEST-INFRA-001 + TEST-VISUAL-001 unlocks. No existing tests asserted the old broken layout (no references to the deprecated `tt-page__rows`, `tt-page__stages`, or `` in tests/). The unused GridBg component file remains on disk; deleting it is a stylistic cleanup outside this stabilization scope. Test count: 389 → 397. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/component/StageHeaderCell.test.ts | 73 +++++++++++++++++++ .../unit/lib/timetable/row-height.test.ts | 33 +++++++++ 2 files changed, 106 insertions(+) create mode 100644 apps/app/tests/component/StageHeaderCell.test.ts create mode 100644 apps/app/tests/unit/lib/timetable/row-height.test.ts diff --git a/apps/app/tests/component/StageHeaderCell.test.ts b/apps/app/tests/component/StageHeaderCell.test.ts new file mode 100644 index 00000000..096ea806 --- /dev/null +++ b/apps/app/tests/component/StageHeaderCell.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import { mountWithVuexy } from '../utils/mountWithVuexy' +import StageHeaderCell from '@/components/timetable/StageHeaderCell.vue' +import type { Stage } from '@/types/timetable' + +/** + * jsdom-runnable assertions for the B2 + B3 prop seam: + * + * - the explicit row-height-px prop applies as inline blockSize + * - the prop's absence falls back to the legacy CSS path (no inline style) + * - the conflict count badge appears only when > 0 + * + * Visual proof of the freeze panes — sticky-left alignment, pixel-perfect + * stacking against StageRow — is jsdom-blind and lives on TEST-VISUAL-001. + */ +const stage: Stage = { + id: 's1', + event_id: 'ev1', + name: 'Hardstyle District', + color: '#e85d75', + capacity: 1000, + sort_order: 0, + created_at: null, + updated_at: null, +} + +describe('StageHeaderCell — explicit row-height-px prop (B3 freeze-pane alignment)', () => { + it('applies blockSize inline when rowHeightPx prop is provided', () => { + const { wrapper } = mountWithVuexy(StageHeaderCell, { + props: { stage, rowHeightPx: 148 }, + }) + + const root = wrapper.find('[data-stage-id="s1"]') + + expect(root.attributes('style')).toMatch(/block-size:\s*148px/) + }) + + it('does not set blockSize inline when rowHeightPx is omitted (legacy CSS path)', () => { + const { wrapper } = mountWithVuexy(StageHeaderCell, { + props: { stage }, + }) + + const root = wrapper.find('[data-stage-id="s1"]') + const style = root.attributes('style') ?? '' + + expect(style).not.toMatch(/block-size/) + }) + + it('passes through laneCount=10 row height (484px) without truncation', () => { + const { wrapper } = mountWithVuexy(StageHeaderCell, { + props: { stage, rowHeightPx: 484 }, + }) + + const root = wrapper.find('[data-stage-id="s1"]') + + expect(root.attributes('style')).toMatch(/block-size:\s*484px/) + }) + + it('shows the conflict badge only when conflictCount > 0', () => { + const { wrapper: withZero } = mountWithVuexy(StageHeaderCell, { + props: { stage, conflictCount: 0 }, + }) + + expect(withZero.find('.tt-stage-header__conflict').exists()).toBe(false) + + const { wrapper: withTwo } = mountWithVuexy(StageHeaderCell, { + props: { stage, conflictCount: 2 }, + }) + + expect(withTwo.find('.tt-stage-header__conflict').exists()).toBe(true) + expect(withTwo.find('.tt-stage-header__conflict').text()).toContain('2') + }) +}) diff --git a/apps/app/tests/unit/lib/timetable/row-height.test.ts b/apps/app/tests/unit/lib/timetable/row-height.test.ts new file mode 100644 index 00000000..3f91acc1 --- /dev/null +++ b/apps/app/tests/unit/lib/timetable/row-height.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { computeStageRowHeight } from '@/lib/timetable/row-height' + +/** + * One-line helper, four assertion cases — keep this surface tight; this + * is the canonical row-height math for both StageRow content and the + * sticky-left StageHeaderCell. If either side drifts, the freeze panes + * misalign in the browser. The math: + * + * max(1, laneCount) × (laneHeight + lanePad) + lanePad + * + * with the canonical constants laneHeight=44, lanePad=4 → 48 per lane + 4. + */ +const LANE_HEIGHT = 44 +const LANE_PAD = 4 + +describe('computeStageRowHeight', () => { + it('falls back to single-lane height when laneCount is 0', () => { + expect(computeStageRowHeight(0, LANE_HEIGHT, LANE_PAD)).toBe(52) + }) + + it('returns 52px for laneCount=1', () => { + expect(computeStageRowHeight(1, LANE_HEIGHT, LANE_PAD)).toBe(52) + }) + + it('returns 148px for laneCount=3', () => { + expect(computeStageRowHeight(3, LANE_HEIGHT, LANE_PAD)).toBe(148) + }) + + it('returns 484px for laneCount=10 (10 × 48 + 4)', () => { + expect(computeStageRowHeight(10, LANE_HEIGHT, LANE_PAD)).toBe(484) + }) +})