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.
149 lines
5.8 KiB
TypeScript
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();
|
|
});
|
|
});
|