diff --git a/.dockerignore b/.dockerignore index 8567a5b..62669bc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,6 @@ node_modules /.svelte-kit /package -/playwright-report .env .env.* .eslintcache diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index c286741..a7d881e 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -1,10 +1,6 @@ name: Visitenbuch CI on: push: - branches: - - "main" - tags: - - "v*" pull_request: jobs: @@ -24,6 +20,7 @@ jobs: CLIENT_REDIRECT_URIS: http://localhost:4173/auth/callback/keycloak CLIENT_LOGOUT_REDIRECT_URIS: http://localhost:4173/login?noAuto=1 ISSUER_HOST: oidc:3000 + env: DATABASE_URL: "postgresql://postgres:1234@postgres:5432/test?schema=public" TEST_DATABASE_URL: "postgresql://postgres:1234@postgres:5432/test?schema=public" @@ -32,32 +29,28 @@ jobs: steps: - name: πŸ‘οΈ Checkout repository uses: actions/checkout@v4 - - name: πŸ“¦ pnpm install + - name: Test run: | - pnpm install - cp .env.test .env - - name: 🧐 lint - run: | - pnpm run check - pnpm run lint - - name: πŸ§ͺ Unit test - run: pnpm run test:unit - - name: πŸ§ͺ Integration test - run: | - npx prisma migrate reset --force - pnpm run test:integration - - name: πŸ‘¨β€πŸ”¬ E2E test - id: e2etest - run: | - pnpm run build -l silent - npx playwright install chromium - pnpm run test:e2e - - name: πŸ’’ Upload E2E report - if: ${{ failure() && steps.e2etest.conclusion == 'failure' }} - uses: https://code.forgejo.org/forgejo/upload-artifact@v4 - with: - name: playwright-report - path: playwright-report + curl http://oidc:3000/.well-known/openid-configuration || sleep 1000 + # - name: πŸ“¦ pnpm install + # run: | + # pnpm install + # cp .env.test .env + # - name: 🧐 lint + # run: | + # npm run check + # npm run lint + # - name: πŸ§ͺ Unit test + # run: npm run test:unit + # - name: πŸ§ͺ Integration test + # run: | + # npx prisma migrate reset --force + # npm run test:integration + # - name: πŸ‘¨β€πŸ”¬ E2E test + # run: | + # npx playwright install --with-deps + # npm run build + # npm run test:e2e release: runs-on: cimaster-latest @@ -68,12 +61,12 @@ jobs: - name: πŸ‘οΈ Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # important to fetch tag logs + fetch-depth: 0 - name: πŸ“¦ pnpm install run: pnpm install - name: βš’οΈ Build web application - run: pnpm run build + run: npm run build - name: πŸ‹ Build docker image uses: https://code.thetadev.de/ThetaDev/action-kaniko@v1 with: diff --git a/.gitignore b/.gitignore index 2b7e811..ffc5dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules /build /.svelte-kit /package -/playwright-report .env .eslintcache vite.config.js.timestamp-* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d1751c..24bca84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,5 +13,4 @@ repos: entry: npx eslint args: - "--max-warnings=0" - - "--cache" files: \.(js|ts|svelte)$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0540fe4..879e1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,61 +3,6 @@ All notable changes to this project will be documented in this file. -## [v0.3.5](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.4..v0.3.5) - 2024-05-20 - -### πŸš€ Features - -- Focus filter bar when pressing F - ([ad796dc](https://code.thetadev.de/HSA/Visitenbuch/commit/ad796dcb578b79b566559d1c22c99f0231a03251)) -- Add optional Keycloak endpoint config - ([d746e47](https://code.thetadev.de/HSA/Visitenbuch/commit/d746e4787d70080bbc22b37263bbfa695c1a7d72)) - -### πŸ› Bug Fixes - -- Dont create entry executions if entry is only postponed - ([34e54fa](https://code.thetadev.de/HSA/Visitenbuch/commit/34e54fa4afdf17e7258cbc5ccac5a3d094ee161b)) -- HumanDate capitalization - ([f4f03ab](https://code.thetadev.de/HSA/Visitenbuch/commit/f4f03ab4914f850b15acf7bb39da34b1abb587a7)) -- Filterbar does not exclude present filters from URL, text filters dont confirm when defocused - ([9ed5f15](https://code.thetadev.de/HSA/Visitenbuch/commit/9ed5f15b9ef237cc400b069928baeb920b2d3681)) -- [**breaking**] Ensure category, room and station names are unique - ([98c62ac](https://code.thetadev.de/HSA/Visitenbuch/commit/98c62ac4603fa6d7c97e1a439f613379db7a2587)) -- Allow multiple date filters - ([2a4bda7](https://code.thetadev.de/HSA/Visitenbuch/commit/2a4bda70c6cfd85b4a32989a2e19ba718cc7717e)) -- Add dumb-init to docker image - ([47f0a08](https://code.thetadev.de/HSA/Visitenbuch/commit/47f0a08ea3dbd8b1721a11c52b36c42ad56e8e29)) -- Remove test route - ([03f6c58](https://code.thetadev.de/HSA/Visitenbuch/commit/03f6c5848201eff02999c6b9323ec1515d68fd5a)) - -### βš™οΈ Miscellaneous Tasks - -- Remove unused zod-form-data dependency - ([882ae66](https://code.thetadev.de/HSA/Visitenbuch/commit/882ae66a6a137259388525df2c91b8e1ed924d86)) - - -## [v0.3.4](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.3..v0.3.4) - 2024-05-16 - -### πŸš€ Features - -- Select table entries on doubleclick - ([c6abf63](https://code.thetadev.de/HSA/Visitenbuch/commit/c6abf633f8ae5e9b562dda36f9f7ab4d6adcb4e1)) - -### πŸ› Bug Fixes - -- Escape HTML for licenses file - ([f76e7fd](https://code.thetadev.de/HSA/Visitenbuch/commit/f76e7fd97f62d9b41ecbabc3334c2c1876be253d)) -- Use btn-id class for all tables - ([d5e9a94](https://code.thetadev.de/HSA/Visitenbuch/commit/d5e9a9469f0c57939367141985a97d8404fd6fbe)) -- Avoid global state, use context for savedFilters - ([a4eebb9](https://code.thetadev.de/HSA/Visitenbuch/commit/a4eebb944f55da8e87cc899eebada0bd3fd37aa8)) -- Close autocomplete on defocus - ([4a3155c](https://code.thetadev.de/HSA/Visitenbuch/commit/4a3155c33aa354973d4e0ca3ffeab2b7fd442040)) -- Remove process.on hooks (not necessary) - ([cdb3446](https://code.thetadev.de/HSA/Visitenbuch/commit/cdb344609cde80084876faea9f80e7b26b01d0f2)) -- Autocomplete not closing on tab - ([88a5040](https://code.thetadev.de/HSA/Visitenbuch/commit/88a5040f9c4e19ae3efb5ad0894c8dc5b905a92e)) - - -## [v0.3.3](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.2..v0.3.3) - 2024-05-14 - -### πŸš€ Features - -- Add E2E testing - ([8d9b75c](https://code.thetadev.de/HSA/Visitenbuch/commit/8d9b75c5fd634ae547c2690a68957264a6d447e4)) -- Make page printable - ([04d9883](https://code.thetadev.de/HSA/Visitenbuch/commit/04d9883c9655379301e0c41cc55ebdaa90c68821)) - -### πŸ› Bug Fixes - -- Update ESLint config and fix lints - ([009729b](https://code.thetadev.de/HSA/Visitenbuch/commit/009729b877e4f050fa0d1159aaa86dd43d534621)) -- FilterList selection hides items from other FilterLists - ([cc1ebaf](https://code.thetadev.de/HSA/Visitenbuch/commit/cc1ebaff1a4573970f04dc44591ee7e9afb9a842)) - -### πŸ§ͺ Testing - -- Use fixtures for E2E tests, fix wrong OIDC URL in CI - ([cbc7d65](https://code.thetadev.de/HSA/Visitenbuch/commit/cbc7d65103695565db64b4770cce71f5d37920b6)) - - ## [v0.3.2](https://code.thetadev.de/HSA/Visitenbuch/compare/v0.3.1..v0.3.2) - 2024-05-13 ### πŸ› Bug Fixes diff --git a/Dockerfile b/Dockerfile index f0631df..6adb62d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,7 @@ COPY package.json pnpm-lock.yaml run/entrypoint.sh ./ COPY prisma ./prisma # Setup pnpm, install Prisma CLI (for generating client) and install dependencies -RUN apk add dumb-init && \ - npm config set update-notifier false && \ +RUN npm config set update-notifier false && \ corepack enable && \ pnpm i --prod && \ pnpm audit fix && \ diff --git a/README.md b/README.md index cc42fac..1f1a26e 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,21 @@ for the university hospital in Augsburg ## Development -The project template was created using -[`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). +The project template was created using [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). -Once you've created a project and installed dependencies with `npm install` (or -`pnpm install` or `yarn`), start a development server: +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: ```bash -pnpm run dev +npm run dev # or start the server and open the app in a new browser tab -pnpm run dev -- --open +npm run dev -- --open ``` -### Test environment - -Copy the `.env.example` file to `.env` to get access to the required configuration -variables. - -The project depends on a PostgreSQL database and an OIDC authentication server. You can -setup both using the `run/db_up.sh` script. This creates a new testing environment using -docker-compose and fills the test database with mock data. - -### Use the Pisma ORM - -If you apply changes to the database scheme, you have to create a new migration to apply -these changes to the database. +### Handle the prisma ORM ```bash +./run/db_up.sh # Start the docker container, create a new database and run migrations + insert test data npx prisma migrate dev --name my_migration --create-only # Create a new migration npx prisma migrate dev # Apply migrations to the database ``` @@ -41,20 +28,12 @@ npx prisma migrate dev # Apply migrations to the database To create a production version of your app: ```bash -pnpm run build +npm run build ``` You can preview the production build with `npm run preview`. -> To deploy your app, you may need to install an -> [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. - -### Test - -```bash -pnpm test # Unit- und Integrationstests -pnpm test:e2e # End2End-Tests -``` +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. ### Release @@ -63,10 +42,3 @@ To release a new version, tun the release script with the new version as a param ```bash ./release.sh 1.0.0 ``` - -### Building docker image - -```bash -pnpm run build -docker build -t thetadev256/visitenbuch . -``` diff --git a/package.json b/package.json index 1bde82a..61fcd29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "visitenbuch", - "version": "0.3.5", + "version": "0.3.2", "private": true, "license": "AGPL-3.0", "scripts": { @@ -28,7 +28,8 @@ "qs": "^6.12.1", "set-cookie-parser": "^2.6.0", "svelte-floating-ui": "^1.5.8", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-form-data": "^2.0.2" }, "devDependencies": { "@faker-js/faker": "^8.4.1", diff --git a/playwright.config.js b/playwright.config.js index ed4ee46..afa7d21 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,21 +1,13 @@ import { defineConfig } from "@playwright/test"; -// eslint-disable-next-line no-undef -const CI = Boolean(process.env.CI); - export default defineConfig({ webServer: { command: "npm run preview -m test", port: 4173, - reuseExistingServer: !CI, + reuseExistingServer: true, }, testDir: "tests/e2e", + testMatch: /\.[jt]s$/, globalSetup: "tests/helpers/generate-mockdata.ts", outputDir: ".svelte-kit/test-results", - maxFailures: 0, - retries: CI ? 2 : 0, - reporter: CI ? [["line"], ["html"]] : "list", - use: { - trace: "on-first-retry", - }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c90e8bb..8e977f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: zod: specifier: ^3.23.8 version: 3.23.8 + zod-form-data: + specifier: ^2.0.2 + version: 2.0.2(zod@3.23.8) devDependencies: '@faker-js/faker': @@ -5958,6 +5961,14 @@ packages: dev: true optional: true + /zod-form-data@2.0.2(zod@3.23.8): + resolution: {integrity: sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==} + peerDependencies: + zod: '>= 3.11.0' + dependencies: + zod: 3.23.8 + dev: false + /zod-to-json-schema@3.23.0(zod@3.23.8): resolution: {integrity: sha512-az0uJ243PxsRIa2x1WmNE/pnuA05gUq/JB8Lwe1EDCCL/Fz9MgjYQ0fPlyc2Tcv6aF2ZA7WM5TWaRZVEFaAIag==} requiresBuild: true diff --git a/prisma/migrations/20240518143538_unique_cat_room_station/migration.sql b/prisma/migrations/20240518143538_unique_cat_room_station/migration.sql deleted file mode 100644 index 7686fa8..0000000 --- a/prisma/migrations/20240518143538_unique_cat_room_station/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[name]` on the table `categories` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[name]` on the table `rooms` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[name]` on the table `stations` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "categories_name_key" ON "categories"("name"); - --- CreateIndex -CREATE UNIQUE INDEX "rooms_name_key" ON "rooms"("name"); - --- CreateIndex -CREATE UNIQUE INDEX "stations_name_key" ON "stations"("name"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bfc2700..633705d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -50,7 +50,7 @@ model User { // Hospital station model Station { id Int @id @default(autoincrement()) - name String @unique + name String Room Room[] hidden Boolean @default(false) @@ -60,7 +60,7 @@ model Station { // Hospital room model Room { id Int @id @default(autoincrement()) - name String @unique + name String station Station @relation(fields: [station_id], references: [id], onDelete: Restrict) station_id Int Patient Patient[] @@ -90,7 +90,7 @@ model Patient { // Entry category (e.g. Blood test, Exams, ...) model Category { id Int @id @default(autoincrement()) - name String @unique + name String color String? description String? EntryVersion EntryVersion[] diff --git a/run/entrypoint.sh b/run/entrypoint.sh index f7ebe95..c749cb0 100755 --- a/run/entrypoint.sh +++ b/run/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/dumb-init /bin/sh +#!/bin/sh set -e # Migrate database before starting server npx prisma migrate deploy diff --git a/src/app.html b/src/app.html index 50038b5..5f0dd88 100644 --- a/src/app.html +++ b/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 87c81a0..a7fddf5 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -31,3 +31,7 @@ export const handle = sequence( authorization, createTRPCHandle({ router, createContext }), ); + +// Allow server application to exit +process.on("SIGINT", () => process.exit()); // Ctrl+C +process.on("SIGTERM", () => process.exit()); // docker stop diff --git a/src/lib/components/filter/Autocomplete.svelte b/src/lib/components/filter/Autocomplete.svelte index 7a20f6d..f1a7885 100644 --- a/src/lib/components/filter/Autocomplete.svelte +++ b/src/lib/components/filter/Autocomplete.svelte @@ -166,14 +166,6 @@ if (opened) { onClose(kb); } - // select remaining item if autoselect is enabled - if (!selection) { - if (!noAutoselect1 && filteredItems.length === 1) { - selectListItem(filteredItems[0], true); - } else { - setInputValue(""); - } - } opened = false; } @@ -193,38 +185,43 @@ } function onKeyDown(e: KeyboardEvent): void { - switch (e.key) { - case "Tab": - close(); - break; - case "ArrowDown": + let { key } = e; + if (key === "Tab" && e.shiftKey) key = "ShiftTab"; + const fnmap: Record void> = { + Tab: () => close, + ShiftTab: () => close, + ArrowDown: () => { open(); if (highlightIndex < filteredItems.length - 1) { highlightIndex++; highlight(); } - break; - case "ArrowUp": + }, + ArrowUp: () => { open(); if (highlightIndex > 0) { highlightIndex--; highlight(); } - break; - case "Escape": + }, + Escape: () => { e.stopPropagation(); if (opened) { if (inputElm) inputElm.focus(); close(); } - break; - case "Backspace": + }, + Backspace: () => { if (inputValue().length === 0) { onBackspace(); } else if (selection) { clearSelection(); } - break; + }, + }; + const fn = fnmap[key]; + if (typeof fn === "function") { + fn(); } } @@ -237,6 +234,16 @@ } } + function onBlur(): void { + if (!selection) { + if (!noAutoselect1 && filteredItems.length === 1) { + selectListItem(filteredItems[0], true); + } else { + setInputValue(""); + } + } + } + function highlight(): void { if (browser && opened) { window.setTimeout(() => { @@ -296,11 +303,12 @@ on:focus={open} on:keydown={onKeyDown} on:keypress={onKeyPress} + on:blur={onBlur} use:floatingRef /> {#if opened && filteredItems.length > 0} -
+
{#each filteredItems as item, i}
- +
{/if}
diff --git a/src/lib/components/filter/FilterBar.svelte b/src/lib/components/filter/FilterBar.svelte index 2791bb2..8a68b01 100644 --- a/src/lib/components/filter/FilterBar.svelte +++ b/src/lib/components/filter/FilterBar.svelte @@ -1,7 +1,6 @@ - -
{#each activeFilters as fdata, i} diff --git a/src/lib/components/filter/FilterChip.svelte b/src/lib/components/filter/FilterChip.svelte index 4d869f2..260015c 100644 --- a/src/lib/components/filter/FilterChip.svelte +++ b/src/lib/components/filter/FilterChip.svelte @@ -41,15 +41,6 @@ } } : undefined; - function acceptTextInput(e: Event): void { - // @ts-expect-error Event is from HTML input - if (e.target?.value) { - // @ts-expect-error Input value is checked - fdata.selection = { id: null, name: e.target.value }; - } - stopEditing(true); - } - $: if (fdata.editing && autocomplete) { autocomplete.open(); } @@ -127,10 +118,14 @@ }} on:keypress={(e) => { if (e.key === "Enter") { - acceptTextInput(e); + // @ts-expect-error Input value is checked + if (e.target?.value) { + // @ts-expect-error Input value is checked + fdata.selection = { id: null, name: e.target.value }; + } + stopEditing(true); } }} - on:blur={acceptTextInput} /> {/if} {:else} diff --git a/src/lib/components/filter/SavedFilters.svelte b/src/lib/components/filter/SavedFilters.svelte index 20baa2c..5854b2b 100644 --- a/src/lib/components/filter/SavedFilters.svelte +++ b/src/lib/components/filter/SavedFilters.svelte @@ -8,14 +8,12 @@ import { toastError, toastInfo } from "$lib/shared/util/toast"; import Icon from "$lib/components/ui/Icon.svelte"; - import { getSavedFilters } from "$lib/stores"; + import { savedFilters } from "$lib/stores"; import Chip from "./SavedFilterChip.svelte"; export let view: string; - const savedFilters = getSavedFilters(); - $: filters = $savedFilters[view] ?? []; function getQuery(): string { diff --git a/src/lib/components/table/EntryTable.svelte b/src/lib/components/table/EntryTable.svelte index 4af0262..42c2990 100644 --- a/src/lib/components/table/EntryTable.svelte +++ b/src/lib/components/table/EntryTable.svelte @@ -1,6 +1,4 @@ -
{#each { length: 4 } as _, i} - + +
diff --git a/tests/e2e/_test.ts b/tests/e2e/_test.ts deleted file mode 100644 index d0ed5a2..0000000 --- a/tests/e2e/_test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { encode } from "@auth/core/jwt"; -import { test as base, expect, type Page } from "@playwright/test"; - -import { prisma } from "$lib/server/prisma"; - -export const OIDC_BASE_URL = process.env.KEYCLOAK_ISSUER + "/"; -const AUTH_COOKIE = "authjs.session-token"; - -type Account = { - id: number; - name: string; - email: string; - accessToken: string | null; -}; - -export async function isLoggedIn(page: Page): Promise { - const cookies = await page.context().cookies(); - return cookies.findIndex((c) => c.name === AUTH_COOKIE) !== -1; -} - -async function newSessionToken(user: Account): Promise { - return encode({ - salt: AUTH_COOKIE, - secret: process.env.AUTH_SECRET!, - token: { - name: user.name, - email: user.email, - sub: user.id.toString(), - id: user.id.toString(), - accessToken: user.accessToken, - }, - }); -} - -export const test = base.extend({ - account: [async ({ browser }, use, workerInfo) => { - // Unique username - const name = "user" + workerInfo.workerIndex; - const email = name + "@example.org"; - - // Create the account - const page = await browser.newPage(); - await page.goto("/"); - - if (page.url().startsWith(OIDC_BASE_URL)) { - await page.locator('input[name="login"]').fill(name); - await page.locator('input[name="password"]').fill("1234"); - await page.locator("button.login-submit").click(); - await page.getByRole("button", { name: "Continue" }).click(); - expect(await isLoggedIn(page)).toBe(true); - } - - // Get user data - const user = await prisma.user.findUniqueOrThrow({ - select: { - id: true, - accounts: { - select: { access_token: true }, - where: { provider: "keycloak" }, - }, - }, - where: { email }, - }); - expect(user.accounts.length).toBe(1); - - // Use the account data when calling the fixture - await use({ - id: user.id, name, email, accessToken: user.accounts[0].access_token, - }); - }, { scope: "worker" }], -}).extend<{ - login: Page, -}>({ - login: async ({ page, account }, use) => { - const token = await newSessionToken(account); - await page.context().addCookies([{ - name: AUTH_COOKIE, - value: token, - domain: "localhost", - path: "/", - httpOnly: true, - sameSite: "Lax", - }]); - await use(page); - }, -}); - -export { expect } from "@playwright/test"; diff --git a/tests/e2e/login.test.ts b/tests/e2e/login.test.ts index 9d51f53..3150585 100644 --- a/tests/e2e/login.test.ts +++ b/tests/e2e/login.test.ts @@ -1,27 +1,39 @@ -import { - test, expect, isLoggedIn, OIDC_BASE_URL, -} from "./_test"; +import { test, expect } from "@playwright/test"; -test("login", async ({ login: page, account }) => { +import { + isLoggedIn, loginIfNecessary, loginWithToken, USERNAME, OIDC_BASE_URL, +} from "$tests/helpers/login"; + +test.describe.configure({ mode: "serial" }); // Parallel account creation may cause issues + +test("login", async ({ page }) => { await page.goto("/"); + await loginIfNecessary(page); await expect(page).toHaveTitle("Visitenbuch"); - await expect(page.locator("h1.heading")).toHaveText("Hallo, " + account.name); + await expect(page.locator("h1.heading")).toHaveText("Hallo, Lucy Login"); // Test cases may create more entries expect(parseInt(await page.getByTestId("n-entries-todo").innerText())) .toBeGreaterThanOrEqual(193); }); -test("logout", async ({ login: page, baseURL }) => { +test("loginWithToken", async ({ page }) => { + await loginWithToken(page); await page.goto("/"); + await expect(page).toHaveTitle("Visitenbuch"); + await expect(page.locator("h1.heading")).toHaveText("Hallo, " + USERNAME); + // Test cases may create more entries + expect(parseInt(await page.getByTestId("n-entries-todo").innerText())) + .toBeGreaterThanOrEqual(193); +}); + +test("logout", async ({ page, baseURL }) => { + await page.goto("/"); + await loginIfNecessary(page); + await page.goto("/logout"); await page.getByTestId("btn-logout").click(); - - // Sometimes the OIDC provider asks for login confirmation - if (page.url().startsWith(OIDC_BASE_URL)) { - await page.locator('button[value="yes"]').click(); - } - + await page.locator('button[value="yes"]').click(); await page.waitForURL("/login?noAuto=1"); await expect(page.getByTestId("btn-login")).toBeVisible(); expect(await isLoggedIn(page)).toBe(false); diff --git a/tests/e2e/plan.test.ts b/tests/e2e/plan.test.ts index 62aa555..3c6fcff 100644 --- a/tests/e2e/plan.test.ts +++ b/tests/e2e/plan.test.ts @@ -1,6 +1,9 @@ -import { test, expect } from "./_test"; +import { test, expect } from "@playwright/test"; -test("filter", async ({ login: page }) => { +import { loginWithToken } from "$tests/helpers/login"; + +test("filter", async ({ page }) => { + await loginWithToken(page); await page.goto("/plan"); await expect(page).toHaveTitle("Planung"); diff --git a/tests/helpers/login.ts b/tests/helpers/login.ts new file mode 100644 index 0000000..3521ee9 --- /dev/null +++ b/tests/helpers/login.ts @@ -0,0 +1,58 @@ +import { encode } from "@auth/core/jwt"; +import { expect, type Page } from "@playwright/test"; + +import { prisma } from "$lib/server/prisma"; + +const AUTH_COOKIE = "authjs.session-token"; + +export const OIDC_BASE_URL = "http://localhost:9090/interaction/"; +export const USERNAME = "Tico Testboy"; +export const USER_EMAIL = "t.testboy@example.org"; + +export async function loginIfNecessary(page: Page) { + if (page.url().startsWith(OIDC_BASE_URL)) { + await page.locator('input[name="login"]').fill("Lucy Login"); + await page.locator('input[name="password"]').fill("1234"); + await page.locator("button.login-submit").click(); + await page.getByRole("button", { name: "Continue" }).click(); + expect(await isLoggedIn(page)).toBe(true); + } +} + +export async function isLoggedIn(page: Page): Promise { + const cookies = await page.context().cookies(); + return cookies.findIndex((c) => c.name === AUTH_COOKIE) !== -1; +} + +/** + * Create a session token (Cookie: authjs.session-token) to use for E2E tests + * so we dont have to step through the login system every time + */ +export async function newSessionToken(user_id: number): Promise { + const user = await prisma.user.findUniqueOrThrow({ + select: { email: true, name: true }, + where: { id: user_id }, + }); + return encode({ + salt: AUTH_COOKIE, + secret: process.env.AUTH_SECRET!, + token: { + name: user.name, + email: user.email, + sub: user_id.toString(), + id: user_id.toString(), + }, + }); +} + +export async function loginWithToken(page: Page, user_id = 1) { + const token = await newSessionToken(user_id); + await page.context().addCookies([{ + name: AUTH_COOKIE, + value: token, + domain: "localhost", + path: "/", + httpOnly: true, + sameSite: "Lax", + }]); +} diff --git a/vite.config.ts b/vite.config.ts index 195765a..9d34322 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,8 +23,7 @@ export default defineConfig({ createViteLicensePlugin({ additionalFiles: { "oss-licenses.html": (packages) => { - let res = ` - + let res = ` Visitenbuch - Lizenzen @@ -32,12 +31,6 @@ export default defineConfig({

Open-Source-Lizenzen

JSON-formatted license list `; - const escapeHTML = (s: string | null) => s ? s.replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'") : ""; - for (const _p of packages) { type LicenseMetaExt = LicenseMeta & { repository: string | null, @@ -61,13 +54,13 @@ export default defineConfig({ } res += `
\n`; - res += `

${escapeHTML(p.name)}

\n`; + res += `

${p.name}

\n`; res += `\n`; - res += `\n`; - if (aut) res += `\n`; - res += `\n`; - if (repoUrl) res += `\n`; - else if (rp) res += `\n`; + res += `\n`; + if (aut) res += `\n`; + res += `\n`; + if (repoUrl) res += `\n`; + else if (rp) res += `\n`; res += `
Version:${escapeHTML(p.version)}
Author:${escapeHTML(aut)}
License:${escapeHTML(p.license)}
Repository:${escapeHTML(repoUrl)}
Repository:${escapeHTML(rp)}
Version:${p.version}
Author:${aut}
License:${p.license}
Repository:${repoUrl}
Repository:${rp}
\n`; res += "
"; } diff --git a/vitest.config.integration.js b/vitest.config.integration.js index 0289eb3..81c290e 100644 --- a/vitest.config.integration.js +++ b/vitest.config.integration.js @@ -4,11 +4,6 @@ import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vitest/config"; -// eslint-disable-next-line no-undef -const DATABASE_URL = process.env.TEST_DATABASE_URL ?? "postgresql://postgres:1234@localhost:5432/test?schema=public"; -// eslint-disable-next-line no-console -console.log("TEST_DATABASE_URL", DATABASE_URL); - export default defineConfig({ plugins: [sveltekit()], test: { @@ -18,7 +13,8 @@ export default defineConfig({ maxConcurrency: 1, setupFiles: ["tests/helpers/setup.ts"], env: { - DATABASE_URL, + // eslint-disable-next-line no-undef + DATABASE_URL: process.env.TEST_DATABASE_URL ?? "postgresql://postgres:1234@localhost:5432/test?schema=public", }, }, });