Skip to content

Commit

Permalink
Merge branch 'main' into data-entry-reducer
Browse files Browse the repository at this point in the history
# Conflicts:
#	frontend/app/module/data_entry/polling_station/AbortDataEntryControl.test.tsx
  • Loading branch information
oliver3 committed Mar 5, 2025
2 parents aae8f53 + b819a8c commit ed2d539
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { userEvent } from "@testing-library/user-event";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { beforeEach, describe, expect, test } from "vitest";

import { ElectionProvider } from "@kiesraad/api";
import {
electionMockData,
ElectionRequestHandler,
PollingStationDataEntryFinaliseHandler,
PollingStationDataEntryGetHandler,
PollingStationDataEntrySaveHandler,
} from "@kiesraad/api-mocks";
import { overrideOnce, renderReturningRouter, screen, server, within } from "@kiesraad/test";
import { renderReturningRouter, screen, server, spyOnHandler, within } from "@kiesraad/test";

import { DataEntryProvider } from "../state/DataEntryProvider";
import { DataEntryState } from "../state/types";
Expand Down Expand Up @@ -71,27 +72,26 @@ function renderForm() {

describe("Test CheckAndSaveForm", () => {
beforeEach(() => {
server.use(ElectionRequestHandler, PollingStationDataEntryGetHandler, PollingStationDataEntrySaveHandler);
server.use(
ElectionRequestHandler,
PollingStationDataEntryGetHandler,
PollingStationDataEntrySaveHandler,
PollingStationDataEntryFinaliseHandler,
);
});

test("Data entry can be finalised", async () => {
const router = renderForm();
const user = userEvent.setup();

// set up a listener to check if the finalisation request is made
let request_method, request_url;
overrideOnce("post", "/api/polling_stations/1/data_entries/1/finalise", 200, null);
server.events.on("request:start", ({ request }) => {
request_method = request.method;
request_url = request.url;
});
const finalise = spyOnHandler(PollingStationDataEntryFinaliseHandler);

// click the save button
await user.click(await screen.findByRole("button", { name: "Opslaan" }));

// check that the finalisation request was made
expect(request_method).toBe("POST");
expect(request_url).toBe("http://localhost:3000/api/polling_stations/1/data_entries/1/finalise");
expect(finalise).toHaveBeenCalledOnce();

// check that the user is navigated back to the data entry page
expect(router.state.location.pathname).toEqual("/elections/1/data-entry");
Expand All @@ -100,17 +100,15 @@ describe("Test CheckAndSaveForm", () => {

test("Shift+Enter submits form", async () => {
renderForm();
const finalise = spyOnHandler(PollingStationDataEntryFinaliseHandler);

expect(await screen.findByRole("group", { name: "Controleren en opslaan" }));

overrideOnce("post", "/api/polling_stations/1/data_entries/1/finalise", 200, null);

const user = userEvent.setup();
const spy = vi.spyOn(global, "fetch");

await user.keyboard("{shift>}{enter}{/shift}");

expect(spy).toHaveBeenCalled();
expect(finalise).toHaveBeenCalled();
});

test("Data entry does not show finalise button with errors", async () => {
Expand Down
25 changes: 6 additions & 19 deletions frontend/app/component/form/user/login/LoginForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import userEvent from "@testing-library/user-event";
import { describe, expect, test } from "vitest";

import { overrideOnce, render, screen, waitFor } from "@kiesraad/test";
import { LoginHandler } from "@kiesraad/api-mocks";
import { overrideOnce, render, screen, server, spyOnHandler, waitFor } from "@kiesraad/test";

import { LoginForm } from "./LoginForm";

describe("LoginForm", () => {
test("Successful login", async () => {
render(<LoginForm />);
let requestBody: object | null = null;
server.use(LoginHandler);
const login = spyOnHandler(LoginHandler);

overrideOnce(
"post",
"/api/user/login",
200,
{
user_id: 1,
username: "admin",
},
undefined,
async (request) => {
requestBody = (await request.json()) as object;
},
);
render(<LoginForm />);

const user = userEvent.setup();

Expand All @@ -34,9 +23,7 @@ describe("LoginForm", () => {
const submitButton = screen.getByRole("button", { name: "Inloggen" });
await user.click(submitButton);

await waitFor(() => {
expect(requestBody).toStrictEqual({ username: "user", password: "password" });
});
expect(login).toHaveBeenCalledWith({ username: "user", password: "password" });
});

test("Unsuccessful login", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { userEvent } from "@testing-library/user-event";
import { beforeEach, describe, expect, test, vi } from "vitest";

import { ElectionProvider, PollingStation } from "@kiesraad/api";
import { ElectionRequestHandler, PollingStationUpdateHandler } from "@kiesraad/api-mocks";
import { overrideOnce, render, renderReturningRouter, server } from "@kiesraad/test";
import {
ElectionRequestHandler,
PollingStationDeleteHandler,
PollingStationGetHandler,
PollingStationUpdateHandler,
} from "@kiesraad/api-mocks";
import { overrideOnce, render, renderReturningRouter, server, spyOnHandler } from "@kiesraad/test";

import { PollingStationUpdatePage } from "./PollingStationUpdatePage";

Expand All @@ -28,17 +33,10 @@ describe("PollingStationUpdatePage", () => {
};

beforeEach(() => {
server.use(ElectionRequestHandler, PollingStationUpdateHandler);
server.use(ElectionRequestHandler, PollingStationGetHandler, PollingStationUpdateHandler);
});

test("Shows form", async () => {
overrideOnce(
"get",
`/api/elections/${testPollingStation.election_id}/polling_stations/${testPollingStation.id}`,
200,
testPollingStation,
);

render(
<ElectionProvider electionId={1}>
<PollingStationUpdatePage />
Expand All @@ -48,18 +46,11 @@ describe("PollingStationUpdatePage", () => {
const form = await screen.findByTestId("polling-station-form");
expect(form).toBeVisible();

expect(screen.getByRole("textbox", { name: "Nummer" })).toHaveValue("1");
expect(screen.getByRole("textbox", { name: "Naam" })).toHaveValue("test");
expect(screen.getByRole("textbox", { name: "Nummer" })).toHaveValue("33");
expect(screen.getByRole("textbox", { name: "Naam" })).toHaveValue("Op Rolletjes");
});

test("Navigates back on save", async () => {
overrideOnce(
"get",
`/api/elections/${testPollingStation.election_id}/polling_stations/${testPollingStation.id}`,
200,
testPollingStation,
);

const router = renderReturningRouter(
<ElectionProvider electionId={1}>
<PollingStationUpdatePage />
Expand All @@ -77,15 +68,9 @@ describe("PollingStationUpdatePage", () => {

describe("Delete polling station", () => {
test("Returns to list page with a message", async () => {
server.use(PollingStationDeleteHandler);
const user = userEvent.setup();

overrideOnce(
"get",
`/api/elections/${testPollingStation.election_id}/polling_stations/${testPollingStation.id}`,
200,
testPollingStation,
);

const router = renderReturningRouter(
<ElectionProvider electionId={1}>
<PollingStationUpdatePage />
Expand All @@ -98,40 +83,21 @@ describe("PollingStationUpdatePage", () => {
const modal = await screen.findByTestId("modal-dialog");
expect(modal).toHaveTextContent("Stembureau verwijderen");

let request_method: string;
let request_url: string;

overrideOnce(
"delete",
`/api/elections/${testPollingStation.election_id}/polling_stations/${testPollingStation.id}`,
200,
"",
);

server.events.on("request:start", ({ request }) => {
request_method = request.method;
request_url = request.url;
});
const deletePollingStation = spyOnHandler(PollingStationDeleteHandler);

const confirmButton = await within(modal).findByRole("button", { name: "Verwijderen" });
await user.click(confirmButton);

await waitFor(() => {
expect(request_method).toEqual("DELETE");
expect(request_url).toContain(
`/api/elections/${testPollingStation.election_id}/polling_stations/${testPollingStation.id}`,
);
});
expect(deletePollingStation).toHaveBeenCalled();

expect(router.state.location.pathname).toEqual("/elections/1/polling-stations");
expect(router.state.location.search).toEqual("?deleted=1%20(test)");
expect(router.state.location.search).toEqual("?deleted=33%20(Op%20Rolletjes)");
});

test("Shows an error message when delete was not possible", async () => {
const user = userEvent.setup();

const url = `/api/elections/${testPollingStation.election_id}/polling_stations/${testPollingStation.id}`;
overrideOnce("get", url, 200, testPollingStation);
overrideOnce("delete", url, 422, {
error: "Invalid data",
fatal: false,
Expand Down
33 changes: 22 additions & 11 deletions frontend/lib/api-mocks/RequestHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { http, type HttpHandler, HttpResponse } from "msw";

import {
ErrorResponse,
LOGIN_REQUEST_BODY,
LOGIN_REQUEST_PARAMS,
LOGIN_REQUEST_PATH,
LoginResponse,
POLLING_STATION_CREATE_REQUEST_PARAMS,
POLLING_STATION_DATA_ENTRY_SAVE_REQUEST_BODY,
POLLING_STATION_DATA_ENTRY_SAVE_REQUEST_PARAMS,
POLLING_STATION_DELETE_REQUEST_PARAMS,
POLLING_STATION_GET_REQUEST_PARAMS,
POLLING_STATION_LIST_REQUEST_PARAMS,
POLLING_STATION_UPDATE_REQUEST_PARAMS,
Expand All @@ -26,11 +30,13 @@ import {
USER_UPDATE_REQUEST_PARAMS,
USER_UPDATE_REQUEST_PATH,
UserListResponse,
WHOAMI_REQUEST_PARAMS,
WHOAMI_REQUEST_PATH,
} from "@kiesraad/api";

import { electionDetailsMockResponse, electionListMockResponse, electionStatusMockResponse } from "./ElectionMockData";
import { pollingStationMockData } from "./PollingStationMockData";
import { userMockData } from "./UserMockData";
import { loginResponseMockData, userMockData } from "./UserMockData";

type ParamsToString<T> = {
[P in keyof T]: string;
Expand All @@ -55,17 +61,16 @@ export const pingHandler = http.post<PingParams, PingRequestBody, PingResponseBo
});
});

export const LoginHandler = http.post<LOGIN_REQUEST_PARAMS, LOGIN_REQUEST_BODY, LoginResponse, LOGIN_REQUEST_PATH>(
"/api/user/login",
() => HttpResponse.json(loginResponseMockData, { status: 200 }),
);

// get user handler
export const WhoAmIRequestHandler = http.get("/api/user/whoami", () => {
const loginResponse: LoginResponse = {
user_id: 1,
fullname: "Example Name",
username: "admin",
role: "administrator",
needs_password_change: false,
};
return HttpResponse.json(loginResponse, { status: 200 });
});
export const WhoAmIRequestHandler = http.get<WHOAMI_REQUEST_PARAMS, null, LoginResponse, WHOAMI_REQUEST_PATH>(
"/api/user/whoami",
() => HttpResponse.json(loginResponseMockData, { status: 200 }),
);

// get election list handler
export const ElectionListRequestHandler = http.get("/api/elections", () =>
Expand Down Expand Up @@ -129,6 +134,11 @@ export const PollingStationCreateHandler = http.post<ParamsToString<POLLING_STAT
() => HttpResponse.json(pollingStationMockData[1]! satisfies PollingStation, { status: 201 }),
);

export const PollingStationDeleteHandler = http.delete<ParamsToString<POLLING_STATION_DELETE_REQUEST_PARAMS>>(
"/api/elections/:election_id/polling_stations/:polling_station_id",
() => HttpResponse.text("", { status: 200 }),
);

export const PollingStationUpdateHandler = http.put<ParamsToString<POLLING_STATION_UPDATE_REQUEST_PARAMS>>(
"/api/elections/:election_id/polling_stations/:polling_station_id",
() => HttpResponse.text("", { status: 200 }),
Expand Down Expand Up @@ -186,6 +196,7 @@ export const handlers: HttpHandler[] = [
PollingStationDataEntryDeleteHandler,
PollingStationDataEntryFinaliseHandler,
PollingStationCreateHandler,
PollingStationDeleteHandler,
PollingStationGetHandler,
PollingStationUpdateHandler,
UserCreateRequestHandler,
Expand Down
10 changes: 9 additions & 1 deletion frontend/lib/api-mocks/UserMockData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User } from "@kiesraad/api";
import { LoginResponse, User } from "@kiesraad/api";

const today = new Date();
today.setHours(10, 20);
Expand Down Expand Up @@ -50,3 +50,11 @@ export const userMockData: User[] = [
updated_at,
},
];

export const loginResponseMockData: LoginResponse = {
user_id: 1,
username: "Sanne",
role: "administrator",
fullname: "Sanne Molenaar",
needs_password_change: true,
};
9 changes: 2 additions & 7 deletions frontend/lib/test/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
* https://github.com/oxidecomputer/console/blob/8dcddcef62b8d10dfcd3adb470439212b23b3d5e/test/unit/server.ts
*/
import { DefaultBodyType, delay, http, HttpResponse, JsonBodyType, StrictRequest } from "msw";
import { delay, http, HttpResponse, JsonBodyType } from "msw";
import { setupServer } from "msw/node";

export const server = setupServer();
Expand All @@ -19,16 +19,11 @@ export function overrideOnce(
status: number,
body: string | null | JsonBodyType,
delayResponse?: "infinite" | number,
onRequest?: (request: StrictRequest<DefaultBodyType>) => Promise<void>,
) {
server.use(
http[method](
path,
async ({ request }) => {
if (onRequest) {
await onRequest(request);
}

async () => {
if (delayResponse) {
await delay(delayResponse);
}
Expand Down
23 changes: 22 additions & 1 deletion frontend/lib/test/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { createMemoryRouter, RouteObject } from "react-router";

import { render, RenderOptions, screen } from "@testing-library/react";
import { UserEvent } from "@testing-library/user-event";
import { expect } from "vitest";
import { HttpHandler, matchRequestUrl } from "msw";
import { expect, vi } from "vitest";

import { Providers } from "./Providers";
import { getRouter } from "./router";
import { server } from "./server";

const customRender = (ui: ReactElement, options?: Omit<RenderOptions, "wrapper">) =>
render(ui, { wrapper: Providers, ...options });
Expand Down Expand Up @@ -68,3 +70,22 @@ export async function userTypeInputs(user: UserEvent, inputs: { [key: string]: s
expect(input).toHaveValue(value.toString());
}
}

export function spyOnHandler(handler: HttpHandler) {
const spy = vi.fn();
const { method, path } = handler.info;

server.events.on("request:start", ({ request }) => {
const url = new URL(request.url);
if (request.method === method && matchRequestUrl(url, path).matches) {
void request
.clone()
.text()
.then((body) => {
spy(body.length ? JSON.parse(body) : null);
});
}
});

return spy;
}

0 comments on commit ed2d539

Please sign in to comment.