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
A0{{R3
literal 0
HcmV?d00001
diff --git a/web/public/icons/favicon-16.png b/web/public/icons/favicon-16.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e14a13f9a0a1f21e44ede037b98e477625ca76d
GIT binary patch
literal 785
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 {
+ const entry = 6 + 16 * i
+ const offset = view.getUint32(entry + 12, true)
+ return bytes.subarray(offset, offset + view.getUint32(entry + 8, true))
+ })
+}
+
describe('production build', () => {
let server: PreviewServer
let origin: string
@@ -94,13 +111,25 @@ describe('production build', () => {
expect(pngSize(bytes)).toEqual({ width: size, height: size })
})
+ it('ships /favicon.ico holding the logo as PNGs at every favicon size', async () => {
+ const bytes = new Uint8Array(await (await get('/favicon.ico')).arrayBuffer())
+ expect(icoImages(bytes).map((png) => pngSize(png))).toEqual(
+ faviconSizes.map((size) => ({ width: size, height: size })),
+ )
+ })
+
it('emits sw.js with the precache list injected', async () => {
const sw = await (await get('/sw.js')).text()
expect(sw).not.toContain('self.__WB_MANIFEST')
expect(sw).toContain('"url":"index.html"')
- for (const { path } of icons.slice(0, 3)) {
- expect(sw).toContain(`"url":"${path.slice(1)}"`)
+ })
+
+ it('precaches every icon and favicon so the offline shell keeps them', async () => {
+ const urls = precacheEntries(await (await get('/sw.js')).text()).map((entry) => entry.url)
+ for (const path of [...icons.map((icon) => icon.path), '/favicon.ico']) {
+ expect(urls).toContain(path.slice(1))
}
+ expect(new Set(urls).size, 'precache URLs are unique').toBe(urls.length)
})
it('precaches the shell, JS, CSS and every self-hosted font', async () => {
@@ -129,6 +158,17 @@ describe('production build', () => {
expect(html).toContain('')
})
+ it('links the favicons from index.html', async () => {
+ const html = await (await get('/')).text()
+ expect(html).toContain('')
+ expect(html).toContain(
+ '',
+ )
+ expect(html).toContain(
+ '',
+ )
+ })
+
it('registers /sw.js at scope / from index.html', async () => {
const html = await (await get('/')).text()
expect(html).toContain('')
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 48a76c4..8d11f62 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -17,8 +17,11 @@ export default defineConfig({
srcDir: 'src',
filename: 'sw.ts',
injectRegister: 'script',
- // Precache the shell, JS, CSS and the self-hosted fonts.
- injectManifest: { globPatterns: ['**/*.{html,js,css,woff2}'] },
+ // Precache the shell, JS, CSS, the self-hosted fonts and every icon
+ // and favicon. The glob already covers the manifest icons, so the
+ // plugin does not add them a second time.
+ injectManifest: { globPatterns: ['**/*.{html,js,css,woff2,png,ico}'] },
+ includeManifestIcons: false,
// The web manifest lives in manifest.json: its theme and background
// colours are --color-accent and --color-bg from src/styles/tokens.css,
// and hex colours may not be written in TypeScript.