import { describe, expect, it } from 'vitest'; import { createSignController } from '@/journey/sign-controller'; import { SIGN_ROUTE_ORDER, signRouteFor } from '@/journey/sign-routes'; import type { SignControllerSample } from '@/journey/types'; const sample = ( overrides: Partial = {}, ): SignControllerSample => ({ now: 0, source: 'passive-scroll', navigationEpoch: 0, navigationDestinationId: null, direction: 1, acceleration: 0, deltaY: 16, viewportWidth: 1_440, viewportHeight: 800, crossedBoundaries: 0, candidateId: 'profile', candidateDistanceVh: 0.5, activeSectionId: 'start', motionAllowed: true, ...overrides, }); describe('clock-driven sign lifecycle', () => { it('defines one explicit route marker and safe corridor for every eligible destination', () => { expect(SIGN_ROUTE_ORDER.map((id) => signRouteFor(id))).toEqual([ { id: 'start', label: 'Start', marker: '01', eligible: false, corridor: null, triggerMaximumVh: 0 }, { id: 'profile', label: 'Profile', marker: '02', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 }, { id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 }, { id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, dwellOffsetX: -1.6 }, { id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 }, { id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55 }, { id: 'toolkit', label: 'Toolkit', marker: '07', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55 }, { id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55 }, ]); }); it('advances through exact anticipate, approach, dwell, depart, and cooldown timing', () => { const controller = createSignController(); expect(controller.sample(sample())).toEqual({ targetId: 'profile', phase: 'anticipate', progress: 0, opacity: 0, viewportProfile: 'wide', }); expect(controller.needsFrame()).toBe(true); expect(controller.sample(sample({ now: 299, direction: 0 }))).toMatchObject({ phase: 'anticipate', progress: expect.closeTo(299 / 300, 10), }); expect(controller.sample(sample({ now: 300, direction: 0 }))).toMatchObject({ phase: 'approach', progress: 0, }); expect(controller.sample(sample({ now: 1_200, direction: 0 }))).toMatchObject({ phase: 'dwell', progress: 1, opacity: 1, }); expect(controller.sample(sample({ now: 2_599, direction: 0, activeSectionId: 'profile' }))).toMatchObject({ phase: 'dwell', }); expect(controller.sample(sample({ now: 2_600, direction: 0, activeSectionId: 'profile' }))).toMatchObject({ phase: 'depart', progress: 0, opacity: 1, }); expect(controller.sample(sample({ now: 3_299, direction: 0, activeSectionId: 'profile' }))).toMatchObject({ phase: 'depart', progress: expect.closeTo(699 / 700, 10), }); expect(controller.sample(sample({ now: 3_300, direction: 0, activeSectionId: 'profile' }))).toBeNull(); expect(controller.needsFrame()).toBe(false); expect(controller.sample(sample({ now: 3_649, candidateId: 'experience', activeSectionId: 'profile', }))).toBeNull(); expect(controller.sample(sample({ now: 3_650, candidateId: 'experience', activeSectionId: 'profile', }))).toMatchObject({ targetId: 'experience', phase: 'anticipate' }); }); it('shortens approach continuously to 650ms at full acceleration', () => { const controller = createSignController(); controller.sample(sample({ acceleration: 1 })); expect(controller.sample(sample({ now: 300, direction: 0 }))).toMatchObject({ phase: 'approach', progress: 0, }); expect(controller.sample(sample({ now: 949, direction: 0 }))).toMatchObject({ phase: 'approach', progress: expect.closeTo(649 / 650, 10), }); expect(controller.sample(sample({ now: 950, direction: 0 }))).toMatchObject({ phase: 'dwell', progress: 1, }); }); it('uses the portrait section gap instead of latching behind a full-width panel', () => { const controller = createSignController(); expect(controller.sample(sample({ viewportWidth: 390, viewportHeight: 844, candidateDistanceVh: 0.3, }))).toBeNull(); expect(controller.sample(sample({ now: 16, viewportWidth: 390, viewportHeight: 844, candidateDistanceVh: 0.22, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', viewportProfile: 'tall' }); }); it('uses the short-landscape section gap when no horizontal corridor is wide enough', () => { const controller = createSignController(); expect(controller.sample(sample({ viewportWidth: 844, viewportHeight: 390, candidateDistanceVh: 0.42, }))).toBeNull(); expect(controller.sample(sample({ now: 16, viewportWidth: 844, viewportHeight: 390, candidateDistanceVh: 0.38, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', viewportProfile: 'short' }); }); it('keeps the latched viewport profile immutable across a responsive resize', () => { const controller = createSignController(); expect(controller.sample(sample())).toMatchObject({ viewportProfile: 'wide' }); expect(controller.sample(sample({ now: 500, direction: 0, deltaY: 0, viewportWidth: 844, viewportHeight: 390, }))).toMatchObject({ targetId: 'profile', viewportProfile: 'wide' }); }); it('departs after the minimum dwell even when scrolling stops before the target activates', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0 })); expect(controller.sample(sample({ now: 2_599, direction: 0, deltaY: 0 }))).toMatchObject({ phase: 'dwell' }); expect(controller.needsFrame()).toBe(true); expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }))).toMatchObject({ phase: 'depart', progress: 0, }); expect(controller.needsFrame()).toBe(true); }); it('cancels a concealed sign on reverse but preserves the full dwell for a visible approach', () => { const early = createSignController(); early.sample(sample()); expect(early.sample(sample({ now: 310, direction: -1, deltaY: -20 }))).toBeNull(); expect(early.needsFrame()).toBe(false); const readable = createSignController(); readable.sample(sample()); expect(readable.sample(sample({ now: 700, direction: -1, deltaY: -20 }))) .toMatchObject({ phase: 'approach' }); expect(readable.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }))) .toMatchObject({ phase: 'dwell' }); expect(readable.sample(sample({ now: 2_599, direction: 0, deltaY: 0 }))) .toMatchObject({ phase: 'dwell' }); expect(readable.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }))) .toMatchObject({ phase: 'depart', progress: 0 }); }); it('preserves the minimum readable dwell when reverse input requests departure', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0 })); expect(controller.sample(sample({ now: 1_500, direction: -1, deltaY: -20 }))).toMatchObject({ phase: 'dwell', }); expect(controller.sample(sample({ now: 2_599, direction: 0, deltaY: 0 }))).toMatchObject({ phase: 'dwell', }); expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }))).toMatchObject({ phase: 'depart', progress: 0, }); }); it.each([ ['history restoration', { source: 'history-restoration' as const }], ['initial positioning', { source: 'initial' as const }], ['disabled motion', { motionAllowed: false }], ])('suppresses or clears concealed signs for %s', (_label, overrides) => { const controller = createSignController(); controller.sample(sample()); expect(controller.sample(sample({ now: 200, ...overrides }))).toBeNull(); expect(controller.needsFrame()).toBe(false); }); it('consumes an eligible navigator destination once without requiring scroll input', () => { const controller = createSignController(); const navigation = sample({ source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'selected-builds', direction: 0, deltaY: 0, crossedBoundaries: 0, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 2, }); expect(controller.sample(navigation)).toMatchObject({ targetId: 'selected-builds', phase: 'anticipate', navigationArrival: true, }); expect(controller.sample({ ...navigation, now: 1_200 })).toMatchObject({ targetId: 'selected-builds', phase: 'dwell', }); expect(controller.sample({ ...navigation, now: 2_600 })).toMatchObject({ targetId: 'selected-builds', phase: 'depart', progress: 0, }); expect(controller.sample({ ...navigation, now: 3_300 })).toBeNull(); expect(controller.sample({ ...navigation, now: 4_000 })).toBeNull(); expect(controller.needsFrame()).toBe(false); }); it('coalesces rapid navigator clicks to the latest concealed target without label mutation', () => { const controller = createSignController(); expect(controller.sample(sample({ source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate' }); expect(controller.sample(sample({ now: 100, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'selected-builds', direction: 0, deltaY: 0, }))).toMatchObject({ targetId: 'selected-builds', phase: 'anticipate' }); expect(controller.sample(sample({ now: 1_300, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'selected-builds', direction: 0, deltaY: 0, }))).toMatchObject({ targetId: 'selected-builds', phase: 'dwell' }); }); it('does not queue a second presentation for a repeated same-target navigator click', () => { const controller = createSignController(); const navigation = sample({ source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'selected-builds', direction: 0, deltaY: 0, }); controller.sample(navigation); controller.sample({ ...navigation, now: 1_200 }); expect(controller.sample({ ...navigation, now: 1_300, navigationEpoch: 2, })).toMatchObject({ targetId: 'selected-builds', phase: 'dwell' }); controller.sample({ ...navigation, now: 2_600, navigationEpoch: 2 }); controller.sample({ ...navigation, now: 3_300, navigationEpoch: 2 }); expect(controller.sample({ ...navigation, now: 3_650, navigationEpoch: 2, })).toBeNull(); expect(controller.sample({ ...navigation, now: 4_000, navigationEpoch: 3, activeSectionId: 'selected-builds', })).toBeNull(); expect(controller.needsFrame()).toBe(false); }); it('promotes a rearmed passive sign when the prior navigator target is clicked again', () => { const controller = createSignController(); const navigation = sample({ source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, }); controller.sample(navigation); controller.sample({ ...navigation, now: 1_200, activeSectionId: 'profile' }); controller.sample({ ...navigation, now: 2_600, activeSectionId: 'profile' }); controller.sample({ ...navigation, now: 3_300, activeSectionId: 'profile' }); controller.sample(sample({ now: 3_400, direction: -1, deltaY: -800, crossedBoundaries: 1, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, })); controller.sample(sample({ now: 3_650, direction: 1, deltaY: 120, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, })); controller.sample(sample({ now: 4_850, direction: 0, deltaY: 0, activeSectionId: 'profile', candidateId: 'experience', })); expect(controller.sample({ ...navigation, now: 4_900, navigationEpoch: 2, activeSectionId: 'profile', })).toMatchObject({ targetId: 'profile', phase: 'dwell', navigationArrival: true, }); }); it('preserves a full readable dwell when navigation promotes a late passive dwell', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); const promoted = sample({ now: 2_590, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', }); expect(controller.sample(promoted)).toMatchObject({ targetId: 'profile', phase: 'dwell', navigationArrival: true, }); expect(controller.sample({ ...promoted, now: 3_989 })).toMatchObject({ phase: 'dwell', navigationArrival: true, }); expect(controller.sample({ ...promoted, now: 3_990 })).toMatchObject({ phase: 'depart', progress: 0, navigationArrival: true, }); }); it('promotes a rearmed sign queued during cooldown when its navigator target is clicked', () => { const controller = createSignController(); const navigation = sample({ source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, }); controller.sample(navigation); controller.sample({ ...navigation, now: 1_200, activeSectionId: 'profile' }); controller.sample({ ...navigation, now: 2_600, activeSectionId: 'profile' }); controller.sample(sample({ now: 2_800, direction: -1, deltaY: -800, crossedBoundaries: 1, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, })); expect(controller.sample(sample({ now: 3_400, direction: 1, deltaY: 120, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, }))).toBeNull(); expect(controller.needsFrame()).toBe(true); expect(controller.sample({ ...navigation, now: 3_450, navigationEpoch: 2, activeSectionId: 'profile', })).toBeNull(); expect(controller.sample({ ...navigation, now: 3_650, navigationEpoch: 2, activeSectionId: 'profile', })).toMatchObject({ targetId: 'profile', phase: 'anticipate', navigationArrival: true, }); }); it('preserves a readable label and queues only the latest navigator destination', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 1_300, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'experience', direction: 0, deltaY: 0, }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 1_400, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'toolkit', direction: 0, deltaY: 0, }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); controller.sample(sample({ now: 2_600, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'toolkit', direction: 0, deltaY: 0, })); controller.sample(sample({ now: 3_300, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'toolkit', direction: 0, deltaY: 0, })); expect(controller.sample(sample({ now: 3_650, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'toolkit', direction: 0, deltaY: 0, }))).toMatchObject({ targetId: 'toolkit', phase: 'anticipate', navigationArrival: true, }); }); it('cancels an obsolete queued destination when navigation returns to the visible target', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 1_300, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'toolkit', direction: 0, deltaY: 0, activeSectionId: 'toolkit', }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 1_400, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'profile', phase: 'dwell', navigationArrival: true, }); controller.sample(sample({ now: 2_800, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', })); controller.sample(sample({ now: 3_500, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', })); expect(controller.sample(sample({ now: 3_850, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toBeNull(); expect(controller.needsFrame()).toBe(false); expect(controller.sample(sample({ now: 3_900, direction: -1, deltaY: -40, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.6, }))).toBeNull(); expect(controller.sample(sample({ now: 4_000, direction: 1, deltaY: 40, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, }))).toBeNull(); }); it('honors a navigator return to a previously completed route after a readable sign', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_650, activeSectionId: 'profile', candidateId: 'experience', candidateDistanceVh: 0.5, })); controller.sample(sample({ now: 4_850, direction: 0, deltaY: 0, activeSectionId: 'profile', candidateId: 'experience', })); expect(controller.sample(sample({ now: 5_000, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'experience', phase: 'dwell' }); controller.sample(sample({ now: 6_250, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', })); controller.sample(sample({ now: 6_950, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', })); expect(controller.sample(sample({ now: 7_300, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', navigationArrival: true, }); }); it('queues a fresh navigator arrival for the cooldown route after moving elsewhere', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_400, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'experience', }))).toBeNull(); expect(controller.sample(sample({ now: 3_650, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', navigationArrival: true, }); }); it.each([ ['multiple crossed sections', { crossedBoundaries: 2 }], ['a jump over 0.75 viewport heights', { deltaY: 601 }], ])('coalesces %s while preserving the readable dwell', (_label, overrides) => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0 })); expect(controller.sample(sample({ now: 1_400, ...overrides }))).toMatchObject({ targetId: 'profile', phase: 'dwell', }); expect(controller.needsFrame()).toBe(true); expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0, opacity: 1, }); }); it('preserves approach and minimum dwell when the authoritative section passes the target', () => { const controller = createSignController(); controller.sample(sample()); expect(controller.sample(sample({ now: 700, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ targetId: 'profile', phase: 'approach', }); expect(controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 2_599, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 }); }); it('does not truncate dwell when the authoritative section passes the target', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0 })); expect(controller.sample(sample({ now: 1_350, direction: 1, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 2_599, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ phase: 'dwell' }); expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', }))).toMatchObject({ phase: 'depart', progress: 0 }); }); it('retargets a changed far-away candidate only while the sign is concealed', () => { const controller = createSignController(); controller.sample(sample()); expect(controller.sample(sample({ now: 100, candidateId: 'experience', }))).toMatchObject({ targetId: 'experience', phase: 'anticipate', progress: 0, opacity: 0, }); }); it('does not impose cooldown when an idle initial sample precedes real passive input', () => { const controller = createSignController(); expect(controller.sample(sample({ source: 'initial' }))).toBeNull(); expect(controller.sample(sample({ now: 100 }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', }); }); it.each([ ['reverse input', { direction: -1 as const, deltaY: -1 }], ['stationary input', { direction: 0 as const, deltaY: 0 }], ['candidate before the approach window', { candidateDistanceVh: 0.550_001 }], ['candidate already passed', { candidateDistanceVh: 0 }], ['candidate is already active', { activeSectionId: 'profile' as const }], ['missing candidate', { candidateId: null }], ])('does not latch for %s', (_label, overrides) => { const controller = createSignController(); expect(controller.sample(sample(overrides))).toBeNull(); }); it('keeps one target latched when the next candidate changes', () => { const controller = createSignController(); controller.sample(sample()); expect(controller.sample(sample({ now: 600, direction: 0, candidateId: 'experience', }))).toMatchObject({ targetId: 'profile', phase: 'approach' }); }); it('resets malformed samples to a finite idle state', () => { const controller = createSignController(); controller.sample(sample()); expect(controller.sample(sample({ now: Number.NaN }))).toBeNull(); expect(controller.needsFrame()).toBe(false); controller.reset(); expect(controller.sample(sample({ now: 100 }))).toMatchObject({ phase: 'anticipate' }); }); it('resolves a rapid multi-section jump to the latest active destination', () => { const controller = createSignController(); expect(controller.sample(sample({ now: 20, deltaY: 2_400, crossedBoundaries: 4, activeSectionId: 'selected-builds', candidateId: 'additional-work', candidateDistanceVh: 0.4, acceleration: 1, }))).toMatchObject({ targetId: 'selected-builds', phase: 'anticipate', }); }); it('coalesces changing jump targets and reveals only the latest after a readable stale sign departs', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 1_300, deltaY: 2_400, crossedBoundaries: 4, activeSectionId: 'selected-builds', candidateId: 'additional-work', candidateDistanceVh: 0.4, }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 1_500, deltaY: 1_200, crossedBoundaries: 2, activeSectionId: 'toolkit', candidateId: 'contact', candidateDistanceVh: 0.35, }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0, crossedBoundaries: 0, activeSectionId: 'toolkit', candidateId: 'contact', candidateDistanceVh: 0.35, }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 }); expect(controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0, crossedBoundaries: 0, activeSectionId: 'toolkit', candidateId: 'contact', candidateDistanceVh: 0.35, }))).toBeNull(); expect(controller.sample(sample({ now: 3_650, direction: 0, deltaY: 0, crossedBoundaries: 0, activeSectionId: 'toolkit', candidateId: 'contact', candidateDistanceVh: 0.35, }))).toMatchObject({ targetId: 'toolkit', phase: 'anticipate' }); }); it('retains the latest jump target first observed on the departure-completion frame', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_300, deltaY: 2_400, crossedBoundaries: 4, activeSectionId: 'selected-builds', candidateId: 'additional-work', candidateDistanceVh: 0.4, }))).toBeNull(); expect(controller.sample(sample({ now: 3_650, direction: 0, deltaY: 0, crossedBoundaries: 0, activeSectionId: 'selected-builds', candidateId: 'additional-work', candidateDistanceVh: 0.4, }))).toMatchObject({ targetId: 'selected-builds', phase: 'anticipate' }); }); it('starts the readable dwell clock on the first rendered dwell frame after a delayed gap', () => { const controller = createSignController(); controller.sample(sample()); expect(controller.sample(sample({ now: 5_000, direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 6_399, direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'profile', phase: 'dwell' }); expect(controller.sample(sample({ now: 6_400, direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 }); }); it('keeps a passed sign opaque until its spatial departure is well underway', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 2_985, direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ phase: 'depart', progress: 0.55, opacity: 1, }); expect(controller.sample(sample({ now: 3_230, direction: 0, deltaY: 0, activeSectionId: 'profile', }))).toMatchObject({ phase: 'depart', progress: 0.9, opacity: expect.closeTo(0.04, 1), }); }); it('discards a queued forward jump after a meaningful reverse retreat', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 1_300, deltaY: 2_400, crossedBoundaries: 4, activeSectionId: 'selected-builds', candidateId: 'additional-work', candidateDistanceVh: 0.4, })); controller.sample(sample({ now: 1_500, direction: -1, deltaY: -2_400, crossedBoundaries: 4, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_650, direction: 0, deltaY: 0, activeSectionId: 'start', candidateId: 'selected-builds', candidateDistanceVh: 0.4, }))).toBeNull(); }); it('does not retrigger a completed route sign during small boundary oscillations', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_700, direction: -1, deltaY: -16, crossedBoundaries: 1, activeSectionId: 'start', }))).toBeNull(); expect(controller.sample(sample({ now: 3_800, direction: 1, deltaY: 16, crossedBoundaries: 1, activeSectionId: 'profile', candidateId: 'experience', }))).toBeNull(); }); it('re-arms a completed sign only after retreating beyond the trigger hysteresis', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_500, direction: -1, deltaY: -600, crossedBoundaries: 1, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, }))).toBeNull(); expect(controller.sample(sample({ now: 3_650, direction: 1, deltaY: 120, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate' }); }); it('does not let a departing sign overwrite a meaningful reverse rearm', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 2_800, direction: -1, deltaY: -800, crossedBoundaries: 1, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, }))).toMatchObject({ targetId: 'profile', phase: 'depart' }); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, })); expect(controller.sample(sample({ now: 3_400, direction: 1, deltaY: 120, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, }))).toBeNull(); expect(controller.needsFrame()).toBe(true); expect(controller.sample(sample({ now: 3_650, direction: 0, deltaY: 0, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate' }); }); it('re-arms every later route after one large reverse retreat', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_650, activeSectionId: 'profile', candidateId: 'experience', candidateDistanceVh: 0.5, })); controller.sample(sample({ now: 4_850, direction: 0, deltaY: 0, activeSectionId: 'profile', candidateId: 'experience', })); controller.sample(sample({ now: 6_250, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', })); controller.sample(sample({ now: 6_950, direction: 0, deltaY: 0, activeSectionId: 'experience', candidateId: 'infrastructure', })); controller.sample(sample({ now: 7_100, direction: -1, deltaY: -2_400, crossedBoundaries: 2, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.8, })); controller.sample(sample({ now: 7_300 })); controller.sample(sample({ now: 8_500, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 9_900, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 10_600, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 10_950, activeSectionId: 'profile', candidateId: 'experience', candidateDistanceVh: 0.5, }))).toMatchObject({ targetId: 'experience', phase: 'anticipate' }); }); it.each([ ['an unrequested explicit jump', 'explicit-navigation' as const], ['history restoration', 'history-restoration' as const], ])('starts a fresh passive traversal after %s', (_label, source) => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_400, source, navigationEpoch: 1, navigationDestinationId: null, direction: 0, deltaY: 0, activeSectionId: 'start', candidateId: null, candidateDistanceVh: null, }))).toBeNull(); expect(controller.sample(sample({ now: 3_500, source: 'passive-scroll', navigationEpoch: 2, direction: 1, deltaY: 120, activeSectionId: 'start', candidateId: 'profile', candidateDistanceVh: 0.5, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate' }); }); it('starts a fresh passive sign traversal after explicit navigation', () => { const controller = createSignController(); controller.sample(sample()); controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 })); controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 })); expect(controller.sample(sample({ now: 3_400, source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'contact', activeSectionId: 'contact', candidateId: null, candidateDistanceVh: null, }))).toBeNull(); expect(controller.sample(sample({ now: 3_500, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'start', activeSectionId: 'start', }))).toBeNull(); expect(controller.sample(sample({ now: 3_600 }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', }); }); it('does not let a stale navigator sign re-complete a traversal restarted at Start', () => { const controller = createSignController(); const profileNavigation = sample({ source: 'explicit-navigation', navigationEpoch: 1, navigationDestinationId: 'profile', candidateId: null, candidateDistanceVh: null, }); controller.sample(profileNavigation); controller.sample({ ...profileNavigation, now: 1_200, direction: 0, deltaY: 0, activeSectionId: 'profile', }); controller.sample({ ...profileNavigation, now: 2_600, direction: 0, deltaY: 0, activeSectionId: 'profile', }); expect(controller.sample(sample({ now: 2_800, source: 'explicit-navigation', navigationEpoch: 2, navigationDestinationId: 'start', direction: 0, deltaY: 0, activeSectionId: 'start', candidateId: null, candidateDistanceVh: null, }))).toMatchObject({ targetId: 'profile', phase: 'depart' }); expect(controller.sample(sample({ now: 2_900, viewportWidth: 390, viewportHeight: 844, candidateDistanceVh: 0.22, }))).toMatchObject({ targetId: 'profile', phase: 'depart' }); expect(controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0, activeSectionId: 'start', }))).toBeNull(); expect(controller.sample(sample({ now: 3_650, direction: 0, deltaY: 0, viewportWidth: 390, viewportHeight: 844, }))).toMatchObject({ targetId: 'profile', phase: 'anticipate', }); }); });