Files
Portfolio/tests/unit/maintenance-progress.test.ts

148 lines
5.7 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 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(0.899)).toBe('verify');
expect(maintenanceStep(0.9)).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(0.9, 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();
});
});