Files
Portfolio/tests/unit/maintenance-progress.test.ts
LucaP 75108562a4 polish: time the scene payoffs to the gaps between cards
One shared window (journey/card-gaps.ts, CARD_GAP) describes the open road
after each chapter's panel in section-local progress; the district relight
and cutover, the last yard lift and the relay station's beams now key off
it instead of their own numbers, and the chapter margin widens from 44vh
to 48vh so the gap reads as a stretch of road. Unit tests prove every
payoff's peak sits inside the window for every chapter span.
2026-10-12 19:20:43 +03:00

149 lines
5.8 KiB
TypeScript

import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import { defineComponent, h, nextTick, shallowRef } from 'vue';
import { useMaintenanceWindow } from '@/composables/useMaintenanceWindow';
import { CARD_GAP } from '@/journey/card-gaps';
import type { JourneySnapshot } from '@/journey/types';
import {
createMaintenanceDirection,
maintenanceClock,
maintenanceProgress,
maintenanceStep,
maintenanceSubline,
} from '@/journey/maintenance-progress';
import type { SectionId } from '@/content/types';
const at = (
activeSectionId: SectionId,
activeHeadingOffsetVh: number,
nextHeadingOffsetVh: number | null,
) => ({
activeSectionId,
sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh, nextHeadingOffsetVh },
});
const live = () => '14:07';
describe('maintenance progress', () => {
it('is 0 at the Infrastructure heading and 1 at the next heading', () => {
expect(maintenanceProgress(at('infrastructure', 0, 2.5))).toBe(0);
expect(maintenanceProgress(at('infrastructure', -1.25, 1.25))).toBeCloseTo(0.5);
expect(maintenanceProgress(at('infrastructure', -2.5, 0))).toBe(1);
});
it('clamps to the window and is null outside the chapter or without geometry', () => {
expect(maintenanceProgress(at('infrastructure', 0.2, 2.7))).toBe(0);
expect(maintenanceProgress(at('infrastructure', -3, -0.5))).toBe(1);
expect(maintenanceProgress(at('experience', -1, 1))).toBeNull();
expect(maintenanceProgress(at('selected-builds', -1, 1))).toBeNull();
expect(maintenanceProgress(at('infrastructure', 0, null))).toBeNull();
expect(maintenanceProgress(at('infrastructure', 1, 1))).toBeNull();
expect(maintenanceProgress(at('infrastructure', Number.NaN, 1))).toBeNull();
});
it('maps progress onto open, backup, transfer, verify and done', () => {
expect(maintenanceStep(0)).toBe('open');
expect(maintenanceStep(0.099)).toBe('open');
expect(maintenanceStep(0.1)).toBe('backup');
expect(maintenanceStep(0.349)).toBe('backup');
expect(maintenanceStep(0.35)).toBe('transfer');
expect(maintenanceStep(0.649)).toBe('transfer');
expect(maintenanceStep(0.65)).toBe('verify');
expect(maintenanceStep(CARD_GAP.from - 0.001)).toBe('verify');
expect(maintenanceStep(CARD_GAP.from)).toBe('done');
expect(maintenanceStep(1)).toBe('done');
});
it('holds the clock at 03:12 while the window opens, cuts over at 03:51, then goes live', () => {
expect(maintenanceClock(0, live)).toBe('03:12');
expect(maintenanceClock(0.1, live)).toBe('03:12');
expect(maintenanceClock(0.5, live)).toBe('03:32');
expect(maintenanceClock(CARD_GAP.from, live)).toBe('03:51');
expect(maintenanceClock(0.999, live)).toBe('03:51');
expect(maintenanceClock(1, live)).toBe('14:07');
let previous = -1;
for (let p = 0; p < 1; p += 0.01) {
const minutes = Number(maintenanceClock(p, live).slice(3));
expect(minutes).toBeGreaterThanOrEqual(previous);
previous = minutes;
}
});
it('writes the sub-line from the step, and a rollback while reversing', () => {
expect(maintenanceSubline(0.02, false, live)).toBe('MAINTENANCE WINDOW · 03:12');
expect(maintenanceSubline(0.92, false, live)).toBe('CUTOVER OK · 03:51');
expect(maintenanceSubline(0.5, true, live)).toMatch(/^ROLLBACK CHECKPOINT · 03:\d\d$/u);
expect(maintenanceSubline(0.97, false, live)).toBe('CUTOVER OK · 03:51');
});
it('latches a rollback while progress falls and clears it when it rises', () => {
const track = createMaintenanceDirection();
expect(track(0.2)).toBe(false);
expect(track(0.3)).toBe(false);
expect(track(0.25)).toBe(true);
expect(track(0.25)).toBe(true);
expect(track(0.2505)).toBe(true);
expect(track(0.3)).toBe(false);
expect(track(0.2)).toBe(true);
expect(track(null)).toBe(false);
expect(track(0.9)).toBe(false);
expect(track(0)).toBe(false);
});
});
describe('useMaintenanceWindow', () => {
const snapshot = (activeSectionId: SectionId, a: number, n: number | null): JourneySnapshot => ({
scrollY: 0,
routeProgress: 0.4,
direction: 0,
acceleration: 0,
driveSpeed: 0,
activeSectionId,
chapter: 'infrastructure',
sign: null,
arrived: false,
sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh: a, nextHeadingOffsetVh: n },
});
it('steps html[data-maintenance-step] and the sub-line from scroll, then cleans up', async () => {
const journey = shallowRef(snapshot('experience', -1, 1));
let subline = shallowRef('');
const wrapper = mount(defineComponent({
setup() {
subline = useMaintenanceWindow(journey) as typeof subline;
return () => h('span', subline.value);
},
}));
const root = document.documentElement;
await nextTick();
expect(root.dataset.maintenanceStep).toBeUndefined();
expect(subline.value).toBe('');
journey.value = snapshot('infrastructure', 0, 2);
await nextTick();
expect(root.dataset.maintenanceStep).toBe('open');
expect(subline.value).toBe('MAINTENANCE WINDOW \u00b7 03:12');
journey.value = snapshot('infrastructure', -1.84, 0.16);
await nextTick();
expect(root.dataset.maintenanceStep).toBe('done');
expect(subline.value).toBe('CUTOVER OK \u00b7 03:51');
journey.value = snapshot('infrastructure', -1, 1);
await nextTick();
expect(root.dataset.maintenanceStep).toBe('transfer');
expect(subline.value).toMatch(/^ROLLBACK CHECKPOINT/u);
journey.value = snapshot('selected-builds', 0, 2);
await nextTick();
expect(root.dataset.maintenanceStep).toBeUndefined();
expect(subline.value).toBe('');
journey.value = snapshot('infrastructure', -0.4, 1.6);
await nextTick();
expect(root.dataset.maintenanceStep).toBe('backup');
wrapper.unmount();
expect(root.dataset.maintenanceStep).toBeUndefined();
});
});