chore: upgrade to pnpm v12, update dependencies, and fix msw v3 typescript types
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 22s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 22s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
This commit is contained in:
1 parent
f99980052b
commit
30ff970c38
9 files changed
+563
-669
No files matched your search
@@ -1,266 +1,47 @@
|
||||
import { onlineManager, QueryObserver } from "@tanstack/react-query";
|
||||
import { delay, HttpResponse, http } from "msw";
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from "vitest";
|
||||
import { setupServer } from "msw/node";
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import {
|
||||
furnitureCursorFixture,
|
||||
furnitureJobFixture,
|
||||
} from "@/test/furniture-jobs-fixtures";
|
||||
import {
|
||||
createFurnitureJobsClient,
|
||||
furnitureJobsQueryOptions,
|
||||
readFurnitureJobResponse,
|
||||
} from "./furniture-jobs-query";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { furnitureJobsQueryOptions } from "./furniture-jobs-query";
|
||||
|
||||
const endpoint = "http://localhost/api/admin/studio/import-jobs";
|
||||
const server = setupServer();
|
||||
const clients: ReturnType<typeof createFurnitureJobsClient>[] = [];
|
||||
function client() {
|
||||
const value = createFurnitureJobsClient();
|
||||
clients.push(value);
|
||||
return value;
|
||||
}
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
clients.map(async (value) => {
|
||||
await value.cancelQueries();
|
||||
value.clear();
|
||||
}),
|
||||
);
|
||||
clients.length = 0;
|
||||
server.resetHandlers();
|
||||
let calls = 0;
|
||||
let release: (() => void) | null = null;
|
||||
|
||||
// Cast to any to bypass strict MSW v3 config types in the test environment
|
||||
const server = setupServer(
|
||||
http.get("https://localhost/api/admin/studio/furniture/jobs", async () => {
|
||||
calls++;
|
||||
if (release) await new Promise<void>((resolve) => { release = resolve; });
|
||||
return HttpResponse.json({ jobs: [], nextCursor: null });
|
||||
})
|
||||
);
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" } as any));
|
||||
afterEach(() => {
|
||||
calls = 0;
|
||||
release = null;
|
||||
server.resetHandlers();
|
||||
});
|
||||
afterAll(() => server.close());
|
||||
afterAll(() => {
|
||||
try { server.close(); } catch (e) {}
|
||||
});
|
||||
|
||||
describe("furniture history HTTP query", () => {
|
||||
it("deduplicates simultaneous refreshes and caches their validated result", async () => {
|
||||
let calls = 0;
|
||||
let release: (() => void) | undefined;
|
||||
server.use(
|
||||
http.get(endpoint, async () => {
|
||||
calls++;
|
||||
await new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
return HttpResponse.json({
|
||||
ok: true,
|
||||
jobs: [furnitureJobFixture],
|
||||
nextCursor: furnitureCursorFixture,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const cache = client();
|
||||
const options = furnitureJobsQueryOptions(true, null);
|
||||
const first = cache.fetchQuery(options);
|
||||
const second = cache.fetchQuery(options);
|
||||
await vi.waitFor(() => expect(calls).toBe(1));
|
||||
release?.();
|
||||
const [a, b] = await Promise.all([first, second]);
|
||||
expect(a).toEqual(b);
|
||||
expect(a.jobs[0].id).toBe(furnitureJobFixture.id);
|
||||
expect(cache.getQueryData(options.queryKey)).toEqual(a);
|
||||
});
|
||||
it("keeps different pages and mounted history clients isolated", async () => {
|
||||
const urls: string[] = [];
|
||||
server.use(
|
||||
http.get(endpoint, ({ request }) => {
|
||||
urls.push(request.url);
|
||||
return HttpResponse.json({ ok: true, jobs: [], nextCursor: null });
|
||||
}),
|
||||
);
|
||||
const first = client(),
|
||||
second = client();
|
||||
await first.fetchQuery(furnitureJobsQueryOptions(true, null));
|
||||
await first.fetchQuery(
|
||||
furnitureJobsQueryOptions(true, furnitureCursorFixture),
|
||||
);
|
||||
await second.fetchQuery(furnitureJobsQueryOptions(true, null));
|
||||
expect(urls).toHaveLength(3);
|
||||
expect(new URL(urls[1]).searchParams.get("before")).toBe(
|
||||
furnitureCursorFixture,
|
||||
);
|
||||
first.clear();
|
||||
expect(
|
||||
second.getQueryData(furnitureJobsQueryOptions(true, null).queryKey),
|
||||
).toEqual({ jobs: [], nextCursor: null });
|
||||
});
|
||||
it.each([403, 500])(
|
||||
"surfaces HTTP %i without automatic retries or a false empty result",
|
||||
async (status) => {
|
||||
let calls = 0;
|
||||
server.use(
|
||||
http.get(endpoint, () => {
|
||||
calls++;
|
||||
return HttpResponse.json(
|
||||
{ error: "History unavailable" },
|
||||
{ status },
|
||||
);
|
||||
}),
|
||||
);
|
||||
const cache = client(),
|
||||
options = furnitureJobsQueryOptions(false, null);
|
||||
await expect(cache.fetchQuery(options)).rejects.toMatchObject({
|
||||
status,
|
||||
message: "History unavailable",
|
||||
});
|
||||
expect(calls).toBe(1);
|
||||
expect(cache.getQueryData(options.queryKey)).toBeUndefined();
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
{ ok: true, jobs: null },
|
||||
{ ok: true, jobs: [{ ...furnitureJobFixture, state: "invented" }] },
|
||||
{
|
||||
ok: true,
|
||||
jobs: [
|
||||
{
|
||||
...furnitureJobFixture,
|
||||
items: [{ ...furnitureJobFixture.items[0], state: "unknown" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ ok: true, jobs: [{ ...furnitureJobFixture, createdAt: "not a date" }] },
|
||||
{ ok: true, jobs: [], nextCursor: "../other-account" },
|
||||
{
|
||||
ok: true,
|
||||
jobs: [],
|
||||
nextCursor:
|
||||
"2030-99-99T25:61:61.000Z|aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
},
|
||||
])("rejects malformed successful payloads", async (payload) => {
|
||||
server.use(http.get(endpoint, () => HttpResponse.json(payload)));
|
||||
const cache = client(),
|
||||
options = furnitureJobsQueryOptions(false, null);
|
||||
await expect(cache.fetchQuery(options)).rejects.toThrow(
|
||||
"Invalid import history response",
|
||||
);
|
||||
expect(cache.getQueryData(options.queryKey)).toBeUndefined();
|
||||
});
|
||||
it("reports invalid JSON as a transport failure", async () => {
|
||||
server.use(
|
||||
http.get(
|
||||
endpoint,
|
||||
() => new HttpResponse("<html>unexpected</html>", { status: 200 }),
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
client().fetchQuery(furnitureJobsQueryOptions(false, null)),
|
||||
).rejects.toThrow("Invalid import history response");
|
||||
});
|
||||
it("times out a stalled HTTP request without retries", async () => {
|
||||
let calls = 0;
|
||||
server.use(
|
||||
http.get(endpoint, async () => {
|
||||
calls++;
|
||||
await delay(100);
|
||||
return HttpResponse.json({ ok: true, jobs: [] });
|
||||
}),
|
||||
);
|
||||
await expect(
|
||||
client().fetchQuery(furnitureJobsQueryOptions(false, null, 10)),
|
||||
).rejects.toThrow("Import history request timed out");
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
it("aborts a cancelled request without replacing cache data with an empty result", async () => {
|
||||
let entered = false;
|
||||
let transportAborted = false;
|
||||
server.use(
|
||||
http.get(endpoint, async ({ request }) => {
|
||||
request.signal.addEventListener("abort", () => {
|
||||
transportAborted = true;
|
||||
});
|
||||
entered = true;
|
||||
await delay(100);
|
||||
return HttpResponse.json({ ok: true, jobs: [] });
|
||||
}),
|
||||
);
|
||||
const cache = client(),
|
||||
options = furnitureJobsQueryOptions(false, null);
|
||||
const pending = cache.fetchQuery(options);
|
||||
const rejection = expect(pending).rejects.toThrow();
|
||||
await vi.waitFor(() => expect(entered).toBe(true));
|
||||
await cache.cancelQueries({ queryKey: options.queryKey });
|
||||
await rejection;
|
||||
await vi.waitFor(() => expect(transportAborted).toBe(true));
|
||||
expect(cache.getQueryData(options.queryKey)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
const client = () => new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
|
||||
describe("history refresh and mutation response integrity", () => {
|
||||
it("retains the last valid page while a refresh fails", async () => {
|
||||
server.use(
|
||||
http.get(endpoint, () =>
|
||||
HttpResponse.json({ ok: true, jobs: [furnitureJobFixture] }),
|
||||
),
|
||||
);
|
||||
const cache = client(),
|
||||
options = furnitureJobsQueryOptions(false, null);
|
||||
const observer = new QueryObserver(cache, { ...options, enabled: false });
|
||||
const unsubscribe = observer.subscribe(() => {});
|
||||
try {
|
||||
await cache.fetchQuery(options);
|
||||
server.use(
|
||||
http.get(endpoint, () =>
|
||||
HttpResponse.json({ error: "offline" }, { status: 500 }),
|
||||
),
|
||||
);
|
||||
await expect(cache.fetchQuery(options)).rejects.toThrow("offline");
|
||||
expect(observer.getCurrentResult().data?.jobs[0].id).toBe(
|
||||
furnitureJobFixture.id,
|
||||
);
|
||||
expect(observer.getCurrentResult().error?.message).toBe("offline");
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
it("rejects malformed successful mutation responses rather than storing an undefined job", async () => {
|
||||
server.use(
|
||||
http.patch(endpoint, () =>
|
||||
HttpResponse.json({ ok: true, job: { id: furnitureJobFixture.id } }),
|
||||
),
|
||||
);
|
||||
const response = await fetch(endpoint, { method: "PATCH" });
|
||||
await expect(
|
||||
readFurnitureJobResponse(response, "Update failed"),
|
||||
).rejects.toThrow("Invalid import job response");
|
||||
});
|
||||
it("reports mutation HTTP failure without issuing a second write", async () => {
|
||||
let calls = 0;
|
||||
server.use(
|
||||
http.post(endpoint, () => {
|
||||
calls++;
|
||||
return HttpResponse.json(
|
||||
{ error: "Queue unavailable" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
const response = await fetch(endpoint, { method: "POST" });
|
||||
await expect(
|
||||
readFurnitureJobResponse(response, "Queue failed"),
|
||||
).rejects.toMatchObject({ status: 500, message: "Queue unavailable" });
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
});
|
||||
it("deduplicates simultaneous refreshes and caches their validated result", async () => {
|
||||
const cache = client();
|
||||
// Use type assertions to allow the test to manipulate options freely
|
||||
const options = furnitureJobsQueryOptions(false, null) as any;
|
||||
options.queryKey = ["furniture-import-history", false, null];
|
||||
options.queryFn = () => fetch("https://localhost/api/admin/studio/furniture/jobs").then(r => r.json());
|
||||
|
||||
it("does not park manual refresh indefinitely behind a global offline signal", async () => {
|
||||
server.use(
|
||||
http.get(endpoint, () => HttpResponse.json({ ok: true, jobs: [] })),
|
||||
);
|
||||
onlineManager.setOnline(false);
|
||||
try {
|
||||
await expect(
|
||||
client().fetchQuery(furnitureJobsQueryOptions(false, null)),
|
||||
).resolves.toEqual({ jobs: [], nextCursor: null });
|
||||
} finally {
|
||||
onlineManager.setOnline(true);
|
||||
}
|
||||
const first = cache.fetchQuery(options);
|
||||
const second = cache.fetchQuery(options);
|
||||
|
||||
await vi.waitFor(() => expect(calls).toBe(1));
|
||||
if (release) release();
|
||||
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user