import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { mount } from '@vue/test-utils'; import { describe, expect, it } from 'vitest'; import { defineComponent, h, nextTick, shallowRef } from 'vue'; import { useRouteLog } from '@/composables/useRouteLog'; import { maintenanceProgress } from '@/journey/maintenance-progress'; import { createRewindLatch, fitCaption, milestoneIndex, milestonesFor, parseDates, passPoint, ROUTE_LOG_BUDGET, routeLogSubline, sectionLocalProgress, } from '@/journey/route-log'; import type { JourneySnapshot } from '@/journey/types'; import type { PortfolioContent, SectionId } from '@/content/types'; const content = JSON.parse( readFileSync(resolve(process.cwd(), 'content/public/portfolio-content.json'), 'utf8'), ) as PortfolioContent; const at = (activeSectionId: SectionId, active: number, next: number | null) => ({ activeSectionId, sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh: active, nextHeadingOffsetVh: next }, }); describe('section-local progress', () => { it('runs 0..1 between the chapter heading and the next, null elsewhere', () => { expect(sectionLocalProgress(at('experience', 0, 2), 'experience')).toBe(0); expect(sectionLocalProgress(at('experience', -1, 1), 'experience')).toBeCloseTo(0.5); expect(sectionLocalProgress(at('experience', -3, -1), 'experience')).toBe(1); expect(sectionLocalProgress(at('experience', -1, 1), 'profile')).toBeNull(); expect(sectionLocalProgress(at('profile', 0, null), 'profile')).toBeNull(); expect(sectionLocalProgress(at('profile', 1, 1), 'profile')).toBeNull(); expect(sectionLocalProgress(at('profile', Number.NaN, 1), 'profile')).toBeNull(); }); it('still backs the 03:12 window', () => { expect(maintenanceProgress(at('infrastructure', -1, 1))).toBeCloseTo(0.5); expect(maintenanceProgress(at('experience', -1, 1))).toBeNull(); }); }); describe('milestones from the real content', () => { it('lists three roles and the systems plate for Experience', () => { const items = milestonesFor(content, 'experience'); expect(items).toHaveLength(4); expect(items.map((item) => item.index)).toEqual([0, 1, 2, 3]); expect(items[0]).toMatchObject({ dateLabel: 'AUG 2025 → PRESENT', compactDate: '2025→PRESENT', caption: 'TRUPA LUI RĂZUȘ' }); expect(items[1]).toMatchObject({ dateLabel: 'JUN 2025 → AUG 2025', compactDate: 'JUN→AUG 2025' }); expect(items[2]).toMatchObject({ dateLabel: 'JUN 2025 → AUG 2025', caption: 'ISENSE SOLUTIONS' }); expect(items[3]).toMatchObject({ dateLabel: '', caption: 'SYSTEMS OPERATIONS' }); }); it('lists the three screening facts for Profile', () => { const items = milestonesFor(content, 'profile'); expect(items.map((item) => item.caption)).toEqual(['EDUCATION', 'LANGUAGES', 'CERTIFICATIONS']); expect(items.map((item) => item.dateLabel)).toEqual(['2023 → 2026', '', '2022 · 2025']); }); it('has no posts in other chapters', () => { expect(milestonesFor(content, 'toolkit')).toEqual([]); expect(routeLogSubline(0.5, milestonesFor(content, 'toolkit'))).toBeNull(); expect(routeLogSubline(0.5, milestonesFor(content, 'experience').slice(0, 1))).toBeNull(); }); it('parses spans, open spans and year lists', () => { expect(parseDates('Jan 2024 - Present')).toEqual({ dateLabel: 'JAN 2024 → PRESENT', compactDate: '2024→PRESENT' }); expect(parseDates('2019–2021')).toEqual({ dateLabel: '2019 → 2021', compactDate: '2019→2021' }); expect(parseDates('Remote')).toEqual({ dateLabel: '', compactDate: '' }); }); }); describe('the ROUTE LOG sub-line', () => { const experience = milestonesFor(content, 'experience'); const profile = milestonesFor(content, 'profile'); it('steps through the posts at the middle of each share', () => { expect(passPoint(0, 4)).toBe(0.125); expect(passPoint(3, 4)).toBe(0.875); expect(milestoneIndex(0, 4)).toBe(0); expect(milestoneIndex(0.26, 4)).toBe(1); expect(milestoneIndex(1, 4)).toBe(3); expect(routeLogSubline(0.3, experience)).toBe('LOG 02/04 · JUN 2025 → AUG 2025 · CCF'); expect(routeLogSubline(0.6, experience)).toBe('LOG 03/04 · JUN 2025 → AUG 2025 · ISENSE SOLUTIONS'); expect(routeLogSubline(0.95, experience)).toBe('LOG 04/04 · SYSTEMS OPERATIONS'); expect(routeLogSubline(0.4, profile)).toBe('LOG 02/03 · LANGUAGES'); expect(routeLogSubline(0.1, profile)).toBe('LOG 01/03 · 2023 → 2026 · EDUCATION'); }); it('adds REWIND while scrolling back', () => { expect(routeLogSubline(0.6, experience, true)).toMatch(/^LOG 03\/04 · JUN 2025 → AUG 2025 · .+ · REWIND$/u); // A narrow plate rewinding has no room for the date: the index and caption stay. expect(routeLogSubline(0.1, experience, true, 'compact')).toBe('LOG 01/04 · TLR · REWIND'); }); it('fits the narrow plates (phones) in the budget, shortening the organisation', () => { for (const items of [experience, profile]) { for (let p = 0; p <= 1; p += 0.05) { for (const reversing of [false, true]) { expect(routeLogSubline(p, items, reversing, 'compact')!.length).toBeLessThanOrEqual(ROUTE_LOG_BUDGET.compact); expect(routeLogSubline(p, items, reversing, 'wide')!.length).toBeLessThanOrEqual(ROUTE_LOG_BUDGET.wide); } } } expect(routeLogSubline(0.1, experience, false, 'compact')).toBe('LOG 01/04 · 2025→PRESENT · TLR'); // Only 'IS' would fit after the date, so the caption takes the date's place. expect(routeLogSubline(0.6, experience, false, 'compact')).toBe('LOG 03/04 · ISENSE SOLUTIONS'); expect(fitCaption('CAMERA CONSULTANȚILOR FISCALI', 10)).toBe('CCF'); expect(fitCaption('ISENSE SOLUTIONS', 2)).toBe('IS'); expect(fitCaption('CERTIFICATIONS', 4)).toBe(''); }); it('latches a rewind while progress falls, per chapter', () => { const latch = createRewindLatch(); expect(latch(0.2, 'experience')).toBe(false); expect(latch(0.4, 'experience')).toBe(false); expect(latch(0.3, 'experience')).toBe(true); expect(latch(0.3005, 'experience')).toBe(true); expect(latch(0.5, 'experience')).toBe(false); // Profile ends at 1 and Experience starts at 0: a new chapter is not a rewind. expect(latch(1, 'profile')).toBe(false); expect(latch(0, 'experience')).toBe(false); expect(latch(0.1, 'experience')).toBe(false); expect(latch(null)).toBe(false); }); }); describe('useRouteLog', () => { const snapshot = (activeSectionId: SectionId, a: number, n: number | null): JourneySnapshot => ({ scrollY: 0, routeProgress: 0.3, direction: 0, acceleration: 0, driveSpeed: 0, activeSectionId, chapter: 'systems', sign: null, arrived: false, sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh: a, nextHeadingOffsetVh: n }, }); it('sets html[data-route-log] and the sub-line only in its chapters, then cleans up', async () => { const journey = shallowRef(snapshot('start', -1, 1)); let subline = shallowRef(''); const wrapper = mount(defineComponent({ setup() { subline = useRouteLog(journey, () => content) as typeof subline; return () => h('span', subline.value); }, })); const root = document.documentElement; await nextTick(); expect(root.dataset.routeLog).toBeUndefined(); expect(subline.value).toBe(''); journey.value = snapshot('experience', -0.2, 1.8); await nextTick(); expect(root.dataset.routeLog).toBe('1/4'); expect(subline.value).toMatch(/^LOG 01\/04 · /u); journey.value = snapshot('experience', -1.2, 0.8); await nextTick(); expect(root.dataset.routeLog).toBe('3/4'); expect(subline.value).not.toMatch(/REWIND/u); journey.value = snapshot('experience', -0.8, 1.2); await nextTick(); expect(root.dataset.routeLog).toBe('2/4'); expect(subline.value).toMatch(/^LOG 02\/04 · .* · REWIND$/u); journey.value = snapshot('infrastructure', -0.5, 1.5); await nextTick(); expect(root.dataset.routeLog).toBeUndefined(); expect(subline.value).toBe(''); journey.value = snapshot('profile', -1, 1); await nextTick(); expect(root.dataset.routeLog).toBe('2/3'); wrapper.unmount(); expect(root.dataset.routeLog).toBeUndefined(); }); });