Skip to content

Commit 07cb44f

Browse files
committed
Update route tests to match dumb-consumer architecture
CentralRouteManager now owns URL parsing, GraphQL resolution, and reactive-var population for /extracts/:extractId, /users/:slug, and /label_sets/:id. The existing route component tests still drove the old query-based behavior: - UserProfileRoute.test.tsx now asserts that the component reads openedUser/routeLoading/routeError and renders accordingly. The /profile redirect cases moved to a new ProfileRedirect.test.tsx. - ExtractDetailRoute.test.tsx is rewritten to drive the dumb-consumer behavior via openedExtract/routeLoading/routeError directly. - LabelSetLandingRoute.test.tsx no longer expects the route to clear openedLabelset on close — that clear is owned by Phase 1 of the manager. The test now verifies onClose navigates to /label_sets.
1 parent 2d85cee commit 07cb44f

4 files changed

Lines changed: 188 additions & 244 deletions

File tree

Lines changed: 39 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { render, screen, waitFor } from "@testing-library/react";
1+
import { render, screen } from "@testing-library/react";
22
import { MockedProvider } from "@apollo/client/testing";
3-
import { GraphQLError } from "graphql";
43
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
54
import { MemoryRouter, Route, Routes } from "react-router-dom";
65
import { ExtractDetailRoute } from "../ExtractDetailRoute";
7-
import { openedExtract } from "../../../graphql/cache";
8-
import { RESOLVE_EXTRACT_BY_ID } from "../../../graphql/queries";
6+
import {
7+
openedExtract,
8+
routeLoading,
9+
routeError,
10+
} from "../../../graphql/cache";
911
import type { ExtractType } from "../../../types/graphql-api";
1012

