From 463952d9ab01b80f71d7e32f98da908723303079 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 7 Dec 2025 18:56:45 +0900 Subject: test(pwa): add tests for PWA configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify vite-plugin-pwa manifest settings, workbox caching patterns, offline fallback page, and icon configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/dev/roadmap.md | 2 +- src/client/pwa.test.ts | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/client/pwa.test.ts diff --git a/docs/dev/roadmap.md b/docs/dev/roadmap.md index 88aa403..3cba614 100644 --- a/docs/dev/roadmap.md +++ b/docs/dev/roadmap.md @@ -144,7 +144,7 @@ Smaller features first to enable early MVP validation. - [x] Web manifest - [x] Service Worker - [x] Offline fallback page -- [ ] Add tests +- [x] Add tests ### IndexedDB (Local Storage) - [ ] Dexie.js setup diff --git a/src/client/pwa.test.ts b/src/client/pwa.test.ts new file mode 100644 index 0000000..18522c0 --- /dev/null +++ b/src/client/pwa.test.ts @@ -0,0 +1,156 @@ +/** + * @vitest-environment node + */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const projectRoot = resolve(__dirname, "../.."); + +describe("PWA Configuration", () => { + describe("Web Manifest (via vite.config.ts)", () => { + it("has required manifest fields in vite config", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + // Verify manifest configuration exists + expect(viteConfig).toContain('name: "Kioku"'); + expect(viteConfig).toContain('short_name: "Kioku"'); + expect(viteConfig).toContain( + 'description: "A spaced repetition learning app"', + ); + expect(viteConfig).toContain('theme_color: "#4CAF50"'); + expect(viteConfig).toContain('background_color: "#ffffff"'); + expect(viteConfig).toContain('display: "standalone"'); + expect(viteConfig).toContain('start_url: "/"'); + }); + + it("has icon configuration", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + expect(viteConfig).toContain('src: "icon.svg"'); + expect(viteConfig).toContain('type: "image/svg+xml"'); + expect(viteConfig).toContain('purpose: "any maskable"'); + }); + + it("has registerType autoUpdate", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + expect(viteConfig).toContain('registerType: "autoUpdate"'); + }); + }); + + describe("Workbox Configuration", () => { + it("has workbox caching patterns configured", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + expect(viteConfig).toContain("globPatterns:"); + expect(viteConfig).toContain("runtimeCaching:"); + }); + + it("has navigate fallback for offline support", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + expect(viteConfig).toContain('navigateFallback: "/offline.html"'); + }); + + it("excludes API routes from navigate fallback", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + expect(viteConfig).toContain("navigateFallbackDenylist:"); + expect(viteConfig).toContain("/^\\/api\\//"); + }); + + it("has image caching configuration", () => { + const viteConfig = readFileSync( + resolve(projectRoot, "vite.config.ts"), + "utf-8", + ); + + expect(viteConfig).toContain('handler: "CacheFirst"'); + expect(viteConfig).toContain('cacheName: "images-cache"'); + }); + }); + + describe("Offline Fallback Page", () => { + const offlineHtml = readFileSync( + resolve(projectRoot, "public/offline.html"), + "utf-8", + ); + + it("exists and is valid HTML", () => { + expect(offlineHtml).toContain(""); + expect(offlineHtml).toContain(""); + }); + + it("has proper meta tags", () => { + expect(offlineHtml).toContain('charset="UTF-8"'); + expect(offlineHtml).toContain('name="viewport"'); + expect(offlineHtml).toContain('name="theme-color"'); + expect(offlineHtml).toContain('content="#4CAF50"'); + }); + + it("has appropriate title", () => { + expect(offlineHtml).toContain("Kioku - Offline"); + }); + + it("displays offline message", () => { + expect(offlineHtml).toContain("You're Offline"); + expect(offlineHtml).toContain("lost your internet connection"); + }); + + it("has retry button", () => { + expect(offlineHtml).toContain(" { + expect(offlineHtml).toContain(" { + const iconSvg = readFileSync( + resolve(projectRoot, "public/icon.svg"), + "utf-8", + ); + + it("exists and is valid SVG", () => { + expect(iconSvg).toContain(""); + expect(iconSvg).toContain('xmlns="http://www.w3.org/2000/svg"'); + }); + + it("has proper viewBox for square icon", () => { + expect(iconSvg).toContain('viewBox="0 0 512 512"'); + }); + + it("uses theme color", () => { + expect(iconSvg).toContain('fill="#4CAF50"'); + }); + + it("contains K letter for Kioku branding", () => { + expect(iconSvg).toContain(">K<"); + }); + }); +}); -- cgit v1.2.3-70-g09d2