diff --git a/package.json b/package.json index 7f3fbe0..beb089d 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "site:build": "node site/build-site.mjs", "site:dev": "npm run site:build && node scripts/site-local-server.mjs", "site:serve": "node scripts/site-local-server.mjs", - "test:site": "node tests/system-map.test.mjs && node tests/site.test.mjs && node tests/site-brand-assets.test.mjs && node tests/site-local-server.test.mjs && node tests/site-header-browser.test.mjs && node tests/site-first-use-browser.test.mjs && node tests/patterns-browser.test.mjs", + "test:site": "node tests/system-map.test.mjs && node tests/site.test.mjs && node tests/site-brand-assets.test.mjs && node tests/site-favicon-browser.test.mjs && node tests/site-local-server.test.mjs && node tests/site-header-browser.test.mjs && node tests/site-first-use-browser.test.mjs && node tests/patterns-browser.test.mjs", "test:components:inventory": "node tests/components/component-reference-inventory.test.mjs", "test:components:contracts": "node tests/components/component-contract-registry.test.mjs", "test:components:unit": "node tests/components/component-state-model.test.mjs && node tests/components/component-specimen-runtime.test.mjs && node tests/components/native-components.test.mjs && node tests/components/interactive-components.test.mjs", diff --git a/scripts/build-site-brand-assets.py b/scripts/build-site-brand-assets.py index ab794d8..658174f 100644 --- a/scripts/build-site-brand-assets.py +++ b/scripts/build-site-brand-assets.py @@ -59,11 +59,11 @@ def mark(parts, foreground, height, identifier='', monochrome=False): return f'{polygons}' -def icon_svg(theme='light', height=None, radius=None, optical=False, responsive=False, monochrome=False): +def icon_svg(theme='light', height=None, radius=None, optical=False, responsive=False, monochrome=False, transparent=False): height = height if height is not None else ICONS['visible_mark_height'] radius = radius if radius is not None else ICONS['browser_radius'] background, foreground = (CONFIG['ivory'], CONFIG['ink']) if theme == 'light' else (CONFIG['ink'], CONFIG['ivory']) - body = '' if monochrome else f'' + body = '' if monochrome or transparent else f'' if responsive: body += f'' body += mark(GEOMETRY['canonical'], foreground, height, 'canonical') + mark(GEOMETRY['optical_16'], foreground, height, 'optical') @@ -92,14 +92,17 @@ def icon_svg(theme='light', height=None, radius=None, optical=False, responsive= with tempfile.TemporaryDirectory(prefix='judgmentkit-web-icons-') as temporary: temporary = Path(temporary) for theme in ['light', 'dark']: - write(f'{ICONS["output_directory"]}/favicon-{theme}.svg', icon_svg(theme, responsive=True)) + write(f'{ICONS["output_directory"]}/favicon-{theme}.svg', icon_svg(theme, responsive=True, transparent=ICONS['browser_background'] == 'transparent')) for size in [16, 32, 48]: source = temporary / f'{theme}-{size}.svg' - source.write_text(icon_svg(theme, optical=size == 16)) + source.write_text(icon_svg(theme, optical=size == 16, transparent=ICONS['browser_background'] == 'transparent')) render(source, icons_dir / f'favicon-{theme}-{size}.png', (size, size)) - frames = [Image.open(icons_dir / f'favicon-{ICONS["fallback_theme"]}-{size}.png').convert('RGBA') for size in [16, 32, 48]] + frames = [Image.open(icons_dir / f'favicon-{theme}-{size}.png').convert('RGBA') for size in [16, 32, 48]] + ico = icons_dir / f'favicon-{theme}.ico' + frames[-1].save(ico, format='ICO', sizes=[(16, 16), (32, 32), (48, 48)], append_images=frames[:-1]) + OUTPUTS.append(ico) ico = icons_dir / 'favicon.ico' - frames[-1].save(ico, format='ICO', sizes=[(16, 16), (32, 32), (48, 48)], append_images=frames[:-1]) + ico.write_bytes((icons_dir / f'favicon-{ICONS["fallback_theme"]}.ico').read_bytes()) OUTPUTS.append(ico) write(f'{ICONS["output_directory"]}/safari-pinned-tab.svg', icon_svg(monochrome=True)) for name, height in [('app-icon', ICONS['visible_mark_height']), ('maskable', ICONS['maskable_visible_mark_height'])]: @@ -115,7 +118,7 @@ def icon_svg(theme='light', height=None, radius=None, optical=False, responsive= 'generator': 'scripts/build-site-brand-assets.py', 'canonical_lockup': SOCIAL['lockup_source'], 'canonical_lockup_sha256': digest(lockup_path), - 'sources': {p: digest(ROOT / p) for p in [ICONS['geometry_source'], ICONS['master_source'], 'site/brand-assets.json']}, + 'sources': {p: digest(ROOT / p) for p in [ICONS['geometry_source'], ICONS['master_source'], 'site/brand-assets.json', 'site/assets/brand/favicon-theme.js']}, 'canvas': [1200, 630], 'social_placement': {'scale': scale, 'translation': [x, y], 'source_viewBox': [0, 0, social_width, social_height]}, 'web_icons': ICONS, diff --git a/scripts/verify-public-release.mjs b/scripts/verify-public-release.mjs index c0c366b..30dea62 100644 --- a/scripts/verify-public-release.mjs +++ b/scripts/verify-public-release.mjs @@ -1180,7 +1180,7 @@ async function verifyPublicRoutes(baseUrl, options = {}) { "See a screen repaired", "Explore examples", '', - `', ], "homepage", @@ -1227,6 +1227,18 @@ async function verifyPublicRoutes(baseUrl, options = {}) { const docs = await fetchText(baseUrl, "/docs/"); assert.equal(getAnalyticsScriptSrc(docs.text, "docs"), analyticsScriptSrc); assertReleaseFooter(docs.text, expectedPackageVersion, "docs"); + for (const [label, html] of [["home", home.text], ["value", value.text], ["docs", docs.text]]) { + const head = html.match(/]*>([\s\S]*?)<\/head>/i)?.[1]; + assert.ok(head, `${label}: document head`); + const controllers = [...head.matchAll(/]*\bdata-brand-favicon-controller[^>]*>/gi)]; + assert.equal(controllers.length, 1, `${label}: one synchronous favicon controller`); + assert.equal(/\s(?:async|defer|type)(?:\s|=|>)/i.test(controllers[0][0]), false, `${label}: classic parser-blocking script`); + const active = head.replace(/|]*>[\s\S]*?<\/noscript>|<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, ""); + assert.equal(/]*\brel=["'](?:shortcut )?icon["']/i.test(active), false, `${label}: no competing static favicon`); + for (const fragment of [`src="${SITE_BRAND_ICON_BUNDLE.routes["/assets/brand/favicon-theme.js"]}"`, `data-light-icon="${SITE_BRAND_ICON_BUNDLE.routes["/assets/brand/favicon-light.ico"]}"`, `data-dark-icon="${SITE_BRAND_ICON_BUNDLE.routes["/assets/brand/favicon-dark.ico"]}"`]) assert.ok(controllers[0][0].includes(fragment), `${label}: current controller and themed ICO URLs`); + assert.ok(head.includes(``), `${label}: light no-script fallback`); + } + assertIncludes( docs.text, [ @@ -1342,7 +1354,9 @@ async function verifyPublicRoutes(baseUrl, options = {}) { } assert.deepEqual(webManifest.icons.map(icon => [icon.sizes, icon.purpose]), [["192x192", "any"], ["512x512", "any"], ["192x192", "maskable"], ["512x512", "maskable"]]); for (const fragment of [ - `rel="icon" href="${SITE_BRAND_ICON_BUNDLE.routes["/favicon.ico"]}"`, + `data-light-icon="${SITE_BRAND_ICON_BUNDLE.routes["/assets/brand/favicon-light.ico"]}"`, + `data-dark-icon="${SITE_BRAND_ICON_BUNDLE.routes["/assets/brand/favicon-dark.ico"]}"`, + `