188 lines
8.2 KiB
TypeScript
188 lines
8.2 KiB
TypeScript
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();
|
||
});
|
||
});
|