import { describe, it, expect } from 'vitest'; import { buildFaviconLinks, renderFaviconLinkTag, type FaviconIcon } from '../../utils/favicon-links'; /** * Regression test for the sub-path deployment 404 bug + the * data-driven icons API. * * Bug 1 (filed by user, 2026-08-01): deployments at sub-paths * (e.g. `/cie-eilv/`) saw console 404s for `favicon-32x32.png`, * `favicon-16x16.png`, `favicon-48x48.png` because the emitted * `` pointed at the ROOT instead of the deployment sub-path. * * Bug 2 (filed by user, 2026-08-01): the `links_html` field required * consumers to inject raw HTML — impossible to validate, can't be safely * BASE_PATH-rewritten. Refactored to DATA: `icons: [{ rel, href, type?, * sizes? }]` — href is a filename, system applies the correct prefix. */ describe('buildFaviconLinks — BASE_PATH regression', () => { it('emits /-prefixed hrefs for root deployment', () => { const links = buildFaviconLinks({ astroBaseUrl: '/' }); const png16 = links.find(l => l.sizes === '16x16'); const png32 = links.find(l => l.sizes === '32x32'); const png48 = links.find(l => l.sizes === '48x48'); expect(png16?.href).toBe('/favicon-16x16.png'); expect(png32?.href).toBe('/favicon-32x32.png'); expect(png48?.href).toBe('/favicon-48x48.png'); }); /** * THE LOAD-BEARING TEST — pins the exact bug the user reported. */ it('emits /cie-eilv/-prefixed hrefs for sub-path deployment', () => { const links = buildFaviconLinks({ siteConfigBasePath: '/cie-eilv/', astroBaseUrl: '/', }); const png16 = links.find(l => l.sizes === '16x16'); const png32 = links.find(l => l.sizes === '32x32'); const png48 = links.find(l => l.sizes === '48x48'); expect(png16?.href).toBe('/cie-eilv/favicon-16x16.png'); expect(png32?.href).toBe('/cie-eilv/favicon-32x32.png'); expect(png48?.href).toBe('/cie-eilv/favicon-48x48.png'); }); it('handles deep sub-paths', () => { const links = buildFaviconLinks({ siteConfigBasePath: '/vocab/iala/' }); const icon = links.find(l => l.rel === 'icon' && l.type === 'image/x-icon'); expect(icon?.href).toBe('/vocab/iala/favicon.ico'); }); it('handles basePath without trailing slash', () => { const links = buildFaviconLinks({ siteConfigBasePath: '/cie-eilv' }); const icon = links.find(l => l.rel === 'icon' && l.type === 'image/x-icon'); expect(icon?.href).toBe('/cie-eilv/favicon.ico'); }); it('never produces a bare "/" href', () => { const scenarios = [ { astroBaseUrl: '/' }, { astroBaseUrl: '/vocab/' }, { siteConfigBasePath: '/cie-eilv/' }, { brandingFaviconBasePath: '/custom/' }, ]; for (const input of scenarios) { const links = buildFaviconLinks(input); for (const link of links) { expect(link.href, `bare href for input ${JSON.stringify(input)}`).not.toBe('/'); expect(link.href.length, `empty href for input ${JSON.stringify(input)}`).toBeGreaterThan(1); } } }); it('emits the canonical 16 default favicon tags', () => { const links = buildFaviconLinks({ astroBaseUrl: '/' }); expect(links.length).toBe(16); expect(links.filter(l => l.rel === 'icon').length).toBe(5); expect(links.filter(l => l.rel === 'apple-touch-icon').length).toBe(11); }); }); describe('buildFaviconLinks — consumer-declared icons (data form)', () => { const consumerIcons: FaviconIcon[] = [ { rel: 'icon', type: 'image/svg+xml', href: 'favicon.svg' }, { rel: 'icon', type: 'image/png', sizes: '96x96', href: 'favicon-96x96.png' }, { rel: 'shortcut icon', href: 'favicon.ico' }, { rel: 'apple-touch-icon', sizes: '180x180', href: 'apple-touch-icon.png' }, { rel: 'manifest', href: 'site.webmanifest' }, ]; it('replaces the default set when icons is non-empty', () => { const links = buildFaviconLinks({ astroBaseUrl: '/', icons: consumerIcons }); expect(links.length).toBe(consumerIcons.length); // Should NOT contain any of the default 16 links. expect(links.find(l => l.sizes === '16x16')).toBeUndefined(); expect(links.find(l => l.sizes === '32x32')).toBeUndefined(); }); it('prefixes bare filenames with the resolved base path', () => { const links = buildFaviconLinks({ siteConfigBasePath: '/cie-eilv/', icons: consumerIcons, }); const svg = links.find(l => l.type === 'image/svg+xml'); const png96 = links.find(l => l.sizes === '96x96'); const ico = links.find(l => l.rel === 'shortcut icon'); const manifest = links.find(l => l.rel === 'manifest'); expect(svg?.href).toBe('/cie-eilv/favicon.svg'); expect(png96?.href).toBe('/cie-eilv/favicon-96x96.png'); expect(ico?.href).toBe('/cie-eilv/favicon.ico'); expect(manifest?.href).toBe('/cie-eilv/site.webmanifest'); }); it('emits absolute URLs as-is (no prefix)', () => { const links = buildFaviconLinks({ astroBaseUrl: '/vocab/', icons: [ { rel: 'icon', type: 'image/svg+xml', href: 'https://cdn.example.com/favicon.svg' }, { rel: 'icon', href: 'favicon.ico' }, ], }); const cdn = links.find(l => l.href.startsWith('https://')); const local = links.find(l => l.href.endsWith('favicon.ico') && !l.href.startsWith('https://')); expect(cdn?.href).toBe('https://cdn.example.com/favicon.svg'); expect(local?.href).toBe('/vocab/favicon.ico'); }); it('emits root-relative paths as-is', () => { const links = buildFaviconLinks({ astroBaseUrl: '/vocab/', icons: [{ rel: 'icon', href: '/absolute-path.png' }], }); expect(links[0].href).toBe('/absolute-path.png'); }); it('preserves type and sizes attributes from the data', () => { const links = buildFaviconLinks({ astroBaseUrl: '/', icons: consumerIcons }); const svg = links.find(l => l.type === 'image/svg+xml'); const png96 = links.find(l => l.sizes === '96x96'); const touch = links.find(l => l.rel === 'apple-touch-icon'); expect(svg?.type).toBe('image/svg+xml'); expect(png96?.sizes).toBe('96x96'); expect(touch?.sizes).toBe('180x180'); }); it('falls back to default set when icons is empty array', () => { const links = buildFaviconLinks({ astroBaseUrl: '/', icons: [] }); expect(links.length).toBe(16); }); it('falls back to default set when icons is undefined', () => { const links = buildFaviconLinks({ astroBaseUrl: '/' }); expect(links.length).toBe(16); }); }); describe('renderFaviconLinkTag', () => { it('renders all attributes in deterministic order', () => { const html = renderFaviconLinkTag({ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png', }); expect(html).toBe(''); }); it('omits optional attributes when not set', () => { const html = renderFaviconLinkTag({ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png', }); expect(html).toBe(''); }); it('never emits an empty href', () => { const links = buildFaviconLinks({ siteConfigBasePath: '/sub-path/' }); for (const link of links) { const html = renderFaviconLinkTag(link); const hrefMatch = html.match(/href="([^"]+)"/); expect(hrefMatch, `no href in: ${html}`).not.toBeNull(); expect(hrefMatch![1].length, `empty href in: ${html}`).toBeGreaterThan(0); } }); });