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 `; }