Skip to content

Commit 2f0780b

Browse files
author
DevBot
committed
fix(www): restore docs sidebar + footer chrome lost in the compiled-shell refactor (#1317)
Root cause: 7bbef34 (ADR-0143 compiled-shell refactor) deleted the .docs-sidebar styles and the rich four-column footer from open-layout; the follow-up test cleanup codified the stripped chrome instead of restoring it, while entry-render-runtime kept injecting navItems / currentPath / locale / home into the shell props every render. Restores: - docs sidebar (desktop sticky nav + mobile native-details disclosure, #995 idiom) driven by the previously-dead _generated-nav navSections, filtered to the active section family with aria-current page marking - four labeled footer navigation columns (product/resources/company/ legal, en/zh) + footer-bottom strip; orphaned `padding: 0` media rule replaced with real responsive grid collapse - header nav links now carry localized hrefs and aria-current state Coverage hardening (the process gap that let this ship green): - site-chrome.spec.ts: semantic sidebar/footer assertions (section filtering, active marking, home renders no sidebar, mobile disclosure, four labeled columns, bilingual targets) - visual-baselines.spec.ts: dedicated footer/sidebar element snapshots (every locale x theme x viewport on home, guide, architecture) — the route sweep's fullPage:false never captured the page bottom - accessibility-performance.spec.ts: footer keeps the zero-headings invariant but now also asserts 4 navigation landmarks + structured link targets, so the bare-strip regression cannot pass again - open-layout-navigation.test.ts: pure-function policy tests for URL safety, locale canonicalization, section filtering, footer model
1 parent 9bb6877 commit 2f0780b

170 files changed

Lines changed: 943 additions & 17 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

www/__tests__/open-layout-navigation.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { assert, assertEquals, assertFalse } from '@std/assert';
22
import {
3+
buildSidebarRows,
4+
decorateHeaderNav,
35
filterNavSections,
6+
footerColumn,
47
isExternalLayoutUrl,
58
isSafeLayoutUrl,
9+
layoutChromeStrings,
610
localeSwitchLabel,
711
localeSwitchPath,
812
localizeLayoutPath,
@@ -65,3 +69,142 @@ Deno.test('open-layout navigation labels the nameless generated group as Project
6569
assertEquals(filterNavSections(sections, '/blog').map((s) => s.section), ['History']);
6670
assertEquals(filterNavSections(sections, '/apilist').map((s) => s.section), ['Reference']);
6771
});
72+
73+
const GENERATED_LIKE_SECTIONS = [
74+
{ section: 'Quick Start', items: [{ path: '/docs', label: 'Docs' }] },
75+
{
76+
section: 'Guide',
77+
items: [
78+
{ path: '/guide/getting-started', label: 'Getting Started' },
79+
{ path: '/guide/api', label: 'API Routes' },
80+
],
81+
},
82+
{ section: 'Core', items: [{ path: '/guide/deployment', label: 'Deployment' }] },
83+
{ section: 'Principles', items: [{ path: '/architecture/dsd', label: 'DSD Rendering' }] },
84+
{ section: 'Reference', items: [{ path: '/apilist', label: 'API Reference' }] },
85+
];
86+
87+
Deno.test('buildSidebarRows flattens the filtered section tree into heading and link rows', () => {
88+
const rows = buildSidebarRows(GENERATED_LIKE_SECTIONS, '/guide/api', 'en', ['en', 'zh']);
89+
assertEquals(rows.map((row) => row.kind), [
90+
'section',
91+
'link',
92+
'section',
93+
'link',
94+
'link',
95+
'section',
96+
'link',
97+
]);
98+
assertEquals(rows[0].heading, 'Quick Start');
99+
assertEquals(rows[3].label, 'Getting Started');
100+
// The active page is marked exactly once, on the exact-match link.
101+
assertEquals(rows.filter((row) => row.current === 'page').map((row) => row.href), ['/guide/api']);
102+
// Heading rows carry no link affordance; link rows carry no heading.
103+
assertEquals(rows[0].href, false);
104+
assertEquals(rows[3].heading, '');
105+
// Row keys are unique and stable for the keyed Region.
106+
assertEquals(new Set(rows.map((row) => row.key)).size, rows.length);
107+
});
108+
109+
Deno.test('buildSidebarRows localizes link targets and matches the localized current path', () => {
110+
const rows = buildSidebarRows(GENERATED_LIKE_SECTIONS, '/zh/guide/api', 'zh', ['en', 'zh']);
111+
const links = rows.filter((row) => row.kind === 'link');
112+
assert(links.every((row) => row.href !== false && row.href.startsWith('/zh/')));
113+
assertEquals(rows.filter((row) => row.current === 'page').map((row) => row.href), [
114+
'/zh/guide/api',
115+
]);
116+
});
117+
118+
Deno.test('buildSidebarRows filters to the active section family before flattening', () => {
119+
const rows = buildSidebarRows(GENERATED_LIKE_SECTIONS, '/architecture/dsd', 'en', ['en', 'zh']);
120+
assertEquals(
121+
rows.filter((row) => row.kind === 'section').map((row) => row.heading),
122+
['Principles', 'Reference'],
123+
);
124+
});
125+
126+
Deno.test('buildSidebarRows guards unsafe hrefs and marks external links', () => {
127+
const rows = buildSidebarRows(
128+
[{
129+
section: 'Guide',
130+
items: [
131+
{ href: 'javascript:alert(1)', label: 'Evil' },
132+
{ href: 'https://example.com/x', label: 'External' },
133+
],
134+
}],
135+
'/guide',
136+
'en',
137+
['en', 'zh'],
138+
);
139+
const evil = rows.find((row) => row.label === 'Evil');
140+
const external = rows.find((row) => row.label === 'External');
141+
assertEquals(evil?.href, false);
142+
assertEquals(external?.href, 'https://example.com/x');
143+
assertEquals(external?.rel, 'noopener noreferrer');
144+
});
145+
146+
Deno.test('decorateHeaderNav marks the current section and never external links', () => {
147+
const links = [
148+
{ href: '/docs', label: 'Docs' },
149+
{ href: '/blog', label: 'Blog' },
150+
{ href: 'https://github.com/open-element/openelement', label: 'GitHub' },
151+
];
152+
assertEquals(decorateHeaderNav(links, '/docs', 'en', ['en', 'zh']).map((link) => link.current), [
153+
'page',
154+
false,
155+
false,
156+
]);
157+
// Section roots stay current on nested routes (blog posts keep Blog current).
158+
assertEquals(
159+
decorateHeaderNav(links, '/blog/0001-keep-hono-vite-dev-server', 'en', ['en', 'zh'])[1].current,
160+
'page',
161+
);
162+
// The adapter pre-localizes header hrefs but passes the bare route path;
163+
// current marking must still land for non-default locales.
164+
const zhLinks = links.map((link) =>
165+
link.href.startsWith('https:') ? link : { ...link, href: `/zh${link.href}` }
166+
);
167+
assertEquals(decorateHeaderNav(zhLinks, '/blog', 'zh', ['en', 'zh']).map((l) => l.current), [
168+
false,
169+
'page',
170+
false,
171+
]);
172+
// A locale-prefixed request-time path normalizes to the same result.
173+
assertEquals(decorateHeaderNav(zhLinks, '/zh/blog', 'zh', ['en', 'zh']).map((l) => l.current), [
174+
false,
175+
'page',
176+
false,
177+
]);
178+
});
179+
180+
Deno.test('footerColumn restores the four-column link structure with localized targets', () => {
181+
const product = footerColumn('en', ['en', 'zh'], 'product');
182+
assertEquals(product.label, 'Product');
183+
assertEquals(
184+
product.links.map((link) => link.href),
185+
[
186+
'/guide/core-concepts',
187+
'/architecture/design-system',
188+
'/architecture/architecture',
189+
'/architecture/standards-registry',
190+
],
191+
);
192+
const zhProduct = footerColumn('zh', ['en', 'zh'], 'product');
193+
assertEquals(zhProduct.label, '产品');
194+
assert(zhProduct.links.every((link) => link.href.startsWith('/zh/')));
195+
const company = footerColumn('en', ['en', 'zh'], 'company');
196+
const github = company.links.find((link) => link.label === 'GitHub');
197+
assertEquals(github?.href, 'https://github.com/open-element/openelement');
198+
assertEquals(github?.rel, 'noopener noreferrer');
199+
const legal = footerColumn('zh', ['en', 'zh'], 'legal');
200+
assertEquals(legal.label, '法律');
201+
assertEquals(legal.links.map((link) => link.label), ['MIT 许可证', '参与贡献']);
202+
});
203+
204+
Deno.test('layoutChromeStrings carries the bilingual shell chrome copy', () => {
205+
assertEquals(layoutChromeStrings('en').sidebarLabel, 'Documentation navigation');
206+
assertEquals(layoutChromeStrings('zh').sidebarLabel, '文档导航');
207+
assertEquals(layoutChromeStrings('en').sidebarToggle, 'Documentation');
208+
assertEquals(layoutChromeStrings('zh').sidebarToggle, '文档');
209+
assertEquals(typeof layoutChromeStrings('zh').footerTagline, 'string');
210+
});

www/__tests__/site-ui.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,36 @@ Deno.test('open-layout is an explicitly hydrated compiled app-shell island', asy
3030
assertStringIncludes(source, 'export default class OpenLayout extends OpenElement');
3131
const result = compileElementProgram(source, url.pathname);
3232
assertEquals(result.program.tag, 'open-layout');
33-
assertEquals(result.program.regions.length, 2);
33+
// Regions: header nav (desktop + mobile panel), sidebar rows (desktop +
34+
// mobile disclosure panel) and the four footer link columns.
35+
assertEquals(result.program.regions.length, 8);
3436
assertEquals(
3537
result.program.metadata.properties.map((property) => property.name),
36-
['headerNav', 'footerText', 'siteName', 'homeHref'],
38+
[
39+
'headerNav',
40+
'footerText',
41+
'siteName',
42+
'homeHref',
43+
'navItems',
44+
'currentPath',
45+
'locale',
46+
'locales',
47+
'home',
48+
'headerNavItems',
49+
'sidebarLabel',
50+
'sidebarToggle',
51+
'sidebarRows',
52+
'sidebarHidden',
53+
'footerTagline',
54+
'footerProductLabel',
55+
'footerProductLinks',
56+
'footerResourcesLabel',
57+
'footerResourcesLinks',
58+
'footerCompanyLabel',
59+
'footerCompanyLinks',
60+
'footerLegalLabel',
61+
'footerLegalLinks',
62+
],
3763
);
3864
assertEquals(
3965
result.program.metadata.properties.find((property) => property.name === 'headerNav')?.attribute,

0 commit comments

Comments
 (0)