diff --git a/frontend/components/ServiceWorkerRegister.test.tsx b/frontend/components/ServiceWorkerRegister.test.tsx new file mode 100644 index 0000000..13f1417 --- /dev/null +++ b/frontend/components/ServiceWorkerRegister.test.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import ServiceWorkerRegister from "./ServiceWorkerRegister"; +import { reportError } from "@/app/lib/monitoring"; + +jest.mock("@/app/lib/monitoring", () => ({ + reportError: jest.fn(), +})); + +const mockedReportError = reportError as jest.MockedFunction; + +const originalNodeEnv = process.env.NODE_ENV; + +function setServiceWorker(value: unknown) { + Object.defineProperty(navigator, "serviceWorker", { + value, + configurable: true, + }); +} + +function removeServiceWorker() { + delete (navigator as { serviceWorker?: unknown }).serviceWorker; +} + +describe("ServiceWorkerRegister", () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + process.env.NODE_ENV = originalNodeEnv; + removeServiceWorker(); + }); + + it("does not register and does not throw when navigator.serviceWorker is absent", () => { + removeServiceWorker(); + + expect(() => render()).not.toThrow(); + expect(mockedReportError).not.toHaveBeenCalled(); + }); + + it("warns and reports to monitoring when registration rejects", async () => { + process.env.NODE_ENV = "production"; + const error = new Error("boom"); + const register = jest.fn().mockRejectedValue(error); + setServiceWorker({ register }); + + expect(() => render()).not.toThrow(); + + await waitFor(() => + expect(register).toHaveBeenCalledWith("/sw.js", { scope: "/" }) + ); + await waitFor(() => + expect(mockedReportError).toHaveBeenCalledWith(error, { + source: "service-worker", + }) + ); + expect(warnSpy).toHaveBeenCalledWith( + "[pwa] Service worker registration failed:", + error + ); + }); + + it("does not warn when registration resolves", async () => { + process.env.NODE_ENV = "production"; + const register = jest.fn().mockResolvedValue({}); + setServiceWorker({ register }); + + render(); + + await waitFor(() => expect(register).toHaveBeenCalled()); + expect(mockedReportError).not.toHaveBeenCalled(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it("logs a development explanation when registration is skipped", () => { + process.env.NODE_ENV = "development"; + setServiceWorker({ register: jest.fn() }); + + render(); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("production-only feature") + ); + expect(mockedReportError).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/components/ServiceWorkerRegister.tsx b/frontend/components/ServiceWorkerRegister.tsx index 06e4f4a..e93af49 100644 --- a/frontend/components/ServiceWorkerRegister.tsx +++ b/frontend/components/ServiceWorkerRegister.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect } from "react"; +import { reportError } from "@/app/lib/monitoring"; /** * Registers the PWA service worker (issue #107). @@ -14,20 +15,36 @@ import { useEffect } from "react"; */ export default function ServiceWorkerRegister() { useEffect(() => { - if ( - typeof window === "undefined" || - !("serviceWorker" in navigator) || - process.env.NODE_ENV !== "production" - ) { + if (typeof window === "undefined") { return; } // next-pwa disables itself in development (`disable` flag in // next.config.js), so /sw.js only exists in production builds. + if (process.env.NODE_ENV !== "production") { + console.warn( + "[pwa] Service worker registration skipped: service worker " + + "registration is a production-only feature; next-pwa disables it " + + "in development, so /sw.js does not exist." + ); + return; + } + + // Graceful degradation: without this API the app still works, just + // without offline/PWA support. + if (!("serviceWorker" in navigator)) { + console.warn( + "[pwa] Service worker registration skipped: navigator.serviceWorker " + + "is unavailable in this environment." + ); + return; + } + navigator.serviceWorker .register("/sw.js", { scope: "/" }) .catch((err) => { - console.error("[pwa] Service worker registration failed:", err); + console.warn("[pwa] Service worker registration failed:", err); + reportError(err, { source: "service-worker" }); }); }, []);