143 lines
7.2 KiB
TypeScript
143 lines
7.2 KiB
TypeScript
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].map((element) => element.textContent?.trim())).toEqual([
|
||
'Luca Pîslaru',
|
||
'System Administrator & Full-Stack Developer',
|
||
'Building and operating reliable systems and software.',
|
||
'Explore work',
|
||
]);
|
||
expect(hero.querySelector('a')?.getAttribute('href')).toBe('#selected-builds');
|
||
expect(document.querySelector('a[href="mailto:hello@example.test"]')?.textContent).toBe('Start a conversation');
|
||
expect(document.body.textContent).toContain("Thanks for making the trip. The road's open if you'd like to build something together.");
|
||
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('a[href="mailto:hello@example.test"]');
|
||
expect(email?.textContent).toBe('Start a conversation');
|
||
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');
|
||
});
|
||
});
|