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

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('Experience');
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', 'Experience'], ['#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');
});
});