feat: render semantic portfolio document
This commit is contained in:
63
tests/unit/AppShell.test.ts
Normal file
63
tests/unit/AppShell.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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';
|
||||
|
||||
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> => {
|
||||
const document = globalThis.document.implementation.createHTMLDocument('Portfolio');
|
||||
document.body.innerHTML = await render('/', validInputs);
|
||||
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('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');
|
||||
});
|
||||
});
|
||||
100
tests/unit/components.test.ts
Normal file
100
tests/unit/components.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import ContactDestination from '@/components/ContactDestination.vue';
|
||||
import InfrastructureDiagram from '@/components/InfrastructureDiagram.vue';
|
||||
import ProjectCaseStudy from '@/components/ProjectCaseStudy.vue';
|
||||
import ToolkitGrid from '@/components/ToolkitGrid.vue';
|
||||
import type { ContactContent, InfrastructureCaseStudy, PublicProject, ToolkitGroup } from '@/content/types';
|
||||
|
||||
const infrastructure: InfrastructureCaseStudy = {
|
||||
id: 'safe-infrastructure',
|
||||
title: 'Secure self-hosted platform and lifecycle',
|
||||
summary: 'A compact infrastructure case study.',
|
||||
bands: [
|
||||
{ id: 'ingress-tls', title: 'Ingress and TLS', summary: 'Managed encrypted ingress.' },
|
||||
{ id: 'identity-access', title: 'Identity and access', summary: 'Protected service access.' },
|
||||
],
|
||||
};
|
||||
|
||||
const privateProject: PublicProject = {
|
||||
id: 'private-work',
|
||||
title: 'Private self-hosted medication tracker',
|
||||
visibility: 'private',
|
||||
maturity: 'active',
|
||||
summary: 'A private application.',
|
||||
problem: 'A recurring workflow needs reliable tracking.',
|
||||
contribution: 'Designed and maintained the application.',
|
||||
technicalDecisions: ['Used a synchronised client and server design.'],
|
||||
outcome: 'The work remains private.',
|
||||
assetIds: [],
|
||||
};
|
||||
|
||||
const publicProject: PublicProject = {
|
||||
...privateProject,
|
||||
id: 'public-work',
|
||||
title: 'Footy Tracker',
|
||||
visibility: 'public',
|
||||
url: 'https://github.com/example/footy-tracker',
|
||||
};
|
||||
|
||||
describe('semantic section components', () => {
|
||||
it('gives the infrastructure graphic an accessible name, description, caption, and visible text equivalent', () => {
|
||||
const wrapper = mount(InfrastructureDiagram, { props: { infrastructure } });
|
||||
const svg = wrapper.get('svg');
|
||||
|
||||
expect(svg.attributes('role')).toBe('img');
|
||||
expect(svg.attributes('aria-labelledby')).toContain('safe-infrastructure-diagram-title');
|
||||
expect(svg.attributes('aria-describedby')).toContain('safe-infrastructure-diagram-description');
|
||||
expect(wrapper.get('figcaption').text()).toContain('Secure self-hosted platform and lifecycle');
|
||||
expect(wrapper.get('[data-testid="infrastructure-text-equivalent"]').text()).toContain('Ingress and TLS');
|
||||
expect(wrapper.get('[data-testid="infrastructure-text-equivalent"]').text()).toContain('Identity and access');
|
||||
});
|
||||
|
||||
it('does not turn private project content into a link container', () => {
|
||||
const wrapper = mount(ProjectCaseStudy, { props: { project: privateProject } });
|
||||
|
||||
expect(wrapper.get('article').findAll('a')).toHaveLength(0);
|
||||
expect(wrapper.get('h3').text()).toBe('Private self-hosted medication tracker');
|
||||
});
|
||||
|
||||
it('uses a safe external link only for a public project with an approved URL', () => {
|
||||
const wrapper = mount(ProjectCaseStudy, { props: { project: publicProject } });
|
||||
const link = wrapper.get('article a');
|
||||
|
||||
expect(link.attributes('href')).toBe('https://github.com/example/footy-tracker');
|
||||
expect(link.attributes('target')).toBe('_blank');
|
||||
expect(link.attributes('rel')).toBe('noopener noreferrer');
|
||||
});
|
||||
|
||||
it('announces every toolkit technology once while hiding the decorative icon placeholder', () => {
|
||||
const groups: readonly ToolkitGroup[] = [{
|
||||
title: 'Systems & delivery',
|
||||
technologies: [{ name: 'Linux' }],
|
||||
}];
|
||||
const wrapper = mount(ToolkitGrid, { props: { groups } });
|
||||
const tile = wrapper.get('[data-testid="toolkit-technology"]');
|
||||
|
||||
expect(tile.text()).toBe('Linux');
|
||||
expect(tile.get('[aria-hidden="true"]').attributes('aria-hidden')).toBe('true');
|
||||
expect(tile.findAll('[aria-label="Linux"]').length).toBe(0);
|
||||
});
|
||||
|
||||
it('renders a visible mailto contact and safe verified social links', () => {
|
||||
const contact: ContactContent = {
|
||||
email: 'hello@example.test',
|
||||
cvMessage: 'For a current CV or role-specific information, contact me directly.',
|
||||
social: [{ service: 'GitHub', url: 'https://github.com/example' }],
|
||||
};
|
||||
const wrapper = mount(ContactDestination, {
|
||||
props: {
|
||||
contact,
|
||||
identity: { name: 'Luca Pîslaru', role: 'System Administrator & Full-Stack Developer' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.get('a[href="mailto:hello@example.test"]').text()).toBe('hello@example.test');
|
||||
const social = wrapper.get('a[href="https://github.com/example"]');
|
||||
expect(social.text()).toBe('GitHub');
|
||||
expect(social.attributes('rel')).toBe('noopener noreferrer');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user