fix: keep navigation labels single and accessible

This commit is contained in:
2026-09-02 08:35:16 +03:00
parent 5525557c3a
commit 5b7e47c2a2
5 changed files with 28 additions and 39 deletions

View File

@@ -194,16 +194,15 @@ test("responsive navigation exposes the exact visible label as each link's acces
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
await expect(page.getByRole("link", { name: "Experience", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Career", exact: true })).toHaveCount(0);
await expect(page.getByRole("link", { name: "More work", exact: true })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Career", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "More work", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);
await page.setViewportSize({ width: 320, height: 800 });
const career = page.getByRole("link", { name: "Career", exact: true });
const moreWork = page.getByRole("link", { name: "More work", exact: true });
await expect(career.locator(".site-navigation__compact-label")).toHaveText("Career");
await expect(moreWork.locator(".site-navigation__compact-label")).toHaveText("More work");
await expect(career).toHaveText("Career");
await expect(page.getByRole("link", { name: "More work", exact: true })).toHaveText("More work");
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);

View File

@@ -25,6 +25,17 @@ const sectionHeadings = [
'CONTACT',
] as const;
const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> = {
start: 'Start',
profile: 'Profile',
experience: 'Career',
infrastructure: 'Infra',
'selected-builds': 'Builds',
'additional-work': 'More work',
toolkit: 'Toolkit',
contact: 'Contact',
};
const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a,svg text';
const visibleTextSelector = [
'.site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
@@ -96,12 +107,7 @@ const expectedVisibleTextSegments = (
return [
content.identity.name,
content.identity.role,
...sectionIds.map((id) => {
const label = navigationById.get(id);
if (label === undefined)
throw new Error(`Missing navigation label: ${id}`);
return label;
}),
...sectionIds.map((id) => navigationLabels[id]),
...section('start', 1, [content.identity.valueProposition]),
...section('profile', 2, [
content.profile.introduction,

View File

@@ -57,17 +57,17 @@ describe('semantic portfolio document', () => {
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 () => {
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.querySelector('.site-navigation__full-label')?.textContent).toBe('Experience');
expect(experience.querySelector('.site-navigation__compact-label')?.textContent).toBe('Career');
expect(experience.textContent).toBe('Career');
expect(experience.childElementCount).toBe(0);
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');
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 () => {
@@ -91,12 +91,9 @@ describe('semantic portfolio document', () => {
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'],
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'],
]);
});