1113
vi.mock("../../../views/ExtractDetail", () => ({
@@ -31,13 +33,11 @@ vi.mock("../../widgets/ModernErrorDisplay", () => ({
3133
}));
3234

3335
/**
34-
* Tests for ExtractDetailRoute.
36+
* Tests for ExtractDetailRoute (dumb consumer).
3537
*
36-
* ExtractDetailRoute resolves /extracts/:extractId by either reusing the
37-
* openedExtract reactive var (when it matches the URL id) or executing a
38-
* RESOLVE_EXTRACT_BY_ID query. It surfaces loading, error, and not-found
39-
* states with the ModernLoading/Error displays and defers to ExtractDetail
40-
* on success.
38+
* URL parsing and the RESOLVE_EXTRACT_BY_ID query now live in
39+
* CentralRouteManager. ExtractDetailRoute reads openedExtract / routeLoading
40+
* / routeError and renders one of three states.
4141
*/
4242
describe("ExtractDetailRoute", () => {
4343
const mockExtract: ExtractType = {
@@ -50,115 +50,61 @@ describe("ExtractDetailRoute", () => {
5050
myPermissions: ["read_extract"],
5151
} as unknown as ExtractType;
5252

53-
const renderRoute = (extractId: string | undefined, mocks: any[] = []) => {
54-
const path = extractId ? `/extracts/${extractId}` : "/extracts/";
55-
return render(
56-
<MockedProvider mocks={mocks} addTypename={false}>
57-
<MemoryRouter initialEntries={[path]}>
53+
const renderRoute = () =>
54+
render(
55+
<MockedProvider mocks={[]} addTypename={false}>
56+
<MemoryRouter initialEntries={["/extracts/extract-123"]}>
5857
<Routes>
5958
<Route
6059
path="/extracts/:extractId"
6160
element={<ExtractDetailRoute />}
6261
/>
63-
<Route path="/extracts/" element={<ExtractDetailRoute />} />
6462
</Routes>
6563
</MemoryRouter>
6664
</MockedProvider>
6765
);
68-
};
6966

7067
beforeEach(() => {
7168
openedExtract(null);
69+
routeLoading(false);
70+
routeError(null);
7271
});
7372

7473
afterEach(() => {
7574
vi.clearAllMocks();
7675
openedExtract(null);
76+
routeLoading(false);
77+
routeError(null);
7778
});
7879

79-
it("shows 'No extract ID provided' when extractId param is missing", () => {
80-
renderRoute(undefined);
81-
expect(
82-
screen.getByText(/Error:.*No extract ID provided/)
83-
).toBeInTheDocument();
84-
});
85-
86-
it("skips the query and renders ExtractDetail when reactive var already has a matching extract", () => {
87-
openedExtract({ ...mockExtract, id: "extract-123" } as any);
88-
89-
renderRoute("extract-123");
90-
91-
// Query is skipped because existingExtract.id === extractId, so we
92-
// should see the ExtractDetail view immediately.
93-
expect(screen.getByText("ExtractDetail Component")).toBeInTheDocument();
94-
});
95-
96-
it("renders loading state while resolving an unfamiliar extract id", async () => {
97-
renderRoute("extract-999", [
98-
{
99-
request: {
100-
query: RESOLVE_EXTRACT_BY_ID,
101-
variables: { extractId: "extract-999" },
102-
},
103-
delay: 100,
104-
result: { data: { extract: { ...mockExtract, id: "extract-999" } } },
105-
},
106-
]);
107-
80+
it("shows loading display when routeLoading is true and no extract is resolved", () => {
81+
routeLoading(true);
82+
renderRoute();
10883
expect(screen.getByText("Loading...")).toBeInTheDocument();
10984
});
11085

111-
it("renders error state when RESOLVE_EXTRACT_BY_ID returns an error", async () => {
112-
renderRoute("extract-404", [
113-
{
114-
request: {
115-
query: RESOLVE_EXTRACT_BY_ID,
116-
variables: { extractId: "extract-404" },
117-
},
118-
result: { errors: [new GraphQLError("Extract not found")] },
119-
},
120-
]);
121-
122-
await waitFor(() => {
123-
expect(screen.getByText(/Error:/)).toBeInTheDocument();
124-
});
86+
it("shows error display when routeError is set", () => {
87+
routeError(new Error("Boom"));
88+
renderRoute();
89+
expect(screen.getByText(/Error:.*Boom/)).toBeInTheDocument();
12590
});
12691

127-
it("renders not-found state when the query resolves with no extract", async () => {
128-
renderRoute("extract-missing", [
129-
{
130-
request: {
131-
query: RESOLVE_EXTRACT_BY_ID,
132-
variables: { extractId: "extract-missing" },
133-
},
134-
result: { data: { extract: null } },
135-
},
136-
]);
137-
138-
await waitFor(() => {
139-
expect(screen.getByText(/Error:.*Extract not found/)).toBeInTheDocument();
140-
});
92+
it("shows 'Extract not found' when no extract is resolved and no error/loading is set", () => {
93+
renderRoute();
94+
expect(screen.getByText(/Error:.*Extract not found/)).toBeInTheDocument();
14195
});
14296

143-
it("renders ExtractDetail and sets openedExtract when the query resolves successfully", async () => {
144-
const resolved = {
145-
...mockExtract,
146-
id: "extract-555",
147-
name: "Resolved Extract",
148-
};
149-
renderRoute("extract-555", [
150-
{
151-
request: {
152-
query: RESOLVE_EXTRACT_BY_ID,
153-
variables: { extractId: "extract-555" },
154-
},
155-
result: { data: { extract: resolved } },
156-
},
157-
]);
97+
it("renders ExtractDetail when an extract has been resolved", () => {
98+
openedExtract(mockExtract as any);
99+
renderRoute();
100+
expect(screen.getByText("ExtractDetail Component")).toBeInTheDocument();
101+
});
158102

159-
await waitFor(() => {
160-
expect(screen.getByText("ExtractDetail Component")).toBeInTheDocument();
161-
});
162-
expect(openedExtract()?.id).toBe("extract-555");
103+
it("prefers a resolved extract over the loading state when both are set", () => {
104+
routeLoading(true);
105+
openedExtract(mockExtract as any);
106+
renderRoute();
107+
expect(screen.getByText("ExtractDetail Component")).toBeInTheDocument();
108+
expect(screen.queryByText("Loading...")).toBeNull();
163109
});
164110
});

frontend/src/components/routes/__tests__/LabelSetLandingRoute.test.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
33
import { MockedProvider } from "@apollo/client/testing";
44
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
5-
import { MemoryRouter } from "react-router-dom";
5+
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
66
import { LabelSetLandingRoute } from "../LabelSetLandingRoute";
77
import {
88
openedLabelset,
@@ -99,13 +99,32 @@ describe("LabelSetLandingRoute", () => {
9999
).toBeInTheDocument();
100100
});
101101

102-
it("clears openedLabelset when LabelSetDetailPage invokes onClose", async () => {
102+
it("navigates to /label_sets when LabelSetDetailPage invokes onClose", async () => {
103+
const LocationReporter: React.FC = () => {
104+
const location = useLocation();
105+
return <div data-testid="location">{location.pathname}</div>;
106+
};
107+
103108
openedLabelset(mockLabelset);
104-
renderRoute();
109+
110+
render(
111+
<MockedProvider mocks={[]} addTypename={false}>
112+
<MemoryRouter initialEntries={["/label_sets/ls-1"]}>
113+
<Routes>
114+
<Route path="/label_sets/:id" element={<LabelSetLandingRoute />} />
115+
<Route path="/label_sets" element={<LocationReporter />} />
116+
</Routes>
117+
</MemoryRouter>
118+
</MockedProvider>
119+
);
105120

106121
const closeBtn = screen.getByRole("button", { name: "close-labelset" });
107122
await userEvent.click(closeBtn);
108123

109-
expect(openedLabelset()).toBeNull();
124+
// CentralRouteManager Phase 1 owns the openedLabelset(null) clear when
125+
// the new path resolves to a browse route — the route component just
126+
// navigates and lets the manager handle the var.
127+
const loc = await screen.findByTestId("location");
128+
expect(loc.textContent).toBe("/label_sets");
110129
});
111130
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { MockedProvider } from "@apollo/client/testing";
3+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4+
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
5+
import { ProfileRedirect } from "../ProfileRedirect";
6+
import { backendUserObj } from "../../../graphql/cache";
7+
8+
/**
9+
* Tests for ProfileRedirect.
10+
*
11+
* /profile is auth-state-driven, not URL-state-driven, so the redirect lives
12+
* outside CentralRouteManager. ProfileRedirect reads backendUserObj and
13+
* issues a Navigate to /login (anonymous) or /users/<slug> (logged in).
14+
*/
15+
describe("ProfileRedirect", () => {
16+
const LocationReporter: React.FC = () => {
17+
const location = useLocation();
18+
return <div data-testid="location">{location.pathname}</div>;
19+
};
20+
21+
const renderAt = (path: string) =>
22+
render(
23+
<MockedProvider mocks={[]} addTypename={false}>
24+
<MemoryRouter initialEntries={[path]}>
25+
<Routes>
26+
<Route path="/profile" element={<ProfileRedirect />} />
27+
<Route path="/login" element={<LocationReporter />} />
28+
<Route path="/users/:slug" element={<LocationReporter />} />
29+
</Routes>
30+
</MemoryRouter>
31+
</MockedProvider>
32+
);
33+
34+
beforeEach(() => {
35+
backendUserObj(null);
36+
});
37+
38+
afterEach(() => {
39+
vi.clearAllMocks();
40+
backendUserObj(null);
41+
});
42+
43+
it("redirects to /login when no user is authenticated", async () => {
44+
renderAt("/profile");
45+
const loc = await screen.findByTestId("location");
46+
expect(loc.textContent).toBe("/login");
47+
});
48+
49+
it("redirects to /users/<slug> when a user is authenticated", async () => {
50+
backendUserObj({ id: "u-1", slug: "alice" } as any);
51+
renderAt("/profile");
52+
const loc = await screen.findByTestId("location");
53+
expect(loc.textContent).toBe("/users/alice");
54+
});
55+
56+
it("redirects to /login when the authenticated user has no slug", async () => {
57+
backendUserObj({ id: "u-1" } as any);
58+
renderAt("/profile");
59+
const loc = await screen.findByTestId("location");
60+
expect(loc.textContent).toBe("/login");
61+
});
62+
});

0 commit comments

Comments
 (0)