Files
gmtool/tests/components/gm-shell-break.test.js
T
slaguru666andClaude Opus 4.8 73a7ea72ad feat: break timer — pause the session clock so breaks don't poison drift
Add <break-timer> tray: pick a duration, Start break pauses the session
clock (reusing clock.pause/resume), End break resumes it — so meal/comfort
break time is excluded from elapsed and drift stays honest. The shell owns
pause/resume + persistence (break store key); the rail shows a on-break
badge and gains a coffee chip. fmtCountdown (M:SS) added to format.js.

tests/components/break-timer.test.js + gm-shell-break.test.js (incl. the
30-play/20-break/10-play drift-honesty case) + fmtCountdown. 159 tests pass.

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

72 lines
2.9 KiB
JavaScript

import { describe, it, expect, beforeEach } from 'vitest';
import '../../src/components/gm-shell.js';
import afterimage from '../../src/scenarios/afterimage.js';
describe('<gm-shell> break timer', () => {
let el, t;
beforeEach(() => {
localStorage.clear();
document.body.innerHTML = '';
t = 0;
el = document.createElement('gm-shell');
el.now = () => t;
document.body.appendChild(el);
el.loadScenario(afterimage);
});
it('mounts a hidden break-timer, revealed by open-tool break', () => {
expect(el.querySelector('break-timer').hidden).toBe(true);
el.dispatchEvent(new CustomEvent('open-tool', { detail: { tool: 'break' }, bubbles: true }));
expect(el.querySelector('break-timer').hidden).toBe(false);
});
it('a break pauses the clock so break time never poisons drift', () => {
el.startSession(); // t=0
t = 30 * 60000; // 30 min of play
expect(Math.round(el.elapsedMs() / 60000)).toBe(30);
el.dispatchEvent(new CustomEvent('start-break', { detail: { minutes: 10 }, bubbles: true }));
expect(el.querySelector('director-rail [data-role=break-badge]')).not.toBe(null);
expect(el.querySelector('break-timer').state.onBreak).toBe(true);
t = 50 * 60000; // 20 min of wall-clock break
expect(Math.round(el.elapsedMs() / 60000)).toBe(30); // frozen while on break
el.dispatchEvent(new CustomEvent('end-break', { bubbles: true }));
expect(el.querySelector('director-rail [data-role=break-badge]')).toBe(null);
t = 60 * 60000; // 10 more min of play after the break
expect(Math.round(el.elapsedMs() / 60000)).toBe(40); // 40 min play, break excluded
});
it('persists the break so the paused clock survives a reload', () => {
el.startSession();
t = 10 * 60000;
el.dispatchEvent(new CustomEvent('start-break', { detail: { minutes: 5 }, bubbles: true }));
expect(JSON.parse(localStorage.getItem('gmd.afterimage.break'))).toBe(el.breakEndsAt);
// simulate reload
document.body.innerHTML = '';
const el2 = document.createElement('gm-shell');
el2.now = () => t;
document.body.appendChild(el2);
el2.loadScenario(afterimage);
expect(el2.querySelector('break-timer').state.onBreak).toBe(true);
expect(el2.querySelector('director-rail [data-role=break-badge]')).not.toBe(null);
});
it('ignores a start-break before the session has started', () => {
el.dispatchEvent(new CustomEvent('start-break', { detail: { minutes: 10 }, bubbles: true }));
expect(el.querySelector('break-timer').state.onBreak).toBe(false);
expect(el.breakEndsAt).toBe(null);
});
it('the rail exposes a break chip that emits open-tool', () => {
const rail = el.querySelector('director-rail');
let tool = null;
el.addEventListener('open-tool', (e) => { tool = e.detail.tool; });
rail.querySelector('[data-role=open-break]').click();
expect(tool).toBe('break');
});
});