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>
61 lines
2.6 KiB
JavaScript
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>');
|
|
});
|
|
});
|