Skip to content

Commit 8d94e73

Browse files
feat(referents): require a filter before searching + display robustness (#3278)
1 parent 7910364 commit 8d94e73

4 files changed

Lines changed: 141 additions & 7 deletions

File tree

packages/app/src/e2e/public-referents.e2e.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,35 @@ test.describe("public referents search", () => {
6868
}
6969
});
7070

71+
test("landing on /referents without a filter shows the empty-filter hint and no results", async ({
72+
browser,
73+
}) => {
74+
const anonCtx = await browser.newContext({ storageState: undefined });
75+
try {
76+
const page = await anonCtx.newPage();
77+
await page.goto("/referents");
78+
await expect(
79+
page.getByText(/remplissez au moins un filtre/i),
80+
).toBeVisible();
81+
await expect(page.getByText("E2E Référent Paris")).not.toBeVisible();
82+
} finally {
83+
await anonCtx.close();
84+
}
85+
});
86+
87+
test("/referents shows the public help banner", async ({ browser }) => {
88+
const anonCtx = await browser.newContext({ storageState: undefined });
89+
try {
90+
const page = await anonCtx.newPage();
91+
await page.goto("/referents");
92+
await expect(
93+
page.getByRole("region", { name: /ressources et aide/i }),
94+
).toBeVisible();
95+
} finally {
96+
await anonCtx.close();
97+
}
98+
});
99+
71100
test("search by region filters the results", async ({ browser }) => {
72101
const anonCtx = await browser.newContext({ storageState: undefined });
73102
try {
@@ -120,6 +149,8 @@ test.describe("public referents search", () => {
120149
try {
121150
const page = await anonCtx.newPage();
122151
await page.goto("/referents");
152+
await page.getByLabel("Région").selectOption("11");
153+
await page.getByRole("button", { name: /^rechercher$/i }).click();
123154
await expect(page.getByText("E2E Référent Paris")).toBeVisible();
124155
await expect(page.getByText("e2e-paris@dreets.test")).not.toBeVisible();
125156
await expect(
@@ -137,6 +168,8 @@ test.describe("public referents search", () => {
137168
try {
138169
const page = await anonCtx.newPage();
139170
await page.goto("/referents");
171+
await page.getByLabel("Région").selectOption("11");
172+
await page.getByRole("button", { name: /^rechercher$/i }).click();
140173

141174
const row = page.locator("li", { hasText: "E2E Référent Paris" });
142175
await row.getByRole("link", { name: /voir le contact/i }).click();

packages/app/src/modules/referents/PublicReferentsPage.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ function ReferentsContent() {
2424
pageSize: PUBLIC_PAGE_SIZE,
2525
};
2626

27+
// Require at least one filter before hitting the search endpoint — the full
28+
// referent list must never be returned (anti-harvest + UX: forcing a filter
29+
// gives results users actually need instead of a paginated dump).
30+
const hasFilter = Boolean(input.query || input.region || input.county);
31+
2732
const { data, isLoading, isError } = api.publicReferents.search.useQuery(
2833
input,
29-
{ placeholderData: (prev) => prev },
34+
{ enabled: hasFilter, placeholderData: (prev) => prev },
3035
);
3136

3237
const handlePageChange = useCallback(
@@ -41,17 +46,23 @@ function ReferentsContent() {
4146
return (
4247
<>
4348
<PublicReferentsSearchForm />
44-
{isLoading && !data && (
49+
{!hasFilter && (
50+
<p className="fr-text--lg fr-text-mention--grey fr-my-4w">
51+
Remplissez au moins un filtre (nom, région ou département) pour lancer
52+
la recherche.
53+
</p>
54+
)}
55+
{hasFilter && isLoading && !data && (
4556
<p aria-live="polite">Chargement des résultats…</p>
4657
)}
47-
{isError && (
58+
{hasFilter && isError && (
4859
<div aria-live="polite" className="fr-alert fr-alert--error">
4960
<p>
5061
Une erreur est survenue lors de la recherche. Veuillez réessayer.
5162
</p>
5263
</div>
5364
)}
54-
{data && (
65+
{hasFilter && data && (
5566
<>
5667
<p
5768
aria-live="polite"

packages/app/src/modules/referents/__tests__/PublicReferentDetail.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,27 @@ describe("PublicReferentDetail", () => {
114114
);
115115
expect(screen.getByText(/^99/)).toBeInTheDocument();
116116
});
117+
118+
it("renders gracefully when the referent name is a region label (e.g. PACA)", () => {
119+
render(
120+
<PublicReferentDetail
121+
referent={{
122+
...baseReferent,
123+
name: "PACA",
124+
county: null,
125+
type: "url",
126+
value: "https://dreets.paca.gouv.fr/contact",
127+
substituteName: null,
128+
substituteEmail: null,
129+
}}
130+
/>,
131+
);
132+
expect(
133+
screen.getByRole("heading", { level: 1, name: "PACA" }),
134+
).toBeInTheDocument();
135+
expect(
136+
screen.getByRole("link", { name: /dreets\.paca\.gouv\.fr/i }),
137+
).toHaveAttribute("href", "https://dreets.paca.gouv.fr/contact");
138+
expect(screen.queryByText(/suppléant/i)).not.toBeInTheDocument();
139+
});
117140
});

packages/app/src/modules/referents/__tests__/PublicReferentsPage.test.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, screen } from "@testing-library/react";
2+
import { useSearchParams } from "next/navigation";
23
import { describe, expect, it, vi } from "vitest";
34

45
vi.mock("next/navigation", async () => {
@@ -28,7 +29,7 @@ import { PublicReferentsPage } from "../PublicReferentsPage";
2829
describe("PublicReferentsPage", () => {
2930
it("renders title and intro paragraph", () => {
3031
useQueryMock.mockReturnValue({
31-
data: { rows: [], total: 0, page: 1, pageSize: 20, totalPages: 1 },
32+
data: undefined,
3233
isLoading: false,
3334
isError: false,
3435
});
@@ -44,7 +45,7 @@ describe("PublicReferentsPage", () => {
4445

4546
it("renders the search form", () => {
4647
useQueryMock.mockReturnValue({
47-
data: { rows: [], total: 0, page: 1, pageSize: 20, totalPages: 1 },
48+
data: undefined,
4849
isLoading: false,
4950
isError: false,
5051
});
@@ -54,7 +55,48 @@ describe("PublicReferentsPage", () => {
5455
).toBeInTheDocument();
5556
});
5657

57-
it("shows empty state when no results", () => {
58+
it("does not call the search API when no filter is set", () => {
59+
useQueryMock.mockClear();
60+
useQueryMock.mockReturnValue({
61+
data: undefined,
62+
isLoading: false,
63+
isError: false,
64+
});
65+
render(<PublicReferentsPage />);
66+
expect(useQueryMock).toHaveBeenCalledWith(
67+
expect.anything(),
68+
expect.objectContaining({ enabled: false }),
69+
);
70+
expect(
71+
screen.getByText(/remplissez au moins un filtre/i),
72+
).toBeInTheDocument();
73+
});
74+
75+
it("enables the search API when at least one filter is set", async () => {
76+
vi.mocked(useSearchParams).mockReturnValueOnce(
77+
new URLSearchParams({ region: "93" }) as ReturnType<
78+
typeof useSearchParams
79+
>,
80+
);
81+
useQueryMock.mockClear();
82+
useQueryMock.mockReturnValue({
83+
data: { rows: [], total: 0, page: 1, pageSize: 20, totalPages: 1 },
84+
isLoading: false,
85+
isError: false,
86+
});
87+
render(<PublicReferentsPage />);
88+
expect(useQueryMock).toHaveBeenCalledWith(
89+
expect.anything(),
90+
expect.objectContaining({ enabled: true }),
91+
);
92+
});
93+
94+
it("shows empty state when filter yields no results", () => {
95+
vi.mocked(useSearchParams).mockReturnValueOnce(
96+
new URLSearchParams({ region: "93" }) as ReturnType<
97+
typeof useSearchParams
98+
>,
99+
);
58100
useQueryMock.mockReturnValue({
59101
data: { rows: [], total: 0, page: 1, pageSize: 20, totalPages: 1 },
60102
isLoading: false,
@@ -65,6 +107,11 @@ describe("PublicReferentsPage", () => {
65107
});
66108

67109
it("renders a list of referents when data is returned", () => {
110+
vi.mocked(useSearchParams).mockReturnValueOnce(
111+
new URLSearchParams({ region: "11" }) as ReturnType<
112+
typeof useSearchParams
113+
>,
114+
);
68115
useQueryMock.mockReturnValue({
69116
data: {
70117
rows: [
@@ -90,6 +137,11 @@ describe("PublicReferentsPage", () => {
90137
});
91138

92139
it("renders the error alert when the query fails", () => {
140+
vi.mocked(useSearchParams).mockReturnValueOnce(
141+
new URLSearchParams({ region: "11" }) as ReturnType<
142+
typeof useSearchParams
143+
>,
144+
);
93145
useQueryMock.mockReturnValue({
94146
data: undefined,
95147
isLoading: false,
@@ -100,6 +152,11 @@ describe("PublicReferentsPage", () => {
100152
});
101153

102154
it("shows a loading state when data is not yet available", () => {
155+
vi.mocked(useSearchParams).mockReturnValueOnce(
156+
new URLSearchParams({ region: "11" }) as ReturnType<
157+
typeof useSearchParams
158+
>,
159+
);
103160
useQueryMock.mockReturnValue({
104161
data: undefined,
105162
isLoading: true,
@@ -110,6 +167,11 @@ describe("PublicReferentsPage", () => {
110167
});
111168

112169
it("renders pagination when there is more than one page", () => {
170+
vi.mocked(useSearchParams).mockReturnValueOnce(
171+
new URLSearchParams({ region: "11" }) as ReturnType<
172+
typeof useSearchParams
173+
>,
174+
);
113175
useQueryMock.mockReturnValue({
114176
data: { rows: [], total: 50, page: 1, pageSize: 20, totalPages: 3 },
115177
isLoading: false,
@@ -122,6 +184,11 @@ describe("PublicReferentsPage", () => {
122184
});
123185

124186
it("does not render pagination when there is only one page", () => {
187+
vi.mocked(useSearchParams).mockReturnValueOnce(
188+
new URLSearchParams({ region: "11" }) as ReturnType<
189+
typeof useSearchParams
190+
>,
191+
);
125192
useQueryMock.mockReturnValue({
126193
data: { rows: [], total: 5, page: 1, pageSize: 20, totalPages: 1 },
127194
isLoading: false,

0 commit comments

Comments
 (0)