diff --git a/web/index.html b/web/index.html index f10630f..766063b 100644 --- a/web/index.html +++ b/web/index.html @@ -4,6 +4,9 @@ + + + record-shop diff --git a/web/public/icon.svg b/web/public/icon.svg new file mode 100644 index 0000000..8cf081a --- /dev/null +++ b/web/public/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest new file mode 100644 index 0000000..c1c676e --- /dev/null +++ b/web/public/manifest.webmanifest @@ -0,0 +1,13 @@ +{ + "name": "record-shop", + "short_name": "record-shop", + "description": "Track your physical music collection", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "icons": [ + { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, + { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" } + ] +} diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..01e8b5f --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,11 @@ +self.addEventListener('install', () => { + self.skipWaiting() +}) + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('fetch', () => { + // no-op: presence of a fetch handler enables PWA install +}) diff --git a/web/src/main.tsx b/web/src/main.tsx index 4043b76..eff728d 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -8,3 +8,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ) + +if ('serviceWorker' in navigator && import.meta.env.PROD) { + void navigator.serviceWorker.register('/sw.js') +} diff --git a/web/test/pwa.test.ts b/web/test/pwa.test.ts new file mode 100644 index 0000000..d6b6582 --- /dev/null +++ b/web/test/pwa.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'node:fs' +import path from 'node:path' + +const pub = path.resolve(__dirname, '../public') + +describe('PWA assets', () => { + it('manifest has required fields and the icon exists', () => { + const manifest = JSON.parse(readFileSync(path.join(pub, 'manifest.webmanifest'), 'utf8')) as { + name: string + display: string + start_url: string + icons: { src: string; sizes: string; type: string }[] + } + expect(manifest.name).toContain('record-shop') + expect(manifest.display).toBe('standalone') + expect(manifest.start_url).toBe('/') + expect(manifest.icons.some((i) => i.src === '/icon.svg')).toBe(true) + expect(() => readFileSync(path.join(pub, 'icon.svg'))).not.toThrow() + }) + + it('service worker registers a fetch handler', () => { + const sw = readFileSync(path.join(pub, 'sw.js'), 'utf8') + expect(sw).toContain("addEventListener('fetch'") + }) +})