// @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { GameData, SimSnapshot } from '../contracts'; import { createFax } from './fax'; const DATA: GameData = { items: [ { id: 'melt', name: 'MELT', codex: '', tier: 2, color: '#ff7a3f' }, { id: 'anchor-slab', name: 'I-FRAME', codex: '', tier: 1, color: '#fff2a0' }, { id: 'hf-dust', name: 'HF DUST', codex: '', tier: 1, color: '#e8e0ff' }, ], machines: [], recipes: [], tech: [], commissions: [ { id: 'first-taste', flavor: 'One (1) unit of MELT.', wants: { melt: 1 }, rewardBandwidth: 50 }, { id: 'dust-allowance', flavor: 'Dust. Weekly.', wants: { 'hf-dust': 5 }, rewardBandwidth: 20, repeat: true }, { id: 'patio-resurfacing', flavor: 'Bricks.', wants: { melt: 9 }, rewardBandwidth: 30 }, ], }; function snap(over: Partial = {}): SimSnapshot { return { tick: 0, paused: false, entities: [], beltItems: [], bandwidth: { gen: 0, draw: 0, stored: 0, brownout: false }, shippedTotal: {}, activeCommission: 'first-taste', commissionProgress: {}, ...over, }; } const q = (el: HTMLElement, s: string) => el.querySelector(s) as HTMLElement; const on = (el: HTMLElement, s: string) => q(el, s).classList.contains('is-on'); describe('fax stamp lifecycle', () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); it('is not stamped until a commission completes', () => { const fax = createFax(DATA); expect(on(fax.el, '.fk-fax-stamp')).toBe(false); }); it('stamps on demand and clears itself after the animation', () => { const fax = createFax(DATA); fax.stamp(); expect(on(fax.el, '.fk-fax-stamp')).toBe(true); vi.advanceTimersByTime(1799); expect(on(fax.el, '.fk-fax-stamp')).toBe(true); // still landed vi.advanceTimersByTime(1); expect(on(fax.el, '.fk-fax-stamp')).toBe(false); }); it('restarts cleanly when two commissions complete back to back', () => { const fax = createFax(DATA); fax.stamp(); vi.advanceTimersByTime(1000); fax.stamp(); // second order lands mid-animation vi.advanceTimersByTime(1000); // The first stamp's timer must not clear the second one early. expect(on(fax.el, '.fk-fax-stamp')).toBe(true); vi.advanceTimersByTime(800); expect(on(fax.el, '.fk-fax-stamp')).toBe(false); }); }); describe('fax panel', () => { it('stays shut when there is no active commission', () => { const fax = createFax(DATA); fax.update(snap({ activeCommission: null })); expect(fax.el.classList.contains('is-open')).toBe(false); }); it('shows the active commission flavor, wants and reward', () => { const fax = createFax(DATA); fax.update(snap({ commissionProgress: { melt: 0 } })); expect(fax.el.classList.contains('is-open')).toBe(true); expect(q(fax.el, '.fk-fax-flavor').textContent).toBe('One (1) unit of MELT.'); expect(q(fax.el, '.fk-chip').textContent).toBe('MELT0/1'); expect(fax.el.textContent).toContain('+50 BANDWIDTH'); }); it('stamps STANDING ORDER only for a repeat commission', () => { const fax = createFax(DATA); fax.update(snap()); expect(on(fax.el, '.fk-fax-standing')).toBe(false); fax.update(snap({ activeCommission: 'dust-allowance' })); expect(on(fax.el, '.fk-fax-standing')).toBe(true); }); it('shows SANITISING only once anchor-slabs have actually shipped', () => { const fax = createFax(DATA); fax.update(snap()); expect(on(fax.el, '.fk-san')).toBe(false); fax.update(snap({ shippedTotal: { 'anchor-slab': 4 } })); expect(on(fax.el, '.fk-san')).toBe(true); expect(q(fax.el, '.fk-san .fk-chip').textContent).toBe('I-FRAME4'); }); }); describe('NEXT IN TRAY', () => { it('reads snap.commissionQueue as truth: active first, next is queue[1]', () => { const fax = createFax(DATA); fax.update(snap({ activeCommission: 'first-taste', commissionQueue: ['first-taste', 'patio-resurfacing', 'dust-allowance'], })); expect(on(fax.el, '.fk-fax-next')).toBe(true); expect(q(fax.el, '.fk-fax-next-name').textContent).toBe('PATIO RESURFACING'); }); it('follows the queue after a standing order is re-queued to the back', () => { // The exact case the data-order fallback gets wrong. const fax = createFax(DATA); fax.update(snap({ activeCommission: 'patio-resurfacing', commissionQueue: ['patio-resurfacing', 'dust-allowance'], })); expect(q(fax.el, '.fk-fax-next-name').textContent).toBe('DUST ALLOWANCE'); }); it('hides the peek when the queue holds nothing but the active order', () => { const fax = createFax(DATA); fax.update(snap({ activeCommission: 'first-taste', commissionQueue: ['first-taste'] })); expect(on(fax.el, '.fk-fax-next')).toBe(false); }); it('shows no peek at all rather than guessing when the queue is absent', () => { // The round-2 fallback inferred "next" from data order; it was wrong as soon as a // standing order was re-queued to the back. A silent fax beats a lying one. const fax = createFax(DATA); fax.update(snap({ activeCommission: 'first-taste' })); expect(on(fax.el, '.fk-fax-next')).toBe(false); }); });