import { beforeEach, describe, expect, it, vi } from "vitest"; import { apiFetch } from "@/lib/auth/api-client"; import { initialInspectionValues, toInspectionInput, } from "@/app/(app)/inspections/new/inspection-form-values"; import { initialVarroaValues, toVarroaInput } from "@/app/(app)/varroa/new/varroa-form-values"; import { initialFeedingValues, toFeedingInput, } from "@/app/(app)/feedings/new/feeding-form-values"; import { initialTreatmentValues, toTreatmentInput, } from "@/app/(app)/treatments/new/treatment-form-values"; import { queueFeeding, queueInspection, queueTreatment, queueVarroaMeasurement } from "./db"; import { syncPendingEntries } from "./sync"; /** * Stand-in for IndexedDB that stores entries the way the browser does - as a * structured clone - so the test covers what really survives the queue. */ vi.mock("idb", () => { const stores = new Map>(); let nextId = 1; const db = { add: async (store: string, value: object) => { const id = nextId++; const entries = stores.get(store) ?? new Map(); entries.set(id, structuredClone({ ...value, id })); stores.set(store, entries); return id; }, get: async (store: string, id: number) => structuredClone(stores.get(store)?.get(id)), getAll: async (store: string) => [...(stores.get(store)?.values() ?? [])].map((entry) => structuredClone(entry)), delete: async (store: string, id: number) => { stores.get(store)?.delete(id); }, count: async (store: string) => stores.get(store)?.size ?? 0, }; return { openDB: vi.fn(async () => db) }; }); // The photos that follow a queued inspection are covered in lib/photos/store.spec.ts. vi.mock("@/lib/photos/store", () => ({ resolveQueuedInspection: vi.fn(async () => undefined), finishHandOvers: vi.fn(async () => 0), uploadReadyPhotos: vi.fn(async () => ({ synced: 0, failed: 0, skippedForeign: 0 })), })); vi.mock("@/lib/auth/api-client", async (importOriginal) => ({ ...(await importOriginal()), apiFetch: vi.fn(async () => ({ id: "saved" })), })); function sentBody(path: string): Record { const call = vi.mocked(apiFetch).mock.calls.find(([calledPath]) => calledPath === path); expect(call, `request to ${path}`).toBeDefined(); return JSON.parse(String(call?.[1]?.body)); } /** What the server receives when the record is sent directly (online). */ function asSentOnline(input: object): Record { return JSON.parse(JSON.stringify(input)); } describe("offline queue keeps 'not filled in' as null from form to server", () => { beforeEach(() => vi.mocked(apiFetch).mockClear()); it("replays queued records exactly as they would have been sent online", async () => { const inspection = toInspectionInput({ ...initialInspectionValues("colony-1"), eggsPresent: false, }); const varroa = toVarroaInput({ ...initialVarroaValues("colony-1"), miteCount: "0" }); const feeding = toFeedingInput({ ...initialFeedingValues("colony-1"), amount: "2" }); const treatment = toTreatmentInput({ ...initialTreatmentValues("colony-1"), product: "Oxuvar", }); await queueInspection(inspection, "key-inspection", "user-1"); await queueVarroaMeasurement(varroa, "key-varroa", "user-1"); await queueFeeding(feeding, "key-feeding", "user-1"); await queueTreatment(treatment, "key-treatment", "user-1"); const result = await syncPendingEntries("user-1"); expect(result).toEqual({ synced: 4, failed: 0, skippedForeign: 0 }); expect(sentBody("/inspections")).toEqual(asSentOnline(inspection)); expect(sentBody("/varroa-measurements")).toEqual(asSentOnline(varroa)); expect(sentBody("/feedings")).toEqual(asSentOnline(feeding)); expect(sentBody("/treatments")).toEqual(asSentOnline(treatment)); }); it("sends an untouched inspection observation as null, never as a finding", async () => { await queueInspection(toInspectionInput(initialInspectionValues("colony-1")), "key-2", "user-1"); await syncPendingEntries("user-1"); expect(sentBody("/inspections")).toMatchObject({ queenSeen: null, eggsPresent: null, feedStatus: null, weather: null, gentleness: null, }); }); });