Files
Portfolio/tests/unit/AppShell.test.ts

112 lines
5.8 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('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('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([
'START', 'PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT',
]);
expect(document.querySelector('h2 + h4')).toBeNull();
});
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('hello@example.test');
expect(document.body.textContent).toContain('For a current CV or role-specific information, contact me directly.');
});
it('renders separate full and compact navigation labels without overriding their accessible names', 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.querySelector('.site-navigation__full-label')?.textContent).toBe('Experience');
expect(experience.querySelector('.site-navigation__compact-label')?.textContent).toBe('Career');
expect(additional.hasAttribute('aria-label')).toBe(false);
expect(additional.querySelector('.site-navigation__full-label')?.textContent).toBe('Additional work');
expect(additional.querySelector('.site-navigation__compact-label')?.textContent).toBe('More work');
});
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([
'START', 'PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT',
]);
expect([...document.querySelectorAll('nav a')].map((link) => [
link.getAttribute('href'),
link.querySelector('.site-navigation__full-label')?.textContent,
])).toEqual([
['#start', 'Start'], ['#profile', 'Profile'], ['#experience', 'Experience'], ['#infrastructure', 'Infrastructure'],
['#selected-builds', 'Selected builds'], ['#additional-work', 'Additional 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');
});
});