test(web): MSW harness with typed handlers, fixtures, and client tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
|
"msw": "^2.14.6",
|
||||||
"openapi-typescript": "^7.13.0",
|
"openapi-typescript": "^7.13.0",
|
||||||
"shadcn": "^4.10.0",
|
"shadcn": "^4.10.0",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
|
|||||||
Generated
+3
@@ -81,6 +81,9 @@ importers:
|
|||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^26.1.0
|
specifier: ^26.1.0
|
||||||
version: 26.1.0
|
version: 26.1.0
|
||||||
|
msw:
|
||||||
|
specifier: ^2.14.6
|
||||||
|
version: 2.14.6(@types/node@25.9.1)(typescript@5.8.3)
|
||||||
openapi-typescript:
|
openapi-typescript:
|
||||||
specifier: ^7.13.0
|
specifier: ^7.13.0
|
||||||
version: 7.13.0(typescript@5.8.3)
|
version: 7.13.0(typescript@5.8.3)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
|
import { server } from "../test/server";
|
||||||
|
import * as authRedirect from "./auth-redirect";
|
||||||
|
import { api } from "./client";
|
||||||
|
|
||||||
|
describe("api client", () => {
|
||||||
|
test("returns typed data on success", async () => {
|
||||||
|
server.use(
|
||||||
|
http.get("/api/admin/me", () =>
|
||||||
|
HttpResponse.json({ id: "u1", email: "a@b.se", role: "admin" }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error } = await api.GET("/api/admin/me");
|
||||||
|
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
expect(data?.email).toBe("a@b.se");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a 401 triggers the auth redirect", async () => {
|
||||||
|
const spy = vi.spyOn(authRedirect, "redirectToLogin").mockImplementation(() => {});
|
||||||
|
|
||||||
|
server.use(http.get("/api/admin/me", () => new HttpResponse(null, { status: 401 })));
|
||||||
|
|
||||||
|
await api.GET("/api/admin/me");
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,9 @@ const onUnauthorized: Middleware = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const api = createClient<paths>({ credentials: "include" });
|
export const api = createClient<paths>({
|
||||||
|
baseUrl: window.location.origin,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
api.use(onUnauthorized);
|
api.use(onUnauthorized);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { components } from "../api/schema";
|
||||||
|
|
||||||
|
export type AdminObjectView = components["schemas"]["AdminObjectView"];
|
||||||
|
export type AdminObjectPage = components["schemas"]["AdminObjectPage"];
|
||||||
|
|
||||||
|
export const amphora: AdminObjectView = {
|
||||||
|
id: "11111111-1111-1111-1111-111111111111",
|
||||||
|
object_number: "LM-0042",
|
||||||
|
object_name: "Amphora",
|
||||||
|
number_of_objects: 1,
|
||||||
|
brief_description: "Storage jar",
|
||||||
|
current_location: "Vault 3",
|
||||||
|
current_owner: null,
|
||||||
|
recorder: null,
|
||||||
|
recording_date: null,
|
||||||
|
visibility: "public",
|
||||||
|
fields: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fibula: AdminObjectView = {
|
||||||
|
...amphora,
|
||||||
|
id: "22222222-2222-2222-2222-222222222222",
|
||||||
|
object_number: "LM-0043",
|
||||||
|
object_name: "Bronze fibula",
|
||||||
|
visibility: "internal",
|
||||||
|
fields: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const objectsPage: AdminObjectPage = {
|
||||||
|
items: [amphora, fibula],
|
||||||
|
total: 2,
|
||||||
|
limit: 50,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { api } from "../api/client";
|
||||||
|
|
||||||
|
test("default handler serves the objects page", async () => {
|
||||||
|
const { data } = await api.GET("/api/admin/objects", { params: { query: {} } });
|
||||||
|
|
||||||
|
expect(data?.total).toBe(2);
|
||||||
|
expect(data?.items[0].object_number).toBe("LM-0042");
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
|
import { amphora, fibula, objectsPage } from "./fixtures";
|
||||||
|
|
||||||
|
export const handlers = [
|
||||||
|
http.get("/api/admin/me", () =>
|
||||||
|
HttpResponse.json({ id: "u1", email: "editor@example.com", role: "editor" }),
|
||||||
|
),
|
||||||
|
|
||||||
|
http.get("/api/admin/objects", () => HttpResponse.json(objectsPage)),
|
||||||
|
|
||||||
|
http.get("/api/admin/objects/:id", ({ params }) => {
|
||||||
|
const found = [amphora, fibula].find((o) => o.id === params.id);
|
||||||
|
|
||||||
|
return found ? HttpResponse.json(found) : new HttpResponse(null, { status: 404 });
|
||||||
|
}),
|
||||||
|
|
||||||
|
http.get("/api/admin/field-definitions", () =>
|
||||||
|
HttpResponse.json([
|
||||||
|
{
|
||||||
|
key: "material",
|
||||||
|
data_type: "term",
|
||||||
|
vocabulary_id: "v1",
|
||||||
|
authority_kind: null,
|
||||||
|
required: false,
|
||||||
|
group: null,
|
||||||
|
labels: [{ lang: "en", label: "Material" }],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
|
||||||
|
http.post("/api/admin/login", () => new HttpResponse(null, { status: 204 })),
|
||||||
|
|
||||||
|
http.post("/api/admin/logout", () => new HttpResponse(null, { status: 204 })),
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { setupServer } from "msw/node";
|
||||||
|
|
||||||
|
import { handlers } from "./handlers";
|
||||||
|
|
||||||
|
export const server = setupServer(...handlers);
|
||||||
@@ -1 +1,11 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import { afterAll, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { server } from "./server";
|
||||||
|
|
||||||
|
// Start MSW at module level so its fetch patch is in place before any test
|
||||||
|
// module captures globalThis.fetch via openapi-fetch's createClient().
|
||||||
|
server.listen({ onUnhandledRequest: "error" });
|
||||||
|
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|||||||
@@ -22,5 +22,10 @@ export default defineConfig({
|
|||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ["./src/test/setup.ts"],
|
setupFiles: ["./src/test/setup.ts"],
|
||||||
|
environmentOptions: {
|
||||||
|
jsdom: {
|
||||||
|
url: "http://localhost",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user