Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend initial commit #1

Merged
merged 11 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Kiesraad-frontend

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

env:
API_MODE: msw

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- name: Use node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install
run: npm install
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: "e2e test install"
run: npx playwright install --with-deps
- name: "e2e test"
run: npm run e2e
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
6 changes: 6 additions & 0 deletions frontend/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }]
]
}
21 changes: 21 additions & 0 deletions frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:playwright/recommended",
"prettier"
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh", "jsx-a11y", "prettier", "@typescript-eslint"],
rules: {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }]
},
parserOptions: {
project: "./tsconfig.json"
}
};
25 changes: 25 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
test-results
3 changes: 3 additions & 0 deletions frontend/.ladle/config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
stories: "lib/ui/**/*.stories.tsx"
};
1 change: 1 addition & 0 deletions frontend/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.11.1
2 changes: 2 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Electoral Council election results management system - Frontend

4 changes: 4 additions & 0 deletions frontend/app/app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { expect, test } from "vitest";
test("it is an app", () => {
expect(true).toBe(true);
});
9 changes: 9 additions & 0 deletions frontend/app/layout/RootLayout/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Outlet } from "react-router-dom";

export function RootLayout() {
return (
<div>
<Outlet />
</div>
);
}
1 change: 1 addition & 0 deletions frontend/app/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./RootLayout/RootLayout";
32 changes: 32 additions & 0 deletions frontend/app/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import ReactDOM from "react-dom/client";
import { StrictMode } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

// ignore in prod
import { startMockAPI } from "./msw-mock-api.ts";
import { routes } from "./routes.tsx";

const rootDiv = document.getElementById("root");
if (!rootDiv) throw new Error("Root div not found");

const root = ReactDOM.createRoot(rootDiv);

function render() {
const router = createBrowserRouter(routes, {
future: {
v7_normalizeFormMethod: true
}
});

root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);
}

if (process.env.MSW) {
startMockAPI().then(render).catch(console.error);
} else {
render();
}
10 changes: 10 additions & 0 deletions frontend/app/module/global/page/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Button} from "@kiesraad/ui";

export function HomePage() {
return (
<div>
<h1>Home</h1>
<Button>Click</Button>
</div>
)
}
43 changes: 43 additions & 0 deletions frontend/app/msw-mock-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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
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"),
path.startsWith("/node_modules"),
path.startsWith("/js")
].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}

If you want to intercept this unhandled request, create a request handler for it.`);
}
}
});
}
11 changes: 11 additions & 0 deletions frontend/app/routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createRoutesFromElements, Route } from "react-router-dom";
import { RootLayout } from "./layout";
import { HomePage } from "./module/global/page/HomePage";


export const routes = createRoutesFromElements(
<Route element={<RootLayout />}>
<Route path="*" element={<div>Not found</div>} />
<Route index path="/" element={<HomePage />} />
</Route>
);
6 changes: 6 additions & 0 deletions frontend/app/test/e2e/smoketest.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { test, expect } from "@playwright/test";

test("smoke test", async ({ page }) => {
await page.goto("/");
expect(true).toBe(true);
});
37 changes: 37 additions & 0 deletions frontend/app/test/unit/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";

import { handlers } from "@kiesraad/api-mocks";

export const server = setupServer(
...handlers.map((h) => {
// Node's Fetch implementation does not accept URLs with a protocol and host
h.info.path = "http://testhost" + h.info.path;
return h;
})
);

// Override request handlers in order to test special cases
export function overrideOnce(
method: keyof typeof http,
path: string,
status: number,
body: string | Record<string, unknown>
) {
server.use(
http[method](
path,
() =>
// https://mswjs.io/docs/api/response/once
typeof body === "string" ? new HttpResponse(body, { status }) : HttpResponse.json(body, { status }),
{ once: true }
)
);
}
15 changes: 15 additions & 0 deletions frontend/app/test/unit/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import "@testing-library/jest-dom/vitest";

import { cleanup } from "@testing-library/react";
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./server";

beforeAll(() => {
server.listen();
});
afterEach(() => {
cleanup();
});
afterAll(() => {
server.restoreHandlers();
});
13 changes: 13 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TBD</title>

<meta viewport="width=device-width, initial-scale=1" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./app/main.tsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions frontend/lib/api-mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { http, type HttpHandler, HttpResponse } from "msw";

type PingParams = Record<string, never>;
type PingRequestBody = {
ping: string;
};
type PingResponseBody = {
pong: string;
};

const pingHandler = http.post<PingParams, PingRequestBody, PingResponseBody>(
"/v1/ping",
async ({ request }) => {
const data = await request.json();

const pong = data.ping || "pong";

return HttpResponse.json({
pong
});
}
);


export const handlers: HttpHandler[] = [pingHandler];
20 changes: 20 additions & 0 deletions frontend/lib/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from "vitest";

type Response = {
pong: string;
};

describe("Mock api works", () => {
test("echos a value", async () => {
const resp = await fetch("http://testhost/v1/ping", {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({ ping: "test" })
});
const result = (await resp.json()) as unknown as Response;

expect(result.pong).toBe("test");
});
});
27 changes: 27 additions & 0 deletions frontend/lib/ui/Button/Button.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.button-default {
color: #fff;
border-radius: 8px;
border: 1px solid #202939;
background: #202939;
/* Shadow/xs */
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);

display: flex;
width: 208px;
padding: 10px 16px;
justify-content: center;
align-items: center;
gap: 12px;


&:focus {
outline: none;
box-shadow: 0px 0px 4px 4px rgba(66, 210, 243, 0.4);
}

&:disabled {
background: #f2f2f2;
border: 1px solid #f2f2f2;
color: #b3b3b3;
}
}
5 changes: 5 additions & 0 deletions frontend/lib/ui/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Story } from "@ladle/react";

import { Button } from "./Button";

export const DefaultButton: Story = () => <Button>Click me</Button>;
8 changes: 8 additions & 0 deletions frontend/lib/ui/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { render } from "@testing-library/react";
import { Button } from "./Button";
import { expect, test } from "vitest";

test("The button works", () => {
render(<Button>Click me</Button>);
expect(true).toBe(true);
});
Loading