Files
Portfolio/tests/unit/AppShell.test.ts
LucaP f46131723b feat: finish portfolio title, navigation, and destination effects
Add the approved name animation, rounded docking navigation, localized keyboard focus cue, unified builds card, and destination copy hierarchy.

Introduce lightweight one-shot meteors and a sun ring that repeats every three seconds at Contact. Open the email action in a separate browsing context. Preserve reduced-motion behavior and keep private handoff and visual-check artifacts out of version control.

Validation: 649 tests passed; all 20 failures in world-models and SceneCanvas reproduce on baseline 103427f. Typecheck, artifact verification, and transfer budgets passed. Client/SSR preview build passed before this commit. Existing baseline test failures remain unchanged.
2026-09-05 22:16:42 +03:00

153 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { validateProductionInputs } from '@/content/schemas';
import { render } from '@/entry-server';
import type { SectionId } from '@/content/types';
const fixture = (name: string): unknown =>
JSON.parse(readFileSync(resolve(process.cwd(), 'tests/fixtures', name), 'utf8'));
const validInputs = validateProductionInputs(
fixture('portfolio-content.valid.json'),
fixture('asset-manifest.valid.json'),
fixture('site-config.preview.json'),
new URL('file:///prepared-assets/'),
);
const renderedDocument = async (): Promise<Document> => {
return renderedDocumentFor(validInputs);
};
const renderedDocumentFor = async (inputs: typeof validInputs): Promise<Document> => {
const document = globalThis.document.implementation.createHTMLDocument('Portfolio');
document.body.innerHTML = await render('/', inputs);
return document;
};
describe('semantic portfolio document', () => {
it('renders the complete landmark and heading reading structure on the server', async () => {
const document = await renderedDocument();
expect(document.querySelectorAll('.site-header')).toHaveLength(1);
expect(document.querySelector('nav[aria-label="Portfolio sections"]')).not.toBeNull();
expect(document.querySelectorAll('main')).toHaveLength(1);
expect(document.querySelectorAll('footer')).toHaveLength(1);
expect(document.querySelectorAll('h1')).toHaveLength(1);
expect(document.querySelector('#start h1')?.textContent).toBe('Luca Pîslaru');
expect([...document.querySelectorAll('main > section')].map((section) => section.id)).toEqual([
'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact',
]);
expect([...document.querySelectorAll('main > section')].every((section) => section.hasAttribute('aria-labelledby'))).toBe(true);
expect([...document.querySelectorAll('main > section h2')].map((heading) => heading.textContent)).toEqual([
'Profile', 'Experience', 'Infrastructure', 'Selected builds', 'Additional work', 'Toolkit', 'Contact',
]);
expect([...document.querySelectorAll('main > section')].map((section) => section.getAttribute('data-layout'))).toEqual([
'hero', 'right-rail', 'left-timeline', 'full-diagram', 'project-stage', 'compact-rail', 'toolkit-field', 'destination',
]);
expect(document.querySelectorAll('.route-label')).toHaveLength(0);
expect(document.querySelector('h2 + h4')).toBeNull();
});
it('renders the exact first-screen hierarchy and destination actions', async () => {
const document = await renderedDocument();
const hero = document.querySelector('.hero-journey__content')!;
expect([...hero.children].flatMap((element) =>
element.classList.contains('hero-journey__actions')
? [...element.children].map((action) => action.textContent?.trim())
: [element.textContent?.trim()],
)).toEqual([
'Luca Pîslaru',
'System Administrator & Full-Stack Developer',
validInputs.content.identity.valueProposition,
validInputs.content.identity.availability,
'Contact Me',
'Explore work',
]);
expect(hero.querySelector('a[href="#contact"]')?.textContent).toBe('Contact Me');
expect(document.querySelector('[data-featured-operation]')?.textContent).toContain(
validInputs.content.infrastructure.featuredOperation.title,
);
expect(document.querySelector('#contact')?.textContent).toContain('System Administrator');
expect(document.body.textContent).not.toMatch(/[–—]/u);
});
it('assigns explicit project priority without promoting research exploration', async () => {
const document = await renderedDocument();
const priorities = [...document.querySelectorAll('#selected-builds article')].map((article) => [
article.querySelector('h3')?.textContent,
article.getAttribute('data-project-priority'),
]);
expect(priorities).toEqual([
['Private self-hosted medication tracker', 'primary'],
['Footy Tracker', 'primary'],
['Opinion Dynamics Simulation', 'supporting'],
]);
});
it('keeps native section links and visible contact available without JavaScript', async () => {
const document = await renderedDocument();
const navigation = document.querySelector('nav[aria-label="Portfolio sections"]')!;
expect([...navigation.querySelectorAll('a')].map((link) => link.getAttribute('href'))).toEqual([
'#start', '#profile', '#experience', '#infrastructure', '#selected-builds', '#additional-work', '#toolkit', '#contact',
]);
const email = document.querySelector('#contact-email-details');
expect(email?.querySelector('summary')?.textContent).toBe('Email Me');
expect(email?.textContent).toContain('hello [at] example [dot] test');
expect(document.querySelector('a[href^="mailto:"]')).toBeNull();
expect(document.body.textContent).toContain('For a current CV or role-specific information, contact me directly.');
});
it('renders one semantic navigation label with no duplicate responsive text', async () => {
const document = await renderedDocument();
const experience = document.querySelector('nav a[href="#experience"]')!;
const additional = document.querySelector('nav a[href="#additional-work"]')!;
expect(experience.hasAttribute('aria-label')).toBe(false);
expect(experience.textContent).toBe('Career');
expect(experience.childElementCount).toBe(0);
expect(additional.hasAttribute('aria-label')).toBe(false);
expect(additional.textContent).toBe('More work');
expect(additional.childElementCount).toBe(0);
});
it('binds navigation and section labels to their canonical IDs when content navigation is reordered', async () => {
const content = fixture('portfolio-content.valid.json') as {
navigation: Array<{ id: SectionId; label: string }>;
};
const navigationById = new Map(content.navigation.map((item) => [item.id, item]));
const reorderedIds: readonly SectionId[] = [
'contact', 'toolkit', 'additional-work', 'selected-builds', 'infrastructure', 'experience', 'profile', 'start',
];
content.navigation = reorderedIds.map((id) => navigationById.get(id)!);
const reorderedInputs = validateProductionInputs(
content,
fixture('asset-manifest.valid.json'),
fixture('site-config.preview.json'),
new URL('file:///prepared-assets/'),
);
const document = await renderedDocumentFor(reorderedInputs);
expect([...document.querySelectorAll('main > section h2')].map((heading) => heading.textContent)).toEqual([
'Profile', 'Experience', 'Infrastructure', 'Selected builds', 'Additional work', 'Toolkit', 'Contact',
]);
expect([...document.querySelectorAll('nav a')].map((link) => [link.getAttribute('href'), link.textContent])).toEqual([
['#start', 'Start'], ['#profile', 'Profile'], ['#experience', 'Career'], ['#infrastructure', 'Infra'],
['#selected-builds', 'Builds'], ['#additional-work', 'More work'], ['#toolkit', 'Toolkit'], ['#contact', 'Contact'],
]);
});
it('includes systems and software evidence in server-rendered content', async () => {
const document = await renderedDocument();
expect(document.body.textContent).toContain('Professional systems operations');
expect(document.body.textContent).toContain('Secure self-hosted platform and lifecycle');
expect(document.body.textContent).toContain('Private self-hosted medication tracker');
expect(document.body.textContent).toContain('Footy Tracker');
});
});