aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-07 18:56:45 +0900
committernsfisis <nsfisis@gmail.com>2025-12-07 18:56:45 +0900
commit463952d9ab01b80f71d7e32f98da908723303079 (patch)
tree6724d5b975973ad4bb286c312e9653c0e51212aa
parent4b755c758e920fb0295b066fa20ce6667334546b (diff)
downloadkioku-463952d9ab01b80f71d7e32f98da908723303079.tar.gz
kioku-463952d9ab01b80f71d7e32f98da908723303079.tar.zst
kioku-463952d9ab01b80f71d7e32f98da908723303079.zip
test(pwa): add tests for PWA configuration
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 <noreply@anthropic.com>
-rw-r--r--docs/dev/roadmap.md2
-rw-r--r--src/client/pwa.test.ts156
2 files changed, 157 insertions, 1 deletions
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("<!doctype html>");
+ expect(offlineHtml).toContain("<html");
+ expect(offlineHtml).toContain("</html>");
+ });
+
+ 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("<title>Kioku - Offline</title>");
+ });
+
+ it("displays offline message", () => {
+ expect(offlineHtml).toContain("You're Offline");
+ expect(offlineHtml).toContain("lost your internet connection");
+ });
+
+ it("has retry button", () => {
+ expect(offlineHtml).toContain("<button");
+ expect(offlineHtml).toContain("Try Again");
+ expect(offlineHtml).toContain("window.location.reload()");
+ });
+
+ it("has Kioku icon", () => {
+ expect(offlineHtml).toContain("<svg");
+ expect(offlineHtml).toContain('fill="#4CAF50"');
+ });
+ });
+
+ describe("Icon", () => {
+ const iconSvg = readFileSync(
+ resolve(projectRoot, "public/icon.svg"),
+ "utf-8",
+ );
+
+ it("exists and is valid SVG", () => {
+ expect(iconSvg).toContain("<svg");
+ expect(iconSvg).toContain("</svg>");
+ 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<");
+ });
+ });
+});