import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { mount } from '@vue/test-utils'; import { afterEach, describe, expect, it } from 'vitest'; import { defineComponent, h, nextTick, shallowRef } from 'vue'; import { useRelay } from '@/composables/useRelay'; import { BEACON_HZ, publishRelay, readRelay, RELAY_STOPS, relayOnline, rungLit } from '@/journey/relay-progress'; import type { JourneySnapshot } from '@/journey/types'; import type { PortfolioContent, SectionId } from '@/content/types'; const content = JSON.parse( readFileSync(resolve(process.cwd(), 'content/public/portfolio-content.json'), 'utf8'), ) as PortfolioContent; const counts = content.toolkit.map((group) => group.technologies.length); const sweep = (from: number, to: number, steps = 400): number[] => Array.from({ length: steps + 1 }, (_, i) => from + ((to - from) * i) / steps); afterEach(() => publishRelay([])); describe('relay progress', () => { it('has one stop per group and blinks well under 3 Hz', () => { expect(RELAY_STOPS).toHaveLength(4); expect(counts).toHaveLength(4); expect(BEACON_HZ).toBeLessThanOrEqual(3); }); it('brings masts online monotonically and takes them offline in reverse', () => { const forward = sweep(0, 1).map(relayOnline); forward.forEach((n, i) => { if (i > 0) expect(n).toBeGreaterThanOrEqual(forward[i - 1]!); }); const backward = [...sweep(0, 1)].reverse().map(relayOnline); expect(backward).toEqual([...forward].reverse()); expect(relayOnline(null)).toBe(0); expect(relayOnline(0)).toBe(0); expect(relayOnline(0.6)).toBe(4); }); it('lights every rung monotonically and reversibly, all lit by the end of the ramp', () => { counts.forEach((count, mast) => { for (let rung = 0; rung < count; rung += 1) { const forward = sweep(0, 1).map((p) => rungLit(p, mast, rung, count)); forward.forEach((v, i) => { if (i > 0) expect(v).toBeGreaterThanOrEqual(forward[i - 1]!); }); expect([...sweep(0, 1)].reverse().map((p) => rungLit(p, mast, rung, count))).toEqual([...forward].reverse()); expect(rungLit(RELAY_STOPS[mast]! - 0.001, mast, rung, count)).toBe(0); expect(rungLit(0.71, mast, rung, count)).toBe(1); } }); expect(rungLit(null, 0, 0, 3)).toBe(0); }); it('lights rungs bottom-up in index order', () => { for (const p of sweep(0, 1)) { counts.forEach((count, mast) => { for (let rung = 1; rung < count; rung += 1) { expect(rungLit(p, mast, rung, count)).toBeLessThanOrEqual(rungLit(p, mast, rung - 1, count)); } }); } }); it('carries the rung counts from the DOM to the scene', () => { expect(readRelay().counts).toEqual([]); publishRelay(counts); expect(readRelay().counts).toEqual([11, 8, 4, 3]); publishRelay([1, 2, 3, 4, 5]); expect(readRelay().counts).toHaveLength(4); }); }); describe('useRelay', () => { const snapshot = (activeSectionId: SectionId, a: number, n: number | null): JourneySnapshot => ({ scrollY: 0, routeProgress: 0.8, direction: 0, acceleration: 0, driveSpeed: 0, activeSectionId, chapter: 'systems', sign: null, arrived: false, sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh: a, nextHeadingOffsetVh: n }, } as unknown as JourneySnapshot); it('sets html[data-relay-online] through Toolkit only, publishes counts, and cleans up', async () => { const journey = shallowRef(snapshot('additional-work', -0.5, 1.5)); const wrapper = mount(defineComponent({ setup() { useRelay(journey, () => content.toolkit); return () => h('span'); }, })); const root = document.documentElement; await nextTick(); expect(readRelay().counts).toEqual(counts); expect(root.dataset.relayOnline).toBeUndefined(); journey.value = snapshot('toolkit', -0.1, 1.9); // p 0.05 await nextTick(); expect(root.dataset.relayOnline).toBe('1'); journey.value = snapshot('toolkit', -1, 1); // p 0.5 await nextTick(); expect(root.dataset.relayOnline).toBe('3'); journey.value = snapshot('toolkit', -1.3, 0.7); // p 0.65 await nextTick(); expect(root.dataset.relayOnline).toBe('4'); journey.value = snapshot('toolkit', -0.5, 1.5); // p 0.25, scrolling back await nextTick(); expect(root.dataset.relayOnline).toBe('2'); journey.value = snapshot('contact', -0.2, null); await nextTick(); expect(root.dataset.relayOnline).toBeUndefined(); journey.value = snapshot('toolkit', -1, 1); await nextTick(); expect(root.dataset.relayOnline).toBe('3'); wrapper.unmount(); expect(root.dataset.relayOnline).toBeUndefined(); expect(root.dataset.relayFocus).toBeUndefined(); }); });