Files
gmtool/tests/components/con-hub.test.js
T
slaguru666andClaude Opus 4.8 b57880d630 feat: Slice 5 — convention hub (live now / up next, deep-links)
Add the convention hub landing screen: a pure schedule engine
(analyzeSchedule) classifies each slot as live/next/done/upcoming from an
injected now; <con-hub> renders the slate + summary and emits open-scenario;
a scenario registry resolves a slot's scenarioId to its module; gm-shell
routes hub<->session (hiding/restoring the Rail) with a home rail chip.

Ships continuum-2026.js with the real slate (only AFTERIMAGE ported;
others scenarioId:null, start-times TBC) and a fully-timed example-con.js
template/fixture. 122 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:35:47 +01:00

58 lines
2.3 KiB
JavaScript

import { describe, it, expect, beforeEach } from 'vitest';
import '../../src/components/con-hub.js';
const CON = {
meta: { id: 'demo', title: 'Demo Con' },
slots: [
{ id: 's1', scenarioId: 'a', title: 'Past Game', slot: 'Fri · S1', system: 'year-zero', players: 4, playMinutes: 60, startsAt: '2026-07-24T09:00:00Z' },
{ id: 's2', scenarioId: 'b', title: 'Now Game', slot: 'Fri · S2', system: 'coc-d100', players: 5, playMinutes: 60, startsAt: '2026-07-24T12:00:00Z' },
{ id: 's3', scenarioId: null, title: 'Unported', slot: 'Sat · S4', playMinutes: 60, startsAt: '2026-07-25T09:00:00Z' },
],
};
describe('<con-hub>', () => {
let el;
beforeEach(() => {
document.body.innerHTML = '';
el = document.createElement('con-hub');
el.now = () => Date.parse('2026-07-24T12:30:00Z');
document.body.appendChild(el);
});
it('shows an empty state with no con', () => {
expect(el.querySelector('[data-role=empty]')).not.toBe(null);
});
it('renders a card per slot with its status', () => {
el.con = CON;
expect(el.querySelectorAll('[data-slot-id]').length).toBe(3);
expect(el.querySelector('[data-slot-id=s1]').dataset.status).toBe('done');
expect(el.querySelector('[data-slot-id=s2]').dataset.status).toBe('live');
expect(el.querySelector('[data-slot-id=s3]').dataset.status).toBe('next');
});
it('summarises the live session', () => {
el.con = CON;
expect(el.querySelector('[data-role=summary]').textContent).toContain('Now Game');
});
it('emits open-scenario for a ported slot', () => {
el.con = CON;
let detail = null;
el.addEventListener('open-scenario', (e) => { detail = e.detail; });
el.querySelector('[data-slot-id=s2] [data-role=open]').click();
expect(detail).toEqual({ scenarioId: 'b' });
});
it('shows "not yet ported" and no open button for an unported slot', () => {
el.con = CON;
expect(el.querySelector('[data-slot-id=s3] [data-role=not-ready]')).not.toBe(null);
expect(el.querySelector('[data-slot-id=s3] [data-role=open]')).toBe(null);
});
it('escapes author text', () => {
el.con = { meta: { title: 'x' }, slots: [{ id: 'x', scenarioId: null, title: 'evil <b>', slot: 'a" <i>', playMinutes: 60 }] };
expect(el.querySelector('[data-slot-id=x]').innerHTML).not.toContain('<b>');
});
});