Fixes the offline-sync defects QA found on PR #120 (#237/#238/#220). Blank-shell / white-screen on offline reload (HIGH): - auth-query.tsx: the useCollabToken retry predicate read `error.response.status` unguarded. Offline the collab-token POST rejects as an axios NETWORK error (isAxiosError true, response undefined), so `.status` threw an uncaught TypeError in the React Query retryer BEFORE React mounted, white-screening every route. Extracted the predicate as `collabTokenRetry` and guarded it with optional chaining (`error.response?.status === 404`). - user-provider.tsx: gated the whole <Layout> on useCurrentUser() and returned a bare `<></>` on any error, blanking every authenticated route offline even when cached data existed. Now renders the cached app when a (stale) user is present and an explicit OfflineFallback when there is no user to fall back on. - query-persister.ts / make-offline.ts: persist and warm the ['currentUser'] query so the auth gate can hydrate offline (pinned pages now survive relaunch). Offline structural create/move/comment silently lost on reload (HIGH): - offline-mutations.ts: register setMutationDefaults (default mutationFns) for stable mutation keys and tag useCreatePageMutation / useMovePageMutation / useCreateCommentMutation with those keys. A paused mutation dehydrated to IndexedDB while offline now has a mutationFn after reload, so resumePausedMutations() replays it on reconnect instead of no-op'ing. Tests (client vitest): collabTokenRetry no longer throws on a no-response network error; UserProvider renders cached children / the offline fallback (not a blank fragment) on a network error; a rehydrated paused create/move is replayable via resumePausedMutations; currentUser persist-root coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { AxiosError } from "axios";
|
|
import { collabTokenRetry } from "./auth-query";
|
|
|
|
// Regression for the offline white-screen (#237/#238): offline the collab-token
|
|
// POST rejects as an axios NETWORK error (isAxiosError === true but
|
|
// error.response === undefined). The old predicate read `error.response.status`
|
|
// without a guard and threw an uncaught TypeError inside the React Query retryer
|
|
// BEFORE React mounted, blanking the whole app. The predicate must stay total.
|
|
describe("collabTokenRetry", () => {
|
|
it("does NOT throw and returns a retryable value for a network error with no response (offline)", () => {
|
|
// An axios error with no `response` is exactly the offline/network-failure shape.
|
|
const networkError = new AxiosError("Network Error");
|
|
expect(networkError.response).toBeUndefined();
|
|
|
|
let result: boolean | number = false;
|
|
expect(() => {
|
|
result = collabTokenRetry(0, networkError);
|
|
}).not.toThrow();
|
|
// Network failures stay retryable (truthy), matching the original intent.
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false (no retry) for a real 404 response", () => {
|
|
const notFound = new AxiosError("Not Found");
|
|
notFound.response = { status: 404 } as AxiosError["response"];
|
|
expect(collabTokenRetry(0, notFound)).toBe(false);
|
|
});
|
|
|
|
it("retries for a non-404 response (e.g. 500)", () => {
|
|
const serverError = new AxiosError("Server Error");
|
|
serverError.response = { status: 500 } as AxiosError["response"];
|
|
expect(collabTokenRetry(0, serverError)).toBe(true);
|
|
});
|
|
|
|
it("does not throw and retries for a non-axios error", () => {
|
|
let result: boolean | number = false;
|
|
expect(() => {
|
|
result = collabTokenRetry(0, new Error("boom"));
|
|
}).not.toThrow();
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|