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(); }); });