feat: relay station masts come online through the toolkit chapter

This commit is contained in:
2026-10-11 08:32:33 +03:00
parent 568b0802b4
commit 33585f7e34
22 changed files with 935 additions and 2 deletions

View File

@@ -0,0 +1,119 @@
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();
});
});