From 8ecaed4f7ec23bd465f4867d2866fc27b2bccb7f Mon Sep 17 00:00:00 2001 From: Les Date: Mon, 6 May 2024 11:22:56 +0200 Subject: [PATCH 1/9] fix conflict --- .../voters_and_votes/VotersAndVotesForm.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx index d6fcc1b64..05f2cd7fc 100644 --- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx +++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx @@ -22,17 +22,23 @@ export function VotersAndVotesForm() { function handleSubmit(event: React.FormEvent) { event.preventDefault(); const elements = event.currentTarget.elements; - const result = { - pollCards: elements.pollCards.value, - proxyCertificates: elements.proxyCertificates.value, - voterCards: elements.voterCards.value, - totalAdmittedVoters: elements.totalAdmittedVoters.value, - votesOnCandidates: elements.votesOnCandidates.value, - blankVotes: elements.blankVotes.value, - invalidVotes: elements.invalidVotes.value, - totalVotesCast: elements.totalVotesCast.value, + const request = { + data: { + voters_counts: { + poll_card_count: elements.pollCards.value, + proxy_certificate_count: elements.proxyCertificates.value, + voter_card_count: elements.voterCards.value, + total_admitted_voters_count: elements.totalAdmittedVoters.value, + }, + votes_counts: { + votes_candidates_counts: elements.votesOnCandidates.value, + blank_votes_count: elements.blankVotes.value, + invalid_votes_count: elements.invalidVotes.value, + total_votes_cast_coun: elements.totalVotesCast.value, + }, + }, }; - console.log(result); + console.log(request); } return ( From 6a821bb373097a7e8f86ff5099538995df152edd Mon Sep 17 00:00:00 2001 From: Les Date: Mon, 6 May 2024 11:29:20 +0200 Subject: [PATCH 2/9] fix conflict --- .../VotersAndVotesForm.test.tsx | 2 +- .../voters_and_votes/VotersAndVotesForm.tsx | 20 +++++- frontend/app/main.tsx | 5 +- .../input/page/PollingStationPage.test.tsx | 2 +- frontend/app/msw-mock-api.ts | 12 +--- frontend/app/test/unit/Providers.tsx | 11 ++++ frontend/app/test/unit/test-utils.ts | 10 +++ frontend/app/types.d.ts | 2 + frontend/lib/api-mocks/index.ts | 27 +++++++- frontend/lib/api/ApiClient.ts | 20 ++++++ frontend/lib/api/ApiProvider.tsx | 18 ++++++ frontend/lib/api/api.d.ts | 18 ++++++ frontend/lib/api/gen/openapi.ts | 3 +- frontend/lib/api/index.ts | 5 ++ frontend/lib/api/useApi.ts | 11 ++++ frontend/lib/api/useApiRequest.ts | 62 ++++++++++++++++++ .../lib/api/usePollingStationDataEntry.ts | 63 +++++++++++++++++++ .../hook/usePositiveNumberInputMask.test.ts | 9 +++ .../util/hook/usePositiveNumberInputMask.ts | 11 ++++ frontend/package.json | 5 +- frontend/scripts/gen_openapi_types.ts | 2 +- frontend/vite.config.ts | 16 ++++- 22 files changed, 310 insertions(+), 24 deletions(-) create mode 100644 frontend/app/test/unit/Providers.tsx create mode 100644 frontend/app/test/unit/test-utils.ts create mode 100644 frontend/lib/api/ApiClient.ts create mode 100644 frontend/lib/api/ApiProvider.tsx create mode 100644 frontend/lib/api/api.d.ts create mode 100644 frontend/lib/api/index.ts create mode 100644 frontend/lib/api/useApi.ts create mode 100644 frontend/lib/api/useApiRequest.ts create mode 100644 frontend/lib/api/usePollingStationDataEntry.ts diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx index c45d042fb..825abecd0 100644 --- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx +++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen } from "app/test/unit/test-utils"; import { userEvent } from "@testing-library/user-event"; import { describe, expect, test } from "vitest"; diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx index 05f2cd7fc..598636171 100644 --- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx +++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx @@ -1,3 +1,4 @@ +import { usePollingStationDataEntry } from "@kiesraad/api"; import { Button, InputGrid } from "@kiesraad/ui"; import { usePositiveNumberInputMask } from "@kiesraad/util"; @@ -17,12 +18,17 @@ interface VotersAndVotesFormElement extends HTMLFormElement { } export function VotersAndVotesForm() { - const { register, format } = usePositiveNumberInputMask(); + const { register, format, deformat } = usePositiveNumberInputMask(); + const [doSubmit, { data, loading, error }] = usePollingStationDataEntry({ + id: 1, + entry_number: 1 + }); function handleSubmit(event: React.FormEvent) { event.preventDefault(); const elements = event.currentTarget.elements; - const request = { + + doSubmit({ data: { voters_counts: { poll_card_count: elements.pollCards.value, @@ -44,6 +50,12 @@ export function VotersAndVotesForm() { return (

Toegelaten kiezers en uitgebrachte stemmen

+ {data &&

Success

} + {error && ( +

+ Error {error.code} {error.message || ""} +

+ )} Veld @@ -133,7 +145,9 @@ export function VotersAndVotesForm() {

- +
); } diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index c2f2f4fe6..35bdcf3ad 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -5,6 +5,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; // ignore in prod import { startMockAPI } from "./msw-mock-api.ts"; import { routes } from "./routes.tsx"; +import { ApiProvider } from "@kiesraad/api"; const rootDiv = document.getElementById("root"); if (!rootDiv) throw new Error("Root div not found"); @@ -20,7 +21,9 @@ function render() { root.render( - + + + , ); } diff --git a/frontend/app/module/input/page/PollingStationPage.test.tsx b/frontend/app/module/input/page/PollingStationPage.test.tsx index 81480cfaa..862f7a988 100644 --- a/frontend/app/module/input/page/PollingStationPage.test.tsx +++ b/frontend/app/module/input/page/PollingStationPage.test.tsx @@ -1,4 +1,4 @@ -import { render } from "@testing-library/react"; +import { render } from "app/test/unit/test-utils"; import { describe, expect, test } from "vitest"; import { PollingStationPage } from "./PollingStationPage"; diff --git a/frontend/app/msw-mock-api.ts b/frontend/app/msw-mock-api.ts index 23155e54b..6c871c02a 100644 --- a/frontend/app/msw-mock-api.ts +++ b/frontend/app/msw-mock-api.ts @@ -2,28 +2,19 @@ const sleep = async (ms: number) => new Promise((res) => setTimeout(res, ms)); const randInt = (min: number, max: number) => min + Math.floor(Math.random() * (max - min)); export async function startMockAPI() { - // dynamic imports to make extremely sure none of this code ends up in the prod bundle const { handlers } = await import("@kiesraad/api-mocks"); const { http } = await import("msw"); const { setupWorker } = await import("msw/browser"); - // defined in here because it depends on the dynamic import const interceptAll = http.all("/v1/*", async () => { - // random delay on all requests to simulate a real API await sleep(randInt(200, 400)); - // don't return anything means fall through to the real handlers }); // https://mswjs.io/docs/api/setup-worker/start#options await setupWorker(interceptAll, ...handlers).start({ - quiet: true, // don't log successfully handled requests - // custom handler only to make logging less noisy. unhandled requests still - // pass through to the server + quiet: true, onUnhandledRequest(req) { const path = new URL(req.url).pathname; - // Files that get pulled in dynamic imports. It is expected that MSW will - // not handle them and they fall through to the dev server, so warning - // about them is just noise. const ignore = [ path.startsWith("/app"), path.startsWith("/lib"), @@ -32,7 +23,6 @@ export async function startMockAPI() { path.startsWith("/font"), ].some(Boolean); if (!ignore) { - // message format copied from MSW source console.warn(`[MSW] Warning: captured an API request without a matching request handler: • ${req.method} ${path} diff --git a/frontend/app/test/unit/Providers.tsx b/frontend/app/test/unit/Providers.tsx new file mode 100644 index 000000000..e9acee71d --- /dev/null +++ b/frontend/app/test/unit/Providers.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { StrictMode } from "react"; +import { ApiProvider } from "@kiesraad/api"; + +export const Providers = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/frontend/app/test/unit/test-utils.ts b/frontend/app/test/unit/test-utils.ts new file mode 100644 index 000000000..89470cd4d --- /dev/null +++ b/frontend/app/test/unit/test-utils.ts @@ -0,0 +1,10 @@ +import { ReactElement } from "react"; +import { Providers } from "./Providers"; + +import { render, RenderOptions } from "@testing-library/react"; + +const customRender = (ui: ReactElement, options?: Omit) => + render(ui, { wrapper: Providers, ...options }); + +export * from "@testing-library/react"; +export { customRender as render }; diff --git a/frontend/app/types.d.ts b/frontend/app/types.d.ts index 21ca63558..df981d58e 100644 --- a/frontend/app/types.d.ts +++ b/frontend/app/types.d.ts @@ -1,6 +1,8 @@ declare global { namespace NodeJS { interface ProcessEnv { + API_MODE: "mock" | "local"; + API_HOST: string; VERSION: string; } } diff --git a/frontend/lib/api-mocks/index.ts b/frontend/lib/api-mocks/index.ts index c4336dbb2..3c353078f 100644 --- a/frontend/lib/api-mocks/index.ts +++ b/frontend/lib/api-mocks/index.ts @@ -1,4 +1,12 @@ import { http, type HttpHandler, HttpResponse } from "msw"; +import { + POLLING_STATION_DATA_ENTRY_REQUEST_BODY, + POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, +} from "@kiesraad/api"; + +type ParamsToString = { + [P in keyof T]: string; +}; type PingParams = Record; type PingRequestBody = { @@ -21,4 +29,21 @@ const pingHandler = http.post( }, ); -export const handlers: HttpHandler[] = [pingHandler]; +const pollingStationDataEntryHandler = http.post< + ParamsToString, + POLLING_STATION_DATA_ENTRY_REQUEST_BODY +>("/v1/api/polling_stations/:id/data_entries/:entry_number", async ({ request }) => { + let json; + try { + json = await request.json(); + } catch (e) { + //eslint-disable-next-line + return HttpResponse.text(`${e}`, { status: 422 }); + } + if ("voters_counts" in json.data) { + return HttpResponse.text("", { status: 200 }); + } + return HttpResponse.json({ message: "missing fields" }, { status: 500 }); +}); + +export const handlers: HttpHandler[] = [pingHandler, pollingStationDataEntryHandler]; diff --git a/frontend/lib/api/ApiClient.ts b/frontend/lib/api/ApiClient.ts new file mode 100644 index 000000000..8b695777a --- /dev/null +++ b/frontend/lib/api/ApiClient.ts @@ -0,0 +1,20 @@ +export class ApiClient { + host: string; + + constructor(host: string) { + this.host = host; + } + + async postRequest(path: string, requestBody: object): Promise { + const host = process.env.NODE_ENV === "test" ? "http://testhost" : ""; + const response = await fetch(host + "/v1" + path, { + method: "POST", + body: JSON.stringify(requestBody), + headers: { + "Content-Type": "application/json" + } + }); + + return response; + } +} diff --git a/frontend/lib/api/ApiProvider.tsx b/frontend/lib/api/ApiProvider.tsx new file mode 100644 index 000000000..245a26371 --- /dev/null +++ b/frontend/lib/api/ApiProvider.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import { ApiClient } from "./ApiClient"; + +export interface iApiProviderContext { + client: ApiClient; +} + +export const ApiProviderContext = React.createContext(null); + +export interface ApiProviderProps { + host: string; + children: React.ReactNode; +} +export function ApiProvider({ children, host }: ApiProviderProps) { + const client = React.useMemo(() => new ApiClient(host), [host]); + + return {children}; +} diff --git a/frontend/lib/api/api.d.ts b/frontend/lib/api/api.d.ts new file mode 100644 index 000000000..348164f2b --- /dev/null +++ b/frontend/lib/api/api.d.ts @@ -0,0 +1,18 @@ +export interface ApiResponse { + status: string; + code: number; + message?: string; + data?: DATA; +} + +export interface ApiResponseSuccess extends ApiResponse { + status: "20x"; +} + +export interface ApiResponseClientError extends ApiResponse { + status: "40x"; +} + +export interface ApiResponseServerError extends ApiResponse { + status: "50x"; +} diff --git a/frontend/lib/api/gen/openapi.ts b/frontend/lib/api/gen/openapi.ts index 348246c80..1a52626f6 100644 --- a/frontend/lib/api/gen/openapi.ts +++ b/frontend/lib/api/gen/openapi.ts @@ -7,8 +7,7 @@ export interface POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS { id: number; entry_number: number; } -export type POLLING_STATION_DATA_ENTRY_REQUEST_PATH = - `/api/polling_stations/${number}/data_entries/${number};`; +export type POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${number}/data_entries/${number}`; export type POLLING_STATION_DATA_ENTRY_REQUEST_BODY = DataEntryRequest; /** TYPES **/ diff --git a/frontend/lib/api/index.ts b/frontend/lib/api/index.ts new file mode 100644 index 000000000..28cd8582d --- /dev/null +++ b/frontend/lib/api/index.ts @@ -0,0 +1,5 @@ +export * from "./ApiProvider"; +export * from "./ApiClient"; +export * from "./useApiRequest"; +export * from "./usePollingStationDataEntry"; +export * from "./gen/openapi"; diff --git a/frontend/lib/api/useApi.ts b/frontend/lib/api/useApi.ts new file mode 100644 index 000000000..fc4f2f21a --- /dev/null +++ b/frontend/lib/api/useApi.ts @@ -0,0 +1,11 @@ +import * as React from "react"; + +import { ApiProviderContext, iApiProviderContext } from "./ApiProvider"; + +export function useApi() { + const context = React.useContext(ApiProviderContext); + if (context === null) { + throw new Error("useApi must be used within an ApiProvider"); + } + return context; +} diff --git a/frontend/lib/api/useApiRequest.ts b/frontend/lib/api/useApiRequest.ts new file mode 100644 index 000000000..0c5d16209 --- /dev/null +++ b/frontend/lib/api/useApiRequest.ts @@ -0,0 +1,62 @@ +import * as React from "react"; + +import { useApi } from "./useApi"; +import { ApiResponse, ApiResponseSuccess } from "./api"; + +export type UseApiRequestReturn = [ + (requestBody: REQUEST_BODY) => void, + { + loading: boolean; + error: ERROR | null; + data: DATA | null; + } +]; + +export interface UseApiRequestParams { + path: string; + responseHandler: (response: Response) => Promise; +} + +export function useApiRequest({ + path, + responseHandler +}: UseApiRequestParams): UseApiRequestReturn { + const { client } = useApi(); + const [data, setData] = React.useState(null); + const [error, setError] = React.useState(null); + const [apiRequest, setApiRequest] = React.useState(null); + + React.useEffect(() => { + const doRequest = async (b: REQUEST_BODY) => { + const response = await client.postRequest(path, b as object); + + const result = await responseHandler(response); + if (result.status === "20x") { + setData(result as DATA); + } else { + setError(result as ERROR); + } + }; + + if (apiRequest) { + doRequest(apiRequest).catch((e: unknown) => { + console.error(e); + }); + } + }, [apiRequest, client, path, responseHandler]); + + const makeRequest = React.useCallback((requestBody: REQUEST_BODY) => { + setApiRequest(requestBody); + }, []); + + const loading = false; + + return [ + makeRequest, + { + loading, + error, + data + } + ]; +} diff --git a/frontend/lib/api/usePollingStationDataEntry.ts b/frontend/lib/api/usePollingStationDataEntry.ts new file mode 100644 index 000000000..ad265e2d1 --- /dev/null +++ b/frontend/lib/api/usePollingStationDataEntry.ts @@ -0,0 +1,63 @@ +import * as React from "react"; +import { useApiRequest } from "./useApiRequest"; +import { + DataEntryError, + POLLING_STATION_DATA_ENTRY_REQUEST_BODY, + POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, + POLLING_STATION_DATA_ENTRY_REQUEST_PATH +} from "./gen/openapi"; +import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; + +//TODO: add to generate script +//TODO: Make camelcase +type POLLING_STATION_DATA_ENTRY_RESPONSE = + | (Omit & { + status: 200; + json: () => ApiResponseSuccess | PromiseLike; + }) + | (Omit & { + status: 422; + json: () => ApiResponseClientError | PromiseLike; + }) + | (Omit & { + status: 500; + json: () => ApiResponseServerError | PromiseLike>; + }) + | (Omit & { + status: number; + json: () => never; + }); + +export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS) { + const path = React.useMemo(() => { + const result: POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${params.id}/data_entries/${params.entry_number}`; + return result; + }, [params]); + + const responseHandler = React.useCallback(async (response: Response) => { + const res = response as POLLING_STATION_DATA_ENTRY_RESPONSE; + if (res.status === 200) { + return { status: "20x", code: 200, message: "OK" } as ApiResponseSuccess; + } else if (res.status === 422) { + return { status: "40x", code: 422, message: "Unprocessable Entity" } as ApiResponseClientError; + } else if (res.status === 500) { + const data = await res.json(); + return { + status: "50x", + code: 500, + message: "Internal Server Error", + data + } as ApiResponseServerError; + } + throw new Error(`Unexpected response status: ${res.status}`); + }, []); + + return useApiRequest< + POLLING_STATION_DATA_ENTRY_REQUEST_BODY, + ApiResponseSuccess, + ApiResponseServerError | ApiResponseClientError + >({ + path, + responseHandler + }); +} diff --git a/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts b/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts index 6ab8ce4fa..e2fe526a2 100644 --- a/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts +++ b/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts @@ -65,4 +65,13 @@ describe("useInputMask", () => { const { result } = renderHook(() => usePositiveNumberInputMask()); expect(result.current.format(input)).equals("0"); }); + + test("reverse format", () => { + const { result } = renderHook(() => usePositiveNumberInputMask()); + + const testNumber = 12345; + const formatted = result.current.format(testNumber); + const deformatted = result.current.deformat(formatted); + expect(deformatted).equals(testNumber); + }); }); diff --git a/frontend/lib/util/hook/usePositiveNumberInputMask.ts b/frontend/lib/util/hook/usePositiveNumberInputMask.ts index 2e8df1d71..d57384909 100644 --- a/frontend/lib/util/hook/usePositiveNumberInputMask.ts +++ b/frontend/lib/util/hook/usePositiveNumberInputMask.ts @@ -1,9 +1,11 @@ import * as React from "react"; export type FormatFunc = (s: string | number | null | undefined) => string; +export type DefomatFunc = (s: string) => number; export interface UsePositiveNumberInputMaskReturn { format: FormatFunc; + deformat: DefomatFunc; register: () => { onChange: React.ChangeEventHandler; onLoad: React.ChangeEventHandler; @@ -26,6 +28,14 @@ export function usePositiveNumberInputMask(): UsePositiveNumberInputMaskReturn { return result; }, []); + const deformat: DefomatFunc = React.useCallback((s: string) => { + const seperator = numberFormatter.format(11111).replace(/\p{Number}/gu, ""); + const escapedSeparator = seperator.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); + + const cleaned = s.replace(new RegExp(escapedSeparator, "g"), ""); + return parseInt(cleaned, 10); + }, []); + const onChange: React.ChangeEventHandler = React.useCallback( (event) => { //remove all non numbers @@ -55,6 +65,7 @@ export function usePositiveNumberInputMask(): UsePositiveNumberInputMaskReturn { return { format, + deformat, register, }; } diff --git a/frontend/package.json b/frontend/package.json index 8c9527cbd..4b2a25ecf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,12 +7,13 @@ "node": ">=20.11.1" }, "scripts": { - "dev": "API_MODE=msw vite", + "dev": "API_MODE=mock vite", + "dev:server": "API_MODE=local vite", "build": "tsc && vite build", "build:msw": "API_MODE=msw vite build && cp mockServiceWorker.js dist/", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", - "test": "vitest", + "test": "vitest ", "e2e": "playwright test", "e2eui": "playwright test --ui", "ladle": "ladle serve", diff --git a/frontend/scripts/gen_openapi_types.ts b/frontend/scripts/gen_openapi_types.ts index 5a686c8a5..68917b8ff 100644 --- a/frontend/scripts/gen_openapi_types.ts +++ b/frontend/scripts/gen_openapi_types.ts @@ -73,7 +73,7 @@ function addPath(path: string, v: PathsObject | undefined) { }); } result.push("}"); - result.push(`export type ${id}_REQUEST_PATH = \`${requestPath};\``); + result.push(`export type ${id}_REQUEST_PATH = \`${requestPath}\`;`); if (post.requestBody) { if ("$ref" in post.requestBody) { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2a50a9970..a38543b89 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,6 +4,9 @@ import react from "@vitejs/plugin-react-swc"; import tsConfig from "./tsconfig.json"; import pkgjson from "./package.json"; +const apiMode = process.env.API_MODE || "mock"; +const apiHost = process.env.API_HOST || apiMode === "mock" ? "" : "http://localhost:8080"; + // https://vitejs.dev/config/ export default defineConfig(() => ({ build: { @@ -13,8 +16,9 @@ export default defineConfig(() => ({ // minify: false, // uncomment for debugging }, define: { - "process.env.MSW": true, + "process.env.MSW": apiMode === "mock", "process.env.VERSION": JSON.stringify(pkgjson.version), + "process.env.API_HOST": JSON.stringify(apiHost), }, optimizeDeps: { exclude: ["msw"] }, plugins: [react()], @@ -35,6 +39,16 @@ export default defineConfig(() => ({ }, }, }, + server: { + port: 3000, + proxy: { + "/v1": { + target: apiHost, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/v1/, ""), + }, + }, + }, test: { environment: "jsdom", setupFiles: ["app/test/unit/setup.ts"], From 645e8262ab8394354a950940dd485578affcc7d9 Mon Sep 17 00:00:00 2001 From: Mark Janssen <20283+praseodym@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:54:42 +0200 Subject: [PATCH 3/9] Fix InputGrid casing --- .../lib/ui/InputGrid/{inputgrid.e2e.ts => InputGrid.e2e.ts} | 2 +- .../ui/InputGrid/{inputgrid.module.css => InputGrid.module.css} | 0 .../InputGrid/{inputgrid.stories.tsx => InputGrid.stories.tsx} | 0 .../lib/ui/InputGrid/{inputgrid.test.tsx => InputGrid.test.tsx} | 0 frontend/lib/ui/InputGrid/InputGrid.tsx | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) rename frontend/lib/ui/InputGrid/{inputgrid.e2e.ts => InputGrid.e2e.ts} (95%) rename frontend/lib/ui/InputGrid/{inputgrid.module.css => InputGrid.module.css} (100%) rename frontend/lib/ui/InputGrid/{inputgrid.stories.tsx => InputGrid.stories.tsx} (100%) rename frontend/lib/ui/InputGrid/{inputgrid.test.tsx => InputGrid.test.tsx} (100%) diff --git a/frontend/lib/ui/InputGrid/inputgrid.e2e.ts b/frontend/lib/ui/InputGrid/InputGrid.e2e.ts similarity index 95% rename from frontend/lib/ui/InputGrid/inputgrid.e2e.ts rename to frontend/lib/ui/InputGrid/InputGrid.e2e.ts index 79dd32c76..79e336c08 100644 --- a/frontend/lib/ui/InputGrid/inputgrid.e2e.ts +++ b/frontend/lib/ui/InputGrid/InputGrid.e2e.ts @@ -2,7 +2,7 @@ import { Locator, test as base, expect } from "@playwright/test"; const test = base.extend<{ gridPage: Locator }>({ gridPage: async ({ page }, use) => { - await page.goto("http://localhost:61000/?story=inputgrid--default-grid"); + await page.goto("http://localhost:61000/?story=input-grid--default-grid"); const main = page.locator("main.ladle-main"); const grid = main.locator("table"); await grid.waitFor(); diff --git a/frontend/lib/ui/InputGrid/inputgrid.module.css b/frontend/lib/ui/InputGrid/InputGrid.module.css similarity index 100% rename from frontend/lib/ui/InputGrid/inputgrid.module.css rename to frontend/lib/ui/InputGrid/InputGrid.module.css diff --git a/frontend/lib/ui/InputGrid/inputgrid.stories.tsx b/frontend/lib/ui/InputGrid/InputGrid.stories.tsx similarity index 100% rename from frontend/lib/ui/InputGrid/inputgrid.stories.tsx rename to frontend/lib/ui/InputGrid/InputGrid.stories.tsx diff --git a/frontend/lib/ui/InputGrid/inputgrid.test.tsx b/frontend/lib/ui/InputGrid/InputGrid.test.tsx similarity index 100% rename from frontend/lib/ui/InputGrid/inputgrid.test.tsx rename to frontend/lib/ui/InputGrid/InputGrid.test.tsx diff --git a/frontend/lib/ui/InputGrid/InputGrid.tsx b/frontend/lib/ui/InputGrid/InputGrid.tsx index eed5d0444..cebf30906 100644 --- a/frontend/lib/ui/InputGrid/InputGrid.tsx +++ b/frontend/lib/ui/InputGrid/InputGrid.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import cls from "./inputgrid.module.css"; +import cls from "./InputGrid.module.css"; import { cn, domtoren } from "@kiesraad/util"; export interface InputGridProps { From 09141474fab93d73453c1c951ccfcfe02823b4c9 Mon Sep 17 00:00:00 2001 From: Les Date: Mon, 6 May 2024 11:38:49 +0200 Subject: [PATCH 4/9] fix conflict --- frontend/.babelrc | 5 +- frontend/.eslintrc.cjs | 5 +- frontend/app/component/footer/Footer.tsx | 3 +- .../voters_and_votes/VotersAndVotesForm.tsx | 21 +- .../module/input/page/PollingStationPage.tsx | 28 +- frontend/app/msw-mock-api.ts | 3 +- frontend/app/test/unit/test-utils.ts | 6 +- frontend/lib/api-mocks/index.ts | 34 ++- frontend/lib/api/ApiClient.ts | 4 +- frontend/lib/api/ApiProvider.tsx | 9 +- frontend/lib/api/api.d.ts | 6 +- frontend/lib/api/gen/openapi.ts | 3 +- frontend/lib/api/useApi.ts | 4 +- frontend/lib/api/useApiRequest.ts | 20 +- .../lib/api/usePollingStationDataEntry.ts | 26 +- frontend/lib/ui/Button/Button.tsx | 3 +- frontend/lib/ui/InputGrid/InputGrid.tsx | 4 +- frontend/lib/ui/ProgressList/ProgressList.tsx | 6 +- frontend/lib/ui/style/variables.css | 6 +- frontend/lib/util/classnames.ts | 6 +- .../hook/usePositiveNumberInputMask.test.ts | 11 +- frontend/mockServiceWorker.js | 284 ++++++++++++++++++ frontend/playwright.config.ts | 6 +- frontend/scripts/gen_icons.js | 4 +- frontend/scripts/gen_openapi_types.ts | 16 +- frontend/scripts/openapi.ts | 7 +- 26 files changed, 457 insertions(+), 73 deletions(-) create mode 100644 frontend/mockServiceWorker.js diff --git a/frontend/.babelrc b/frontend/.babelrc index 2d2434ada..08d007ea3 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -1,3 +1,6 @@ { - "presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }]] + "presets": [ + "@babel/preset-env", + ["@babel/preset-react", { "runtime": "automatic" }] + ] } diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 854834a3c..cb02d86cd 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -12,7 +12,10 @@ module.exports = { parser: "@typescript-eslint/parser", plugins: ["react-refresh", "jsx-a11y", "prettier", "@typescript-eslint"], rules: { - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], "@typescript-eslint/restrict-template-expressions": [ "error", { diff --git a/frontend/app/component/footer/Footer.tsx b/frontend/app/component/footer/Footer.tsx index 1601bf491..77ef250c6 100644 --- a/frontend/app/component/footer/Footer.tsx +++ b/frontend/app/component/footer/Footer.tsx @@ -3,7 +3,8 @@ export function Footer() {
URN-Uitslag
- Server {process.env.MSW ? "Mocked" : "Live"}    + Server {process.env.MSW ? "Mocked" : "Live"}{" "} +    Versie v{process.env.VERSION}
diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx index 598636171..816957890 100644 --- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx +++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx @@ -21,7 +21,7 @@ export function VotersAndVotesForm() { const { register, format, deformat } = usePositiveNumberInputMask(); const [doSubmit, { data, loading, error }] = usePollingStationDataEntry({ id: 1, - entry_number: 1 + entry_number: 1, }); function handleSubmit(event: React.FormEvent) { @@ -31,20 +31,19 @@ export function VotersAndVotesForm() { doSubmit({ data: { voters_counts: { - poll_card_count: elements.pollCards.value, - proxy_certificate_count: elements.proxyCertificates.value, - voter_card_count: elements.voterCards.value, - total_admitted_voters_count: elements.totalAdmittedVoters.value, + poll_card_count: deformat(elements.pollCards.value), + proxy_certificate_count: deformat(elements.proxyCertificates.value), + voter_card_count: deformat(elements.voterCards.value), + total_admitted_voters_count: deformat(elements.totalAdmittedVoters.value), }, votes_counts: { - votes_candidates_counts: elements.votesOnCandidates.value, - blank_votes_count: elements.blankVotes.value, - invalid_votes_count: elements.invalidVotes.value, - total_votes_cast_coun: elements.totalVotesCast.value, + votes_candidates_counts: deformat(elements.votesOnCandidates.value), + blank_votes_count: deformat(elements.blankVotes.value), + invalid_votes_count: deformat(elements.invalidVotes.value), + total_votes_cast_count: deformat(elements.totalVotesCast.value), }, }, - }; - console.log(request); + }); } return ( diff --git a/frontend/app/module/input/page/PollingStationPage.tsx b/frontend/app/module/input/page/PollingStationPage.tsx index 4660628df..32941acb2 100644 --- a/frontend/app/module/input/page/PollingStationPage.tsx +++ b/frontend/app/module/input/page/PollingStationPage.tsx @@ -1,4 +1,10 @@ -import { Button, PollingStationNumber, ProgressList, Tag, WorkStationNumber } from "@kiesraad/ui"; +import { + Button, + PollingStationNumber, + ProgressList, + Tag, + WorkStationNumber, +} from "@kiesraad/ui"; import { useParams } from "react-router-dom"; import { IconCross } from "@kiesraad/icon"; import { VotersAndVotesForm } from "app/component/form/voters_and_votes/VotersAndVotesForm"; @@ -27,13 +33,23 @@ export function PollingStationPage() {
diff --git a/frontend/app/msw-mock-api.ts b/frontend/app/msw-mock-api.ts index 6c871c02a..24b429dfb 100644 --- a/frontend/app/msw-mock-api.ts +++ b/frontend/app/msw-mock-api.ts @@ -1,5 +1,6 @@ const sleep = async (ms: number) => new Promise((res) => setTimeout(res, ms)); -const randInt = (min: number, max: number) => min + Math.floor(Math.random() * (max - min)); +const randInt = (min: number, max: number) => + min + Math.floor(Math.random() * (max - min)); export async function startMockAPI() { const { handlers } = await import("@kiesraad/api-mocks"); diff --git a/frontend/app/test/unit/test-utils.ts b/frontend/app/test/unit/test-utils.ts index 89470cd4d..1fae3d924 100644 --- a/frontend/app/test/unit/test-utils.ts +++ b/frontend/app/test/unit/test-utils.ts @@ -3,8 +3,10 @@ import { Providers } from "./Providers"; import { render, RenderOptions } from "@testing-library/react"; -const customRender = (ui: ReactElement, options?: Omit) => - render(ui, { wrapper: Providers, ...options }); +const customRender = ( + ui: ReactElement, + options?: Omit, +) => render(ui, { wrapper: Providers, ...options }); export * from "@testing-library/react"; export { customRender as render }; diff --git a/frontend/lib/api-mocks/index.ts b/frontend/lib/api-mocks/index.ts index 3c353078f..b5168c0d7 100644 --- a/frontend/lib/api-mocks/index.ts +++ b/frontend/lib/api-mocks/index.ts @@ -32,18 +32,24 @@ const pingHandler = http.post( const pollingStationDataEntryHandler = http.post< ParamsToString, POLLING_STATION_DATA_ENTRY_REQUEST_BODY ->("/v1/api/polling_stations/:id/data_entries/:entry_number", async ({ request }) => { - let json; - try { - json = await request.json(); - } catch (e) { - //eslint-disable-next-line - return HttpResponse.text(`${e}`, { status: 422 }); - } - if ("voters_counts" in json.data) { - return HttpResponse.text("", { status: 200 }); - } - return HttpResponse.json({ message: "missing fields" }, { status: 500 }); -}); +>( + "/v1/api/polling_stations/:id/data_entries/:entry_number", + async ({ request }) => { + let json; + try { + json = await request.json(); + } catch (e) { + //eslint-disable-next-line + return HttpResponse.text(`${e}`, { status: 422 }); + } + if ("voters_counts" in json.data) { + return HttpResponse.text("", { status: 200 }); + } + return HttpResponse.json({ message: "missing fields" }, { status: 500 }); + }, +); -export const handlers: HttpHandler[] = [pingHandler, pollingStationDataEntryHandler]; +export const handlers: HttpHandler[] = [ + pingHandler, + pollingStationDataEntryHandler, +]; diff --git a/frontend/lib/api/ApiClient.ts b/frontend/lib/api/ApiClient.ts index 8b695777a..0d2741363 100644 --- a/frontend/lib/api/ApiClient.ts +++ b/frontend/lib/api/ApiClient.ts @@ -11,8 +11,8 @@ export class ApiClient { method: "POST", body: JSON.stringify(requestBody), headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, }); return response; diff --git a/frontend/lib/api/ApiProvider.tsx b/frontend/lib/api/ApiProvider.tsx index 245a26371..d209adb27 100644 --- a/frontend/lib/api/ApiProvider.tsx +++ b/frontend/lib/api/ApiProvider.tsx @@ -5,7 +5,8 @@ export interface iApiProviderContext { client: ApiClient; } -export const ApiProviderContext = React.createContext(null); +export const ApiProviderContext = + React.createContext(null); export interface ApiProviderProps { host: string; @@ -14,5 +15,9 @@ export interface ApiProviderProps { export function ApiProvider({ children, host }: ApiProviderProps) { const client = React.useMemo(() => new ApiClient(host), [host]); - return {children}; + return ( + + {children} + + ); } diff --git a/frontend/lib/api/api.d.ts b/frontend/lib/api/api.d.ts index 348164f2b..28fb46bf5 100644 --- a/frontend/lib/api/api.d.ts +++ b/frontend/lib/api/api.d.ts @@ -9,10 +9,12 @@ export interface ApiResponseSuccess extends ApiResponse { status: "20x"; } -export interface ApiResponseClientError extends ApiResponse { +export interface ApiResponseClientError + extends ApiResponse { status: "40x"; } -export interface ApiResponseServerError extends ApiResponse { +export interface ApiResponseServerError + extends ApiResponse { status: "50x"; } diff --git a/frontend/lib/api/gen/openapi.ts b/frontend/lib/api/gen/openapi.ts index 1a52626f6..c3991160a 100644 --- a/frontend/lib/api/gen/openapi.ts +++ b/frontend/lib/api/gen/openapi.ts @@ -7,7 +7,8 @@ export interface POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS { id: number; entry_number: number; } -export type POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${number}/data_entries/${number}`; +export type POLLING_STATION_DATA_ENTRY_REQUEST_PATH = + `/api/polling_stations/${number}/data_entries/${number}`; export type POLLING_STATION_DATA_ENTRY_REQUEST_BODY = DataEntryRequest; /** TYPES **/ diff --git a/frontend/lib/api/useApi.ts b/frontend/lib/api/useApi.ts index fc4f2f21a..31f849e92 100644 --- a/frontend/lib/api/useApi.ts +++ b/frontend/lib/api/useApi.ts @@ -3,7 +3,9 @@ import * as React from "react"; import { ApiProviderContext, iApiProviderContext } from "./ApiProvider"; export function useApi() { - const context = React.useContext(ApiProviderContext); + const context = React.useContext( + ApiProviderContext, + ); if (context === null) { throw new Error("useApi must be used within an ApiProvider"); } diff --git a/frontend/lib/api/useApiRequest.ts b/frontend/lib/api/useApiRequest.ts index 0c5d16209..dafcdc64b 100644 --- a/frontend/lib/api/useApiRequest.ts +++ b/frontend/lib/api/useApiRequest.ts @@ -9,7 +9,7 @@ export type UseApiRequestReturn = [ loading: boolean; error: ERROR | null; data: DATA | null; - } + }, ]; export interface UseApiRequestParams { @@ -17,10 +17,18 @@ export interface UseApiRequestParams { responseHandler: (response: Response) => Promise; } -export function useApiRequest({ +export function useApiRequest< + REQUEST_BODY, + DATA extends ApiResponseSuccess, + ERROR extends ApiResponse, +>({ path, - responseHandler -}: UseApiRequestParams): UseApiRequestReturn { + responseHandler, +}: UseApiRequestParams): UseApiRequestReturn< + REQUEST_BODY, + DATA, + ERROR +> { const { client } = useApi(); const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); @@ -56,7 +64,7 @@ export function useApiRequest & { status: 500; - json: () => ApiResponseServerError | PromiseLike>; + json: () => + | ApiResponseServerError + | PromiseLike>; }) | (Omit & { status: number; json: () => never; }); -export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS) { +export function usePollingStationDataEntry( + params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, +) { const path = React.useMemo(() => { const result: POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${params.id}/data_entries/${params.entry_number}`; return result; @@ -39,14 +47,18 @@ export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_RE if (res.status === 200) { return { status: "20x", code: 200, message: "OK" } as ApiResponseSuccess; } else if (res.status === 422) { - return { status: "40x", code: 422, message: "Unprocessable Entity" } as ApiResponseClientError; + return { + status: "40x", + code: 422, + message: "Unprocessable Entity", + } as ApiResponseClientError; } else if (res.status === 500) { const data = await res.json(); return { status: "50x", code: 500, message: "Internal Server Error", - data + data, } as ApiResponseServerError; } throw new Error(`Unexpected response status: ${res.status}`); @@ -58,6 +70,6 @@ export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_RE ApiResponseServerError | ApiResponseClientError >({ path, - responseHandler + responseHandler, }); } diff --git a/frontend/lib/ui/Button/Button.tsx b/frontend/lib/ui/Button/Button.tsx index 75a89feda..2de2ea875 100644 --- a/frontend/lib/ui/Button/Button.tsx +++ b/frontend/lib/ui/Button/Button.tsx @@ -1,7 +1,8 @@ import * as React from "react"; import cls from "./Button.module.css"; -export interface ButtonProps extends React.ButtonHTMLAttributes { +export interface ButtonProps + extends React.ButtonHTMLAttributes { isDisabled?: boolean; isLoading?: boolean; variant?: "default" | "secondary"; diff --git a/frontend/lib/ui/InputGrid/InputGrid.tsx b/frontend/lib/ui/InputGrid/InputGrid.tsx index cebf30906..133b22db7 100644 --- a/frontend/lib/ui/InputGrid/InputGrid.tsx +++ b/frontend/lib/ui/InputGrid/InputGrid.tsx @@ -126,7 +126,9 @@ InputGrid.Header = ({ ); -InputGrid.Body = ({ children }: { children: React.ReactNode }) => {children}; +InputGrid.Body = ({ children }: { children: React.ReactNode }) => ( + {children} +); InputGrid.Seperator = () => ( diff --git a/frontend/lib/ui/ProgressList/ProgressList.tsx b/frontend/lib/ui/ProgressList/ProgressList.tsx index ee0b7066e..e750d0669 100644 --- a/frontend/lib/ui/ProgressList/ProgressList.tsx +++ b/frontend/lib/ui/ProgressList/ProgressList.tsx @@ -32,7 +32,11 @@ export type ProgressListItemProps = BaseProgressListItemProps & } ); -ProgressList.Item = function ({ status, active, children }: ProgressListItemProps) { +ProgressList.Item = function ({ + status, + active, + children, +}: ProgressListItemProps) { return (
  • diff --git a/frontend/lib/ui/style/variables.css b/frontend/lib/ui/style/variables.css index 46041abf7..7eb7f9b5c 100644 --- a/frontend/lib/ui/style/variables.css +++ b/frontend/lib/ui/style/variables.css @@ -1,6 +1,7 @@ @font-face { font-family: "SpaceGrotesk"; - src: url("/font/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf") format("TrueType"); + src: url("/font/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf") + format("TrueType"); } @font-face { @@ -10,7 +11,8 @@ @font-face { font-family: "DMSansItalic"; - src: url("/font/DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf") format("TrueType"); + src: url("/font/DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf") + format("TrueType"); } @font-face { diff --git a/frontend/lib/util/classnames.ts b/frontend/lib/util/classnames.ts index 1141b6e57..0c9728b25 100644 --- a/frontend/lib/util/classnames.ts +++ b/frontend/lib/util/classnames.ts @@ -1,5 +1,9 @@ export function classnames( - ...args: (string | undefined | Record)[] + ...args: ( + | string + | undefined + | Record + )[] ): string { const classes: string[] = []; diff --git a/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts b/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts index e2fe526a2..82625a532 100644 --- a/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts +++ b/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts @@ -61,10 +61,13 @@ describe("useInputMask", () => { expect(result.current.format(input)).equals(expected); }); - test.each(["A", "AB", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])("String %s becomes '0'", (input: string) => { - const { result } = renderHook(() => usePositiveNumberInputMask()); - expect(result.current.format(input)).equals("0"); - }); + test.each(["A", "AB", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])( + "String %s becomes '0'", + (input: string) => { + const { result } = renderHook(() => usePositiveNumberInputMask()); + expect(result.current.format(input)).equals("0"); + }, + ); test("reverse format", () => { const { result } = renderHook(() => usePositiveNumberInputMask()); diff --git a/frontend/mockServiceWorker.js b/frontend/mockServiceWorker.js new file mode 100644 index 000000000..481e5dcaa --- /dev/null +++ b/frontend/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = "2.2.13"; +const INTEGRITY_CHECKSUM = "26357c79639bfa20d64c0efca2a87423"; +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); +const activeClientIds = new Set(); + +self.addEventListener("install", function () { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", async function (event) { + const clientId = event.source.id; + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + switch (event.data) { + case "KEEPALIVE_REQUEST": { + sendToClient(client, { + type: "KEEPALIVE_RESPONSE", + }); + break; + } + + case "INTEGRITY_CHECK_REQUEST": { + sendToClient(client, { + type: "INTEGRITY_CHECK_RESPONSE", + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }); + break; + } + + case "MOCK_ACTIVATE": { + activeClientIds.add(clientId); + + sendToClient(client, { + type: "MOCKING_ENABLED", + payload: true, + }); + break; + } + + case "MOCK_DEACTIVATE": { + activeClientIds.delete(clientId); + break; + } + + case "CLIENT_CLOSED": { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +}); + +self.addEventListener("fetch", function (event) { + const { request } = event; + + // Bypass navigation requests. + if (request.mode === "navigate") { + return; + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === "only-if-cached" && request.mode !== "same-origin") { + return; + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return; + } + + // Generate unique request ID. + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId)); +}); + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event); + const response = await getResponse(event, client, requestId); + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + (async function () { + const responseClone = response.clone(); + + sendToClient( + client, + { + type: "RESPONSE", + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ); + })(); + } + + return response; +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId); + + if (client?.frameType === "top-level") { + return client; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === "visible"; + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id); + }); +} + +async function getResponse(event, client, requestId) { + const { request } = event; + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone(); + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()); + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers["x-msw-intention"]; + + return fetch(requestClone, { headers }); + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough(); + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough(); + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer(); + const clientMessage = await sendToClient( + client, + { + type: "REQUEST", + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ); + + switch (clientMessage.type) { + case "MOCK_RESPONSE": { + return respondWithMock(clientMessage.data); + } + + case "PASSTHROUGH": { + return passthrough(); + } + } + + return passthrough(); +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error); + } + + resolve(event.data); + }; + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ); + }); +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error(); + } + + const mockedResponse = new Response(response.body, response); + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }); + + return mockedResponse; +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index dbfba53dc..3446c59e2 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,4 +1,8 @@ -import { devices, type PlaywrightTestConfig, defineConfig } from "@playwright/test"; +import { + devices, + type PlaywrightTestConfig, + defineConfig, +} from "@playwright/test"; /** * See https://playwright.dev/docs/test-configuration. diff --git a/frontend/scripts/gen_icons.js b/frontend/scripts/gen_icons.js index 0792c02ea..80de24c38 100644 --- a/frontend/scripts/gen_icons.js +++ b/frontend/scripts/gen_icons.js @@ -11,7 +11,9 @@ async function run() { //replace kebab-case with camelCase content = content.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); - result.push(`export const Icon${ucfirst(file.replace(".svg", ""))} = () => (${content});`); + result.push( + `export const Icon${ucfirst(file.replace(".svg", ""))} = () => (${content});`, + ); }); let s = result.join("\n"); diff --git a/frontend/scripts/gen_openapi_types.ts b/frontend/scripts/gen_openapi_types.ts index 68917b8ff..c6fd0fc24 100644 --- a/frontend/scripts/gen_openapi_types.ts +++ b/frontend/scripts/gen_openapi_types.ts @@ -1,7 +1,13 @@ import assert from "assert"; import fs from "fs"; import prettier from "prettier"; -import { OpenAPIV3, ReferenceObject, SchemaObject, PathsObject, OperationObject } from "./openapi"; +import { + OpenAPIV3, + ReferenceObject, + SchemaObject, + PathsObject, + OperationObject, +} from "./openapi"; const TARGET_PATH = "./lib/api/gen"; const FILE_NAME = "openapi.ts"; @@ -27,8 +33,12 @@ async function run() { throw new Error("No schemas found in OpenAPI spec"); } - const paths: string[] = Object.entries(spec.paths).map(([k, v]) => addPath(k, v)); - const types: string[] = Object.entries(schemas).map(([k, v]) => addDefinition(k, v)); + const paths: string[] = Object.entries(spec.paths).map(([k, v]) => + addPath(k, v), + ); + const types: string[] = Object.entries(schemas).map(([k, v]) => + addDefinition(k, v), + ); result.push("\n\n"); diff --git a/frontend/scripts/openapi.ts b/frontend/scripts/openapi.ts index f455e9171..ffa67b052 100644 --- a/frontend/scripts/openapi.ts +++ b/frontend/scripts/openapi.ts @@ -143,7 +143,12 @@ export interface ParameterBaseObject { examples?: { [media: string]: ReferenceObject | ExampleObject }; content?: { [media: string]: MediaTypeObject }; } -export type NonArraySchemaObjectType = "boolean" | "object" | "number" | "string" | "integer"; +export type NonArraySchemaObjectType = + | "boolean" + | "object" + | "number" + | "string" + | "integer"; export type ArraySchemaObjectType = "array"; export type SchemaObject = ArraySchemaObject | NonArraySchemaObject; From 6eafd56dbaa5f672f33a89394f19911ff21bd561 Mon Sep 17 00:00:00 2001 From: Les Date: Mon, 6 May 2024 12:00:03 +0200 Subject: [PATCH 5/9] prettier reformat --- frontend/.babelrc | 5 +-- frontend/.eslintrc.cjs | 5 +-- frontend/app/component/footer/Footer.tsx | 3 +- .../module/input/page/PollingStationPage.tsx | 28 +++------------ frontend/app/msw-mock-api.ts | 3 +- frontend/app/test/unit/test-utils.ts | 6 ++-- frontend/lib/api-mocks/index.ts | 34 ++++++++----------- frontend/lib/api/ApiProvider.tsx | 9 ++--- frontend/lib/api/api.d.ts | 6 ++-- frontend/lib/api/useApi.ts | 4 +-- frontend/lib/api/useApiRequest.ts | 6 +--- .../lib/api/usePollingStationDataEntry.ts | 10 ++---- frontend/lib/ui/Button/Button.tsx | 3 +- frontend/lib/ui/InputGrid/InputGrid.tsx | 4 +-- frontend/lib/ui/ProgressList/ProgressList.tsx | 6 +--- frontend/lib/ui/style/variables.css | 6 ++-- frontend/lib/util/classnames.ts | 6 +--- .../hook/usePositiveNumberInputMask.test.ts | 11 +++--- frontend/playwright.config.ts | 6 +--- frontend/scripts/gen_icons.js | 4 +-- frontend/scripts/gen_openapi_types.ts | 16 ++------- frontend/scripts/openapi.ts | 7 +--- 22 files changed, 49 insertions(+), 139 deletions(-) diff --git a/frontend/.babelrc b/frontend/.babelrc index 08d007ea3..2d2434ada 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -1,6 +1,3 @@ { - "presets": [ - "@babel/preset-env", - ["@babel/preset-react", { "runtime": "automatic" }] - ] + "presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }]] } diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index cb02d86cd..854834a3c 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -12,10 +12,7 @@ module.exports = { parser: "@typescript-eslint/parser", plugins: ["react-refresh", "jsx-a11y", "prettier", "@typescript-eslint"], rules: { - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "@typescript-eslint/restrict-template-expressions": [ "error", { diff --git a/frontend/app/component/footer/Footer.tsx b/frontend/app/component/footer/Footer.tsx index 77ef250c6..1601bf491 100644 --- a/frontend/app/component/footer/Footer.tsx +++ b/frontend/app/component/footer/Footer.tsx @@ -3,8 +3,7 @@ export function Footer() {
    URN-Uitslag
    - Server {process.env.MSW ? "Mocked" : "Live"}{" "} -    + Server {process.env.MSW ? "Mocked" : "Live"}    Versie v{process.env.VERSION}
    diff --git a/frontend/app/module/input/page/PollingStationPage.tsx b/frontend/app/module/input/page/PollingStationPage.tsx index 32941acb2..4660628df 100644 --- a/frontend/app/module/input/page/PollingStationPage.tsx +++ b/frontend/app/module/input/page/PollingStationPage.tsx @@ -1,10 +1,4 @@ -import { - Button, - PollingStationNumber, - ProgressList, - Tag, - WorkStationNumber, -} from "@kiesraad/ui"; +import { Button, PollingStationNumber, ProgressList, Tag, WorkStationNumber } from "@kiesraad/ui"; import { useParams } from "react-router-dom"; import { IconCross } from "@kiesraad/icon"; import { VotersAndVotesForm } from "app/component/form/voters_and_votes/VotersAndVotesForm"; @@ -33,23 +27,13 @@ export function PollingStationPage() {
    diff --git a/frontend/app/msw-mock-api.ts b/frontend/app/msw-mock-api.ts index 24b429dfb..6c871c02a 100644 --- a/frontend/app/msw-mock-api.ts +++ b/frontend/app/msw-mock-api.ts @@ -1,6 +1,5 @@ const sleep = async (ms: number) => new Promise((res) => setTimeout(res, ms)); -const randInt = (min: number, max: number) => - min + Math.floor(Math.random() * (max - min)); +const randInt = (min: number, max: number) => min + Math.floor(Math.random() * (max - min)); export async function startMockAPI() { const { handlers } = await import("@kiesraad/api-mocks"); diff --git a/frontend/app/test/unit/test-utils.ts b/frontend/app/test/unit/test-utils.ts index 1fae3d924..89470cd4d 100644 --- a/frontend/app/test/unit/test-utils.ts +++ b/frontend/app/test/unit/test-utils.ts @@ -3,10 +3,8 @@ import { Providers } from "./Providers"; import { render, RenderOptions } from "@testing-library/react"; -const customRender = ( - ui: ReactElement, - options?: Omit, -) => render(ui, { wrapper: Providers, ...options }); +const customRender = (ui: ReactElement, options?: Omit) => + render(ui, { wrapper: Providers, ...options }); export * from "@testing-library/react"; export { customRender as render }; diff --git a/frontend/lib/api-mocks/index.ts b/frontend/lib/api-mocks/index.ts index b5168c0d7..3c353078f 100644 --- a/frontend/lib/api-mocks/index.ts +++ b/frontend/lib/api-mocks/index.ts @@ -32,24 +32,18 @@ const pingHandler = http.post( const pollingStationDataEntryHandler = http.post< ParamsToString, POLLING_STATION_DATA_ENTRY_REQUEST_BODY ->( - "/v1/api/polling_stations/:id/data_entries/:entry_number", - async ({ request }) => { - let json; - try { - json = await request.json(); - } catch (e) { - //eslint-disable-next-line - return HttpResponse.text(`${e}`, { status: 422 }); - } - if ("voters_counts" in json.data) { - return HttpResponse.text("", { status: 200 }); - } - return HttpResponse.json({ message: "missing fields" }, { status: 500 }); - }, -); +>("/v1/api/polling_stations/:id/data_entries/:entry_number", async ({ request }) => { + let json; + try { + json = await request.json(); + } catch (e) { + //eslint-disable-next-line + return HttpResponse.text(`${e}`, { status: 422 }); + } + if ("voters_counts" in json.data) { + return HttpResponse.text("", { status: 200 }); + } + return HttpResponse.json({ message: "missing fields" }, { status: 500 }); +}); -export const handlers: HttpHandler[] = [ - pingHandler, - pollingStationDataEntryHandler, -]; +export const handlers: HttpHandler[] = [pingHandler, pollingStationDataEntryHandler]; diff --git a/frontend/lib/api/ApiProvider.tsx b/frontend/lib/api/ApiProvider.tsx index d209adb27..245a26371 100644 --- a/frontend/lib/api/ApiProvider.tsx +++ b/frontend/lib/api/ApiProvider.tsx @@ -5,8 +5,7 @@ export interface iApiProviderContext { client: ApiClient; } -export const ApiProviderContext = - React.createContext(null); +export const ApiProviderContext = React.createContext(null); export interface ApiProviderProps { host: string; @@ -15,9 +14,5 @@ export interface ApiProviderProps { export function ApiProvider({ children, host }: ApiProviderProps) { const client = React.useMemo(() => new ApiClient(host), [host]); - return ( - - {children} - - ); + return {children}; } diff --git a/frontend/lib/api/api.d.ts b/frontend/lib/api/api.d.ts index 28fb46bf5..348164f2b 100644 --- a/frontend/lib/api/api.d.ts +++ b/frontend/lib/api/api.d.ts @@ -9,12 +9,10 @@ export interface ApiResponseSuccess extends ApiResponse { status: "20x"; } -export interface ApiResponseClientError - extends ApiResponse { +export interface ApiResponseClientError extends ApiResponse { status: "40x"; } -export interface ApiResponseServerError - extends ApiResponse { +export interface ApiResponseServerError extends ApiResponse { status: "50x"; } diff --git a/frontend/lib/api/useApi.ts b/frontend/lib/api/useApi.ts index 31f849e92..fc4f2f21a 100644 --- a/frontend/lib/api/useApi.ts +++ b/frontend/lib/api/useApi.ts @@ -3,9 +3,7 @@ import * as React from "react"; import { ApiProviderContext, iApiProviderContext } from "./ApiProvider"; export function useApi() { - const context = React.useContext( - ApiProviderContext, - ); + const context = React.useContext(ApiProviderContext); if (context === null) { throw new Error("useApi must be used within an ApiProvider"); } diff --git a/frontend/lib/api/useApiRequest.ts b/frontend/lib/api/useApiRequest.ts index dafcdc64b..880e5ea61 100644 --- a/frontend/lib/api/useApiRequest.ts +++ b/frontend/lib/api/useApiRequest.ts @@ -24,11 +24,7 @@ export function useApiRequest< >({ path, responseHandler, -}: UseApiRequestParams): UseApiRequestReturn< - REQUEST_BODY, - DATA, - ERROR -> { +}: UseApiRequestParams): UseApiRequestReturn { const { client } = useApi(); const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); diff --git a/frontend/lib/api/usePollingStationDataEntry.ts b/frontend/lib/api/usePollingStationDataEntry.ts index 4452f7801..e20ab174f 100644 --- a/frontend/lib/api/usePollingStationDataEntry.ts +++ b/frontend/lib/api/usePollingStationDataEntry.ts @@ -6,11 +6,7 @@ import { POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, POLLING_STATION_DATA_ENTRY_REQUEST_PATH, } from "./gen/openapi"; -import { - ApiResponseClientError, - ApiResponseSuccess, - ApiResponseServerError, -} from "./api"; +import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; //TODO: add to generate script //TODO: Make camelcase @@ -34,9 +30,7 @@ type POLLING_STATION_DATA_ENTRY_RESPONSE = json: () => never; }); -export function usePollingStationDataEntry( - params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, -) { +export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS) { const path = React.useMemo(() => { const result: POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${params.id}/data_entries/${params.entry_number}`; return result; diff --git a/frontend/lib/ui/Button/Button.tsx b/frontend/lib/ui/Button/Button.tsx index 2de2ea875..75a89feda 100644 --- a/frontend/lib/ui/Button/Button.tsx +++ b/frontend/lib/ui/Button/Button.tsx @@ -1,8 +1,7 @@ import * as React from "react"; import cls from "./Button.module.css"; -export interface ButtonProps - extends React.ButtonHTMLAttributes { +export interface ButtonProps extends React.ButtonHTMLAttributes { isDisabled?: boolean; isLoading?: boolean; variant?: "default" | "secondary"; diff --git a/frontend/lib/ui/InputGrid/InputGrid.tsx b/frontend/lib/ui/InputGrid/InputGrid.tsx index 133b22db7..cebf30906 100644 --- a/frontend/lib/ui/InputGrid/InputGrid.tsx +++ b/frontend/lib/ui/InputGrid/InputGrid.tsx @@ -126,9 +126,7 @@ InputGrid.Header = ({ ); -InputGrid.Body = ({ children }: { children: React.ReactNode }) => ( - {children} -); +InputGrid.Body = ({ children }: { children: React.ReactNode }) => {children}; InputGrid.Seperator = () => ( diff --git a/frontend/lib/ui/ProgressList/ProgressList.tsx b/frontend/lib/ui/ProgressList/ProgressList.tsx index e750d0669..ee0b7066e 100644 --- a/frontend/lib/ui/ProgressList/ProgressList.tsx +++ b/frontend/lib/ui/ProgressList/ProgressList.tsx @@ -32,11 +32,7 @@ export type ProgressListItemProps = BaseProgressListItemProps & } ); -ProgressList.Item = function ({ - status, - active, - children, -}: ProgressListItemProps) { +ProgressList.Item = function ({ status, active, children }: ProgressListItemProps) { return (
  • diff --git a/frontend/lib/ui/style/variables.css b/frontend/lib/ui/style/variables.css index 7eb7f9b5c..46041abf7 100644 --- a/frontend/lib/ui/style/variables.css +++ b/frontend/lib/ui/style/variables.css @@ -1,7 +1,6 @@ @font-face { font-family: "SpaceGrotesk"; - src: url("/font/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf") - format("TrueType"); + src: url("/font/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf") format("TrueType"); } @font-face { @@ -11,8 +10,7 @@ @font-face { font-family: "DMSansItalic"; - src: url("/font/DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf") - format("TrueType"); + src: url("/font/DM_Sans/DMSans-Italic-VariableFont_opsz,wght.ttf") format("TrueType"); } @font-face { diff --git a/frontend/lib/util/classnames.ts b/frontend/lib/util/classnames.ts index 0c9728b25..1141b6e57 100644 --- a/frontend/lib/util/classnames.ts +++ b/frontend/lib/util/classnames.ts @@ -1,9 +1,5 @@ export function classnames( - ...args: ( - | string - | undefined - | Record - )[] + ...args: (string | undefined | Record)[] ): string { const classes: string[] = []; diff --git a/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts b/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts index 82625a532..e2fe526a2 100644 --- a/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts +++ b/frontend/lib/util/hook/usePositiveNumberInputMask.test.ts @@ -61,13 +61,10 @@ describe("useInputMask", () => { expect(result.current.format(input)).equals(expected); }); - test.each(["A", "AB", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])( - "String %s becomes '0'", - (input: string) => { - const { result } = renderHook(() => usePositiveNumberInputMask()); - expect(result.current.format(input)).equals("0"); - }, - ); + test.each(["A", "AB", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])("String %s becomes '0'", (input: string) => { + const { result } = renderHook(() => usePositiveNumberInputMask()); + expect(result.current.format(input)).equals("0"); + }); test("reverse format", () => { const { result } = renderHook(() => usePositiveNumberInputMask()); diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 3446c59e2..dbfba53dc 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,8 +1,4 @@ -import { - devices, - type PlaywrightTestConfig, - defineConfig, -} from "@playwright/test"; +import { devices, type PlaywrightTestConfig, defineConfig } from "@playwright/test"; /** * See https://playwright.dev/docs/test-configuration. diff --git a/frontend/scripts/gen_icons.js b/frontend/scripts/gen_icons.js index 80de24c38..0792c02ea 100644 --- a/frontend/scripts/gen_icons.js +++ b/frontend/scripts/gen_icons.js @@ -11,9 +11,7 @@ async function run() { //replace kebab-case with camelCase content = content.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); - result.push( - `export const Icon${ucfirst(file.replace(".svg", ""))} = () => (${content});`, - ); + result.push(`export const Icon${ucfirst(file.replace(".svg", ""))} = () => (${content});`); }); let s = result.join("\n"); diff --git a/frontend/scripts/gen_openapi_types.ts b/frontend/scripts/gen_openapi_types.ts index c6fd0fc24..68917b8ff 100644 --- a/frontend/scripts/gen_openapi_types.ts +++ b/frontend/scripts/gen_openapi_types.ts @@ -1,13 +1,7 @@ import assert from "assert"; import fs from "fs"; import prettier from "prettier"; -import { - OpenAPIV3, - ReferenceObject, - SchemaObject, - PathsObject, - OperationObject, -} from "./openapi"; +import { OpenAPIV3, ReferenceObject, SchemaObject, PathsObject, OperationObject } from "./openapi"; const TARGET_PATH = "./lib/api/gen"; const FILE_NAME = "openapi.ts"; @@ -33,12 +27,8 @@ async function run() { throw new Error("No schemas found in OpenAPI spec"); } - const paths: string[] = Object.entries(spec.paths).map(([k, v]) => - addPath(k, v), - ); - const types: string[] = Object.entries(schemas).map(([k, v]) => - addDefinition(k, v), - ); + const paths: string[] = Object.entries(spec.paths).map(([k, v]) => addPath(k, v)); + const types: string[] = Object.entries(schemas).map(([k, v]) => addDefinition(k, v)); result.push("\n\n"); diff --git a/frontend/scripts/openapi.ts b/frontend/scripts/openapi.ts index ffa67b052..f455e9171 100644 --- a/frontend/scripts/openapi.ts +++ b/frontend/scripts/openapi.ts @@ -143,12 +143,7 @@ export interface ParameterBaseObject { examples?: { [media: string]: ReferenceObject | ExampleObject }; content?: { [media: string]: MediaTypeObject }; } -export type NonArraySchemaObjectType = - | "boolean" - | "object" - | "number" - | "string" - | "integer"; +export type NonArraySchemaObjectType = "boolean" | "object" | "number" | "string" | "integer"; export type ArraySchemaObjectType = "array"; export type SchemaObject = ArraySchemaObject | NonArraySchemaObject; From 65842e88193d2935e3982ed8143bcdd393f324c8 Mon Sep 17 00:00:00 2001 From: Les Date: Mon, 6 May 2024 13:48:56 +0200 Subject: [PATCH 6/9] broken commit --- backend/src/polling_station/mod.rs | 7 ++++ frontend/lib/api/useApiRequest.test.ts | 37 +++++++++++++++++++ .../api/usePollingStationDataEntry.temp.ts | 15 ++++++++ .../lib/api/usePollingStationDataEntry.ts | 16 +++++++- 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 frontend/lib/api/useApiRequest.test.ts create mode 100644 frontend/lib/api/usePollingStationDataEntry.temp.ts diff --git a/backend/src/polling_station/mod.rs b/backend/src/polling_station/mod.rs index 2040fcd55..954ffd49f 100644 --- a/backend/src/polling_station/mod.rs +++ b/backend/src/polling_station/mod.rs @@ -15,6 +15,13 @@ pub struct DataEntryRequest { pub struct DataEntryError { pub message: String, } +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ApiResponse { + pub status: u32, + pub code: String, //should be an enum + pub messsage: String, + pub data: Data, +} /// PollingStationResults, following the fields in /// "Model N 10-1. Proces-verbaal van een stembureau" diff --git a/frontend/lib/api/useApiRequest.test.ts b/frontend/lib/api/useApiRequest.test.ts new file mode 100644 index 000000000..c06fe3e9a --- /dev/null +++ b/frontend/lib/api/useApiRequest.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "vitest"; +import { renderHook } from "app/test/unit/test-utils"; +import { useApiRequest } from "./useApiRequest"; +import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; + +const responseHandler = async (response: Response) => { + const res = response as POLLING_STATION_DATA_ENTRY_RESPONSE; + if (res.status === 200) { + return { status: "20x", code: 200, message: "OK" } as ApiResponseSuccess; + } else if (res.status === 422) { + return { + status: "40x", + code: 422, + message: "Unprocessable Entity", + } as ApiResponseClientError; + } else if (res.status === 500) { + const data = await res.json(); + return { + status: "50x", + code: 500, + message: "Internal Server Error", + data, + } as ApiResponseServerError; + } + throw new Error(`Unexpected response status: ${res.status}`); +}; + +describe("useApiRequest", () => { + test("it renders", async () => { + const { result, waitFor } = renderHook(() => + useApiRequest( + "/ping", + responseHandler, + ), + ); + }); +}); diff --git a/frontend/lib/api/usePollingStationDataEntry.temp.ts b/frontend/lib/api/usePollingStationDataEntry.temp.ts new file mode 100644 index 000000000..8f1773659 --- /dev/null +++ b/frontend/lib/api/usePollingStationDataEntry.temp.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import { renderHook } from "app/test/unit/test-utils"; +import { useApiRequest } from "./useApiRequest"; +import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; + + + + +describe("useApiRequest", () => { + + test("it renders", async () => { + const { result, waitFor } = renderHook(() => useApiRequest()); + + }); +}); diff --git a/frontend/lib/api/usePollingStationDataEntry.ts b/frontend/lib/api/usePollingStationDataEntry.ts index e20ab174f..76f5198b8 100644 --- a/frontend/lib/api/usePollingStationDataEntry.ts +++ b/frontend/lib/api/usePollingStationDataEntry.ts @@ -5,12 +5,13 @@ import { POLLING_STATION_DATA_ENTRY_REQUEST_BODY, POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, POLLING_STATION_DATA_ENTRY_REQUEST_PATH, + POLLING_STATION_DATA_ENTRY_RESPONSE_BODY, } from "./gen/openapi"; import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; //TODO: add to generate script //TODO: Make camelcase -type POLLING_STATION_DATA_ENTRY_RESPONSE = +export type POLLING_STATION_DATA_ENTRY_RESPONSE = | (Omit & { status: 200; json: () => ApiResponseSuccess | PromiseLike; @@ -30,6 +31,19 @@ type POLLING_STATION_DATA_ENTRY_RESPONSE = json: () => never; }); + + + +export const error:object = { + status: 500, + code: "DATABASE_CONNECTION_ERROR", + message: "Could not connect to database", +}; + + + + + export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS) { const path = React.useMemo(() => { const result: POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${params.id}/data_entries/${params.entry_number}`; From 0fa76b8da5bd833c23c702a653d5c3c36e01dbc8 Mon Sep 17 00:00:00 2001 From: Les Date: Tue, 14 May 2024 15:22:34 +0200 Subject: [PATCH 7/9] refactor ApiClient add example test --- .../voters_and_votes/VotersAndVotesForm.tsx | 2 +- frontend/app/test/unit/index.ts | 3 + frontend/lib/api/ApiClient.ts | 71 ++++++++++++++++++- frontend/lib/api/useApiRequest.test.ts | 37 ---------- frontend/lib/api/useApiRequest.ts | 46 ++++++------ .../api/usePollingStationDataEntry.temp.ts | 15 ---- .../api/usePollingStationDataEntry.test.ts | 48 +++++++++++++ .../lib/api/usePollingStationDataEntry.ts | 68 ++---------------- 8 files changed, 149 insertions(+), 141 deletions(-) create mode 100644 frontend/app/test/unit/index.ts delete mode 100644 frontend/lib/api/useApiRequest.test.ts delete mode 100644 frontend/lib/api/usePollingStationDataEntry.temp.ts create mode 100644 frontend/lib/api/usePollingStationDataEntry.test.ts diff --git a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx index 816957890..25da60fbc 100644 --- a/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx +++ b/frontend/app/component/form/voters_and_votes/VotersAndVotesForm.tsx @@ -52,7 +52,7 @@ export function VotersAndVotesForm() { {data &&

    Success

    } {error && (

    - Error {error.code} {error.message || ""} + Error {error.errorCode} {error.message || ""}

    )} diff --git a/frontend/app/test/unit/index.ts b/frontend/app/test/unit/index.ts new file mode 100644 index 000000000..dc518755c --- /dev/null +++ b/frontend/app/test/unit/index.ts @@ -0,0 +1,3 @@ +export * from "./server"; +export * from "./test-utils"; +export * from "./Providers"; diff --git a/frontend/lib/api/ApiClient.ts b/frontend/lib/api/ApiClient.ts index 0d2741363..7a1db34a4 100644 --- a/frontend/lib/api/ApiClient.ts +++ b/frontend/lib/api/ApiClient.ts @@ -1,3 +1,44 @@ +export interface ApiResponse { + status: string; + code: number; + data?: DATA; +} + +export interface ApiResponseSuccess extends ApiResponse { + status: "success"; +} + +export interface ApiResponseClientError extends ApiResponse { + status: "client_error"; +} + +export interface ApiResponseServerError extends ApiResponse { + status: "server_error"; +} + +export interface ApiResponseErrorData { + errorCode: string; + message: string; +} + +export type ApiServerResponse = + | (Omit & { + status: 200; + json: () => DATA | PromiseLike; + }) + | (Omit & { + status: 422; + json: () => ApiResponseErrorData | PromiseLike; + }) + | (Omit & { + status: 500; + json: () => ApiResponseErrorData | PromiseLike; + }) + | (Omit & { + status: number; + json: () => never; + }); + export class ApiClient { host: string; @@ -5,8 +46,34 @@ export class ApiClient { this.host = host; } - async postRequest(path: string, requestBody: object): Promise { + async responseHandler(response: Response) { + const res = response as ApiServerResponse; + if (res.status === 200) { + return { status: "success", code: 200, data: { ok: true } } as ApiResponseSuccess; + } else if (res.status === 422) { + const data = await res.json(); + return { + status: "client_error", + code: 422, + data, + } as ApiResponseClientError; + } else if (res.status === 500) { + const data = await res.json(); + return { + status: "server_error", + code: 500, + data, + } as ApiResponseServerError; + } + throw new Error(`Unexpected response status: ${res.status}`); + } + + async postRequest( + path: string, + requestBody: object, + ): Promise | ApiResponse> { const host = process.env.NODE_ENV === "test" ? "http://testhost" : ""; + const response = await fetch(host + "/v1" + path, { method: "POST", body: JSON.stringify(requestBody), @@ -15,6 +82,6 @@ export class ApiClient { }, }); - return response; + return this.responseHandler(response); } } diff --git a/frontend/lib/api/useApiRequest.test.ts b/frontend/lib/api/useApiRequest.test.ts deleted file mode 100644 index c06fe3e9a..000000000 --- a/frontend/lib/api/useApiRequest.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { renderHook } from "app/test/unit/test-utils"; -import { useApiRequest } from "./useApiRequest"; -import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; - -const responseHandler = async (response: Response) => { - const res = response as POLLING_STATION_DATA_ENTRY_RESPONSE; - if (res.status === 200) { - return { status: "20x", code: 200, message: "OK" } as ApiResponseSuccess; - } else if (res.status === 422) { - return { - status: "40x", - code: 422, - message: "Unprocessable Entity", - } as ApiResponseClientError; - } else if (res.status === 500) { - const data = await res.json(); - return { - status: "50x", - code: 500, - message: "Internal Server Error", - data, - } as ApiResponseServerError; - } - throw new Error(`Unexpected response status: ${res.status}`); -}; - -describe("useApiRequest", () => { - test("it renders", async () => { - const { result, waitFor } = renderHook(() => - useApiRequest( - "/ping", - responseHandler, - ), - ); - }); -}); diff --git a/frontend/lib/api/useApiRequest.ts b/frontend/lib/api/useApiRequest.ts index 880e5ea61..a9261960a 100644 --- a/frontend/lib/api/useApiRequest.ts +++ b/frontend/lib/api/useApiRequest.ts @@ -1,44 +1,39 @@ import * as React from "react"; import { useApi } from "./useApi"; -import { ApiResponse, ApiResponseSuccess } from "./api"; +import { ApiResponseErrorData } from "./ApiClient"; -export type UseApiRequestReturn = [ +export type UseApiRequestReturn = [ (requestBody: REQUEST_BODY) => void, { loading: boolean; - error: ERROR | null; + error: ApiResponseErrorData | null; data: DATA | null; }, ]; -export interface UseApiRequestParams { +export interface UseApiRequestParams { path: string; - responseHandler: (response: Response) => Promise; } -export function useApiRequest< - REQUEST_BODY, - DATA extends ApiResponseSuccess, - ERROR extends ApiResponse, ->({ +export function useApiRequest({ path, - responseHandler, -}: UseApiRequestParams): UseApiRequestReturn { +}: UseApiRequestParams): UseApiRequestReturn { const { client } = useApi(); const [data, setData] = React.useState(null); - const [error, setError] = React.useState(null); + const [error, setError] = React.useState(null); const [apiRequest, setApiRequest] = React.useState(null); React.useEffect(() => { + let isSubscribed = true; const doRequest = async (b: REQUEST_BODY) => { - const response = await client.postRequest(path, b as object); - - const result = await responseHandler(response); - if (result.status === "20x") { - setData(result as DATA); - } else { - setError(result as ERROR); + const response = await client.postRequest(path, b as object); + if (isSubscribed) { + if (response.status === "success") { + setData(response.data as DATA); + } else { + setError(response.data as ApiResponseErrorData); + } } }; @@ -47,13 +42,20 @@ export function useApiRequest< console.error(e); }); } - }, [apiRequest, client, path, responseHandler]); + + return () => { + isSubscribed = false; + }; + }, [apiRequest, client, path]); const makeRequest = React.useCallback((requestBody: REQUEST_BODY) => { setApiRequest(requestBody); }, []); - const loading = false; + const loading = React.useMemo( + () => apiRequest !== null && data === null && error === null, + [apiRequest, data, error], + ); return [ makeRequest, diff --git a/frontend/lib/api/usePollingStationDataEntry.temp.ts b/frontend/lib/api/usePollingStationDataEntry.temp.ts deleted file mode 100644 index 8f1773659..000000000 --- a/frontend/lib/api/usePollingStationDataEntry.temp.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { renderHook } from "app/test/unit/test-utils"; -import { useApiRequest } from "./useApiRequest"; -import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; - - - - -describe("useApiRequest", () => { - - test("it renders", async () => { - const { result, waitFor } = renderHook(() => useApiRequest()); - - }); -}); diff --git a/frontend/lib/api/usePollingStationDataEntry.test.ts b/frontend/lib/api/usePollingStationDataEntry.test.ts new file mode 100644 index 000000000..3a28fdc22 --- /dev/null +++ b/frontend/lib/api/usePollingStationDataEntry.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test, assert } from "vitest"; +import { renderHook, waitFor, Providers } from "app/test/unit"; +import { usePollingStationDataEntry } from "./usePollingStationDataEntry"; +import { overrideOnce } from "app/test/unit"; + +describe("useApiRequest", () => { + test("it renders", async () => { + overrideOnce("post", "/v1/api/polling_stations/:id/data_entries/:entry_number", 200, { + ok: true, + }); + + const { result } = renderHook( + () => + usePollingStationDataEntry({ + id: 1, + entry_number: 1, + }), + { wrapper: Providers }, + ); + + const [doSubmit] = result.current; + doSubmit({ + data: { + voters_counts: { + poll_card_count: 1, + proxy_certificate_count: 2, + voter_card_count: 3, + total_admitted_voters_count: 4, + }, + votes_counts: { + votes_candidates_counts: 5, + blank_votes_count: 6, + invalid_votes_count: 7, + total_votes_cast_count: 8, + }, + }, + }); + + await waitFor(() => { + const [, { data }] = result.current; + expect(data).not.toBe(null); + }); + + const [, { data }] = result.current; + assert(data !== null, "data is not null"); + expect(data.ok).toBe(true); + }); +}); diff --git a/frontend/lib/api/usePollingStationDataEntry.ts b/frontend/lib/api/usePollingStationDataEntry.ts index 76f5198b8..0287ef397 100644 --- a/frontend/lib/api/usePollingStationDataEntry.ts +++ b/frontend/lib/api/usePollingStationDataEntry.ts @@ -1,83 +1,23 @@ import * as React from "react"; import { useApiRequest } from "./useApiRequest"; import { - DataEntryError, POLLING_STATION_DATA_ENTRY_REQUEST_BODY, POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS, POLLING_STATION_DATA_ENTRY_REQUEST_PATH, - POLLING_STATION_DATA_ENTRY_RESPONSE_BODY, } from "./gen/openapi"; -import { ApiResponseClientError, ApiResponseSuccess, ApiResponseServerError } from "./api"; -//TODO: add to generate script -//TODO: Make camelcase -export type POLLING_STATION_DATA_ENTRY_RESPONSE = - | (Omit & { - status: 200; - json: () => ApiResponseSuccess | PromiseLike; - }) - | (Omit & { - status: 422; - json: () => ApiResponseClientError | PromiseLike; - }) - | (Omit & { - status: 500; - json: () => - | ApiResponseServerError - | PromiseLike>; - }) - | (Omit & { - status: number; - json: () => never; - }); - - - - -export const error:object = { - status: 500, - code: "DATABASE_CONNECTION_ERROR", - message: "Could not connect to database", +//TEMP +type ResponseData = { + ok: boolean; }; - - - - export function usePollingStationDataEntry(params: POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS) { const path = React.useMemo(() => { const result: POLLING_STATION_DATA_ENTRY_REQUEST_PATH = `/api/polling_stations/${params.id}/data_entries/${params.entry_number}`; return result; }, [params]); - const responseHandler = React.useCallback(async (response: Response) => { - const res = response as POLLING_STATION_DATA_ENTRY_RESPONSE; - if (res.status === 200) { - return { status: "20x", code: 200, message: "OK" } as ApiResponseSuccess; - } else if (res.status === 422) { - return { - status: "40x", - code: 422, - message: "Unprocessable Entity", - } as ApiResponseClientError; - } else if (res.status === 500) { - const data = await res.json(); - return { - status: "50x", - code: 500, - message: "Internal Server Error", - data, - } as ApiResponseServerError; - } - throw new Error(`Unexpected response status: ${res.status}`); - }, []); - - return useApiRequest< - POLLING_STATION_DATA_ENTRY_REQUEST_BODY, - ApiResponseSuccess, - ApiResponseServerError | ApiResponseClientError - >({ + return useApiRequest({ path, - responseHandler, }); } From b5f6b74f8bd48076c851d8a1560958aab73cfcf8 Mon Sep 17 00:00:00 2001 From: Les Date: Wed, 15 May 2024 15:30:08 +0200 Subject: [PATCH 8/9] remove mockServiceWorker from repository --- frontend/mockServiceWorker.js | 284 ---------------------------------- 1 file changed, 284 deletions(-) delete mode 100644 frontend/mockServiceWorker.js diff --git a/frontend/mockServiceWorker.js b/frontend/mockServiceWorker.js deleted file mode 100644 index 481e5dcaa..000000000 --- a/frontend/mockServiceWorker.js +++ /dev/null @@ -1,284 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ - -/** - * Mock Service Worker. - * @see https://github.com/mswjs/msw - * - Please do NOT modify this file. - * - Please do NOT serve this file on production. - */ - -const PACKAGE_VERSION = "2.2.13"; -const INTEGRITY_CHECKSUM = "26357c79639bfa20d64c0efca2a87423"; -const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); -const activeClientIds = new Set(); - -self.addEventListener("install", function () { - self.skipWaiting(); -}); - -self.addEventListener("activate", function (event) { - event.waitUntil(self.clients.claim()); -}); - -self.addEventListener("message", async function (event) { - const clientId = event.source.id; - - if (!clientId || !self.clients) { - return; - } - - const client = await self.clients.get(clientId); - - if (!client) { - return; - } - - const allClients = await self.clients.matchAll({ - type: "window", - }); - - switch (event.data) { - case "KEEPALIVE_REQUEST": { - sendToClient(client, { - type: "KEEPALIVE_RESPONSE", - }); - break; - } - - case "INTEGRITY_CHECK_REQUEST": { - sendToClient(client, { - type: "INTEGRITY_CHECK_RESPONSE", - payload: { - packageVersion: PACKAGE_VERSION, - checksum: INTEGRITY_CHECKSUM, - }, - }); - break; - } - - case "MOCK_ACTIVATE": { - activeClientIds.add(clientId); - - sendToClient(client, { - type: "MOCKING_ENABLED", - payload: true, - }); - break; - } - - case "MOCK_DEACTIVATE": { - activeClientIds.delete(clientId); - break; - } - - case "CLIENT_CLOSED": { - activeClientIds.delete(clientId); - - const remainingClients = allClients.filter((client) => { - return client.id !== clientId; - }); - - // Unregister itself when there are no more clients - if (remainingClients.length === 0) { - self.registration.unregister(); - } - - break; - } - } -}); - -self.addEventListener("fetch", function (event) { - const { request } = event; - - // Bypass navigation requests. - if (request.mode === "navigate") { - return; - } - - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if (request.cache === "only-if-cached" && request.mode !== "same-origin") { - return; - } - - // Bypass all requests when there are no active clients. - // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). - if (activeClientIds.size === 0) { - return; - } - - // Generate unique request ID. - const requestId = crypto.randomUUID(); - event.respondWith(handleRequest(event, requestId)); -}); - -async function handleRequest(event, requestId) { - const client = await resolveMainClient(event); - const response = await getResponse(event, client, requestId); - - // Send back the response clone for the "response:*" life-cycle events. - // Ensure MSW is active and ready to handle the message, otherwise - // this message will pend indefinitely. - if (client && activeClientIds.has(client.id)) { - (async function () { - const responseClone = response.clone(); - - sendToClient( - client, - { - type: "RESPONSE", - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - body: responseClone.body, - headers: Object.fromEntries(responseClone.headers.entries()), - }, - }, - [responseClone.body], - ); - })(); - } - - return response; -} - -// Resolve the main client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. -async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId); - - if (client?.frameType === "top-level") { - return client; - } - - const allClients = await self.clients.matchAll({ - type: "window", - }); - - return allClients - .filter((client) => { - // Get only those clients that are currently visible. - return client.visibilityState === "visible"; - }) - .find((client) => { - // Find the client ID that's recorded in the - // set of clients that have registered the worker. - return activeClientIds.has(client.id); - }); -} - -async function getResponse(event, client, requestId) { - const { request } = event; - - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const requestClone = request.clone(); - - function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()); - - // Remove internal MSW request header so the passthrough request - // complies with any potential CORS preflight checks on the server. - // Some servers forbid unknown request headers. - delete headers["x-msw-intention"]; - - return fetch(requestClone, { headers }); - } - - // Bypass mocking when the client is not active. - if (!client) { - return passthrough(); - } - - // Bypass initial page load requests (i.e. static assets). - // The absence of the immediate/parent client in the map of the active clients - // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet - // and is not ready to handle requests. - if (!activeClientIds.has(client.id)) { - return passthrough(); - } - - // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer(); - const clientMessage = await sendToClient( - client, - { - type: "REQUEST", - payload: { - id: requestId, - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: requestBuffer, - keepalive: request.keepalive, - }, - }, - [requestBuffer], - ); - - switch (clientMessage.type) { - case "MOCK_RESPONSE": { - return respondWithMock(clientMessage.data); - } - - case "PASSTHROUGH": { - return passthrough(); - } - } - - return passthrough(); -} - -function sendToClient(client, message, transferrables = []) { - return new Promise((resolve, reject) => { - const channel = new MessageChannel(); - - channel.port1.onmessage = (event) => { - if (event.data && event.data.error) { - return reject(event.data.error); - } - - resolve(event.data); - }; - - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ); - }); -} - -async function respondWithMock(response) { - // Setting response status code to 0 is a no-op. - // However, when responding with a "Response.error()", the produced Response - // instance will have status code set to 0. Since it's not possible to create - // a Response instance with status code 0, handle that use-case separately. - if (response.status === 0) { - return Response.error(); - } - - const mockedResponse = new Response(response.body, response); - - Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { - value: true, - enumerable: true, - }); - - return mockedResponse; -} From fdd55106b2784e44d7a6cb932b7eef367b400e12 Mon Sep 17 00:00:00 2001 From: Les Date: Wed, 15 May 2024 15:32:30 +0200 Subject: [PATCH 9/9] remove mock code --- backend/src/polling_station/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/polling_station/mod.rs b/backend/src/polling_station/mod.rs index 954ffd49f..2040fcd55 100644 --- a/backend/src/polling_station/mod.rs +++ b/backend/src/polling_station/mod.rs @@ -15,13 +15,6 @@ pub struct DataEntryRequest { pub struct DataEntryError { pub message: String, } -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq, Eq, Hash)] -pub struct ApiResponse { - pub status: u32, - pub code: String, //should be an enum - pub messsage: String, - pub data: Data, -} /// PollingStationResults, following the fields in /// "Model N 10-1. Proces-verbaal van een stembureau"