diff --git a/craft/src/components/basic/Navbar.toHtml.test.ts b/craft/src/components/basic/Navbar.toHtml.test.ts
index a9aca89..cc27a6a 100644
--- a/craft/src/components/basic/Navbar.toHtml.test.ts
+++ b/craft/src/components/basic/Navbar.toHtml.test.ts
@@ -5,28 +5,84 @@ const toHtml = (Navbar as any).toHtml;
describe('Navbar.toHtml hamburger accessibility (F2.3)', () => {
test('mobile toggle button has an accessible name, aria-expanded, and aria-controls', () => {
- const { html } = toHtml({ showMobileMenu: true }, '');
+ const { html } = toHtml({ showMobileMenu: true }, '', 'node-nav1');
expect(html).toMatch(/class="navbar-hamburger"[^>]*aria-label="Toggle navigation menu"/);
expect(html).toMatch(/aria-expanded="false"/);
- expect(html).toMatch(/aria-controls="navbar-links"/);
+ expect(html).toMatch(/aria-controls="[^"]+"/);
});
test('aria-controls target id exists on the links container', () => {
- const { html } = toHtml({ showMobileMenu: true }, '');
- expect(html).toContain('id="navbar-links"');
+ const { html } = toHtml({ showMobileMenu: true }, '', 'node-nav1');
+ const controls = html.match(/aria-controls="([^"]+)"/)![1];
+ expect(html).toContain(`id="${controls}"`);
});
test('toggle script flips aria-expanded on click', () => {
- const { html } = toHtml({ showMobileMenu: true }, '');
+ const { html } = toHtml({ showMobileMenu: true }, '', 'node-nav1');
expect(html).toMatch(/setAttribute\(['"]aria-expanded['"]/);
});
test('no mobile menu: no hamburger button emitted', () => {
- const { html } = toHtml({ showMobileMenu: false }, '');
+ const { html } = toHtml({ showMobileMenu: false }, '', 'node-nav1');
expect(html).not.toContain('navbar-hamburger');
});
});
+describe('Navbar.toHtml node-scoped ids/hover styles (M-1: two navbars must not collide)', () => {
+ test('no bare unscoped id="navbar-links" is emitted', () => {
+ const { html } = toHtml({ showMobileMenu: true }, '', 'node-nav1');
+ expect(html).not.toContain('id="navbar-links"');
+ });
+
+ test('two different node ids produce different links-container ids', () => {
+ const { html: html1 } = toHtml({ showMobileMenu: true }, '', 'node-nav1');
+ const { html: html2 } = toHtml({ showMobileMenu: true }, '', 'node-nav2');
+ const id1 = html1.match(/id="([^"]+)"/)![1];
+ const id2 = html2.match(/id="([^"]+)"/)![1];
+ expect(id1).not.toBe(id2);
+ });
+
+ test('aria-controls always equals the actual links-container id', () => {
+ const { html } = toHtml({ showMobileMenu: true }, '', 'node-nav1');
+ const controls = html.match(/aria-controls="([^"]+)"/)![1];
+ const linksId = html.match(/id="([^"]+)"/)![1];
+ expect(controls).toBe(linksId);
+ });
+
+ test('hover style selectors are scoped per-instance, not bare .navbar-link/.navbar-cta', () => {
+ const { html } = toHtml({ hoverColor: '#ff0000' }, '', 'node-nav1');
+ // A selector rule that STARTS the line with .navbar-link:hover (i.e. not
+ // preceded by a per-instance ancestor class) would be the old, unscoped,
+ // globally-colliding form.
+ expect(html).not.toMatch(/^\s*\.navbar-link:hover/m);
+ expect(html).not.toMatch(/^\s*\.navbar-cta:hover/m);
+ // still present, just scoped under a per-instance ancestor class
+ expect(html).toMatch(/\.navbar-link:hover/);
+ expect(html).toMatch(/\.[\w-]+ \.navbar-link:hover/);
+ });
+
+ test('two navbars with different hoverColor do not leak style onto each other (scoped selectors differ)', () => {
+ const { html: html1 } = toHtml({ hoverColor: '#ff0000' }, '', 'node-nav1');
+ const { html: html2 } = toHtml({ hoverColor: '#00ff00' }, '', 'node-nav2');
+ const scope1 = html1.match(/
// breakout -> arbitrary `;
}