Files
gmtool/tests/components/run-board.test.js
T
slaguru666andClaude Opus 4.8 0ec09290ea fix: iPad layout — clamp rail labels, fill the stage with a run-board
The rail was a fixed 64px bar cramming 3 info cells against 9 chips, so
long beat/trigger labels wrapped and clipped ('bled'), and trays were thin
top strips leaving the iPad screen mostly empty.

- Rail: min-height + 2-line clamp on the beat/trigger labels (no bleed),
  nowrap 'Reached it', slimmer chips, tabular clock.
- Trays now fill the stage as solid panels (inset:0) instead of a strip.
- New <run-board>: the whole run at a glance behind the trays — every beat
  with target time, done beats stamped/dimmed, next beat highlighted, hard
  triggers + drift. Fills the previously-empty stage with the most useful
  GM view.
- @media (min-width:1024px/1366px): larger type, roomier rows, more art
  columns — uses the tablet space.

tests/components/run-board.test.js. 219 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:41:03 +01:00

61 lines
2.6 KiB
JavaScript

import { describe, it, expect, beforeEach } from 'vitest';
import '../../src/components/run-board.js';
const SCENARIO = {
meta: { title: 'DEMO', slot: 'Fri · Slot 1', playMinutes: 210 },
timeline: [
{ id: 'a', label: 'Opening', targetMin: 0 },
{ id: 'b', label: 'The market breaks', targetMin: 60, hardTrigger: true, cutHint: 'skip the tea' },
{ id: 'c', label: 'The reveal', targetMin: 120, hardTrigger: true },
{ id: 'd', label: 'Epilogue', targetMin: 200 },
],
};
describe('<run-board>', () => {
let el;
beforeEach(() => {
document.body.innerHTML = '';
el = document.createElement('run-board');
document.body.appendChild(el);
});
it('renders nothing without a scenario', () => {
expect(el.innerHTML.trim()).toBe('');
});
it('renders a row per beat with target times and the scenario header', () => {
el.update({ scenario: SCENARIO, elapsedMs: 0, stamps: {} });
expect(el.querySelectorAll('[data-beat-id]').length).toBe(4);
expect(el.querySelector('.rb-title').textContent).toBe('DEMO');
expect(el.querySelector('[data-beat-id="b"] .rb-time').textContent).toBe('1:00');
expect(el.querySelector('[data-beat-id="b"] .rb-hard')).not.toBe(null);
expect(el.querySelector('[data-beat-id="b"] .rb-cut').textContent).toContain('skip the tea');
});
it('marks done beats with their stamped time and dims them', () => {
el.update({ scenario: SCENARIO, elapsedMs: 90 * 60000, stamps: { a: 2, b: 58 } });
const b = el.querySelector('[data-beat-id="b"]');
expect(b.classList.contains('done')).toBe(true);
expect(b.querySelector('.rb-mark').textContent).toContain('0:58');
});
it('highlights the next unreached beat as "next"', () => {
el.update({ scenario: SCENARIO, elapsedMs: 90 * 60000, stamps: { a: 2, b: 58 } });
const current = el.querySelector('.rb-beat.current');
expect(current.dataset.beatId).toBe('c'); // first without a stamp
expect(current.querySelector('.rb-mark').textContent).toContain('next');
expect(el.querySelector('[data-beat-id="a"].current')).toBe(null);
});
it('shows drift in the header when behind/ahead', () => {
// reached the opening 6 min late → behind
el.update({ scenario: SCENARIO, elapsedMs: 6 * 60000, stamps: { a: 6 } });
expect(el.querySelector('.rb-drift')).not.toBe(null);
});
it('escapes author text in labels', () => {
el.update({ scenario: { meta: { title: 'x' }, timeline: [{ id: 'x', label: 'evil <b>', targetMin: 0 }] }, elapsedMs: 0, stamps: {} });
expect(el.querySelector('[data-beat-id="x"]').innerHTML).not.toContain('<b>');
});
});