diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue index 4f4bcf6..c1cf7fa 100644 --- a/src/components/AppShell.vue +++ b/src/components/AppShell.vue @@ -1,4 +1,5 @@ @@ -17,15 +34,15 @@ defineProps<{ content: Readonly }>(); {{ content.identity.name }} {{ content.identity.role }} - + - + {{ content.identity.valueProposition }} - + {{ content.profile.introduction }} @@ -35,27 +52,27 @@ defineProps<{ content: Readonly }>(); - + - + - + - + - + - + diff --git a/tests/unit/AppShell.test.ts b/tests/unit/AppShell.test.ts index c55a5a6..74534e3 100644 --- a/tests/unit/AppShell.test.ts +++ b/tests/unit/AppShell.test.ts @@ -3,6 +3,7 @@ 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')); @@ -15,8 +16,12 @@ const validInputs = validateProductionInputs( ); const renderedDocument = async (): Promise => { + return renderedDocumentFor(validInputs); +}; + +const renderedDocumentFor = async (inputs: typeof validInputs): Promise => { const document = globalThis.document.implementation.createHTMLDocument('Portfolio'); - document.body.innerHTML = await render('/', validInputs); + document.body.innerHTML = await render('/', inputs); return document; }; @@ -52,6 +57,33 @@ describe('semantic portfolio document', () => { expect(document.body.textContent).toContain('For a current CV or role-specific information, contact me directly.'); }); + 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.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();
{{ content.identity.role }}
{{ content.identity.valueProposition }}
{{ content.profile.introduction }}