Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
1de0cf7d59 | |||
7ea87d4240 | |||
e7de2f2588 | |||
b0b2cb5bb9 | |||
e63888a103 | |||
a17ca9bbba | |||
04523b17e9 | |||
7dab4abbd3 |
43 changed files with 223 additions and 443 deletions
|
@ -2,7 +2,6 @@
|
|||
node_modules
|
||||
/.svelte-kit
|
||||
/package
|
||||
/playwright-report
|
||||
.env
|
||||
.env.*
|
||||
.eslintcache
|
||||
|
|
|
@ -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:
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,7 +3,6 @@ node_modules
|
|||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
/playwright-report
|
||||
.env
|
||||
.eslintcache
|
||||
vite.config.js.timestamp-*
|
||||
|
|
|
@ -13,5 +13,4 @@ repos:
|
|||
entry: npx eslint
|
||||
args:
|
||||
- "--max-warnings=0"
|
||||
- "--cache"
|
||||
files: \.(js|ts|svelte)$
|
||||
|
|
55
CHANGELOG.md
55
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
|
||||
|
|
|
@ -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 && \
|
||||
|
|
44
README.md
44
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 .
|
||||
```
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
|
@ -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[]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/dumb-init /bin/sh
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Migrate database before starting server
|
||||
npx prisma migrate deploy
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string, () => 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}
|
||||
<div bind:this={listElm} class="autocomplete-list" tabindex="-1" use:floatingContent>
|
||||
<div bind:this={listElm} class="autocomplete-list" use:floatingContent>
|
||||
{#each filteredItems as item, i}
|
||||
<div
|
||||
class="autocomplete-list-item"
|
||||
|
@ -336,12 +344,7 @@
|
|||
|
||||
{#if clearBtn && selection}
|
||||
<div class="absolute bottom-0 right-0 h-full flex items-center">
|
||||
<IconButton
|
||||
cls=""
|
||||
path={mdiClose}
|
||||
tabindex={-1}
|
||||
title="Löschen"
|
||||
on:click={clearSelection} />
|
||||
<IconButton cls="" path={mdiClose} title="Löschen" on:click={clearSelection} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { Debouncer } from "$lib/shared/util";
|
||||
|
||||
|
@ -193,23 +192,8 @@
|
|||
searchDebounce.now();
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowKeyup(e: KeyboardEvent): void {
|
||||
// Dont catch keybinds when inputting text
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||
if (e.key === "f") {
|
||||
e.preventDefault();
|
||||
focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (hiddenIds.size === 0) activeFilters = activeFilters;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keyup={onWindowKeyup} />
|
||||
|
||||
<div class="filterbar-outer">
|
||||
<div class="filterbar-inner input input-sm input-bordered">
|
||||
{#each activeFilters as fdata, i}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatDate } from "$lib/shared/util";
|
||||
|
||||
|
@ -41,7 +39,6 @@
|
|||
class="transition-colors hover:bg-neutral-content/10"
|
||||
class:done={entry.execution?.done}
|
||||
class:priority={entry.current_version.priority}
|
||||
on:dblclick={() => { void goto("/entry/" + entry.id); }}
|
||||
>
|
||||
<td
|
||||
><a
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import { mdiFilter } from "@mdi/js";
|
||||
|
||||
import { URL_ENTRIES } from "$lib/shared/constants";
|
||||
|
@ -36,11 +34,10 @@
|
|||
<tr
|
||||
class="transition-colors hover:bg-neutral-content/10"
|
||||
class:p-hidden={patient.hidden}
|
||||
on:dblclick={() => { void goto("/patient/" + patient.id); }}
|
||||
>
|
||||
<td
|
||||
><a
|
||||
class="btn btn-xs btn-primary btn-id"
|
||||
class="btn btn-xs btn-primary"
|
||||
aria-label="Eintrag anzeigen"
|
||||
href="/patient/{patient.id}">{patient.id}</a
|
||||
></td
|
||||
|
|
|
@ -1,21 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { humanDate } from "$lib/shared/util";
|
||||
|
||||
function dateIn(n: number): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + n);
|
||||
return humanDate(date);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="join">
|
||||
{#each { length: 4 } as _, i}
|
||||
<button
|
||||
name="todo"
|
||||
class="join-item btn btn-sm"
|
||||
title={i > 0 ? `Eintrag auf ${dateIn(i)} verschieben` : undefined}
|
||||
type="submit"
|
||||
value={i}>
|
||||
<button name="todo" class="join-item btn btn-sm" type="submit" value={i}>
|
||||
{#if i === 0}
|
||||
Notiz
|
||||
{:else}
|
||||
|
|
|
@ -24,16 +24,6 @@ export const AUTH_CFG: AuthConfig = {
|
|||
clientId: env.KEYCLOAK_CLIENT_ID,
|
||||
clientSecret: env.KEYCLOAK_CLIENT_SECRET,
|
||||
issuer: env.KEYCLOAK_ISSUER,
|
||||
/*
|
||||
Optional manual OIDC endpoint config.
|
||||
Normally this is configured via the issuer URL
|
||||
(KEYCLOAK_ISSUER/.well-known/openid-configuration),
|
||||
but if the OIDC server is available under a different
|
||||
internal domain, these variables must be manually set
|
||||
*/
|
||||
authorization: env.KEYCLOAK_EP_AUTHORIZATION,
|
||||
token: env.KEYCLOAK_EP_TOKEN,
|
||||
userinfo: env.KEYCLOAK_EP_USERINFO,
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
|
|
|
@ -287,25 +287,6 @@ left join stations s on s.id = r.station_id`,
|
|||
qb.addFilterClause(`ev.date <= ${qb.pvar()}`, dateRange.end);
|
||||
}
|
||||
});
|
||||
|
||||
const dfClauses: string[] = [];
|
||||
const dfParams: Date[] = [];
|
||||
filterListToArray(filter.date).forEach((itm) => {
|
||||
const dateRange = DateRange.parse(itm, true);
|
||||
const cl = [];
|
||||
if (dateRange?.start) {
|
||||
cl.push(`ev.date >= ${qb.pvar()}`);
|
||||
dfParams.push(dateRange.start);
|
||||
}
|
||||
if (dateRange?.end) {
|
||||
cl.push(`ev.date <= ${qb.pvar()}`);
|
||||
dfParams.push(dateRange.end);
|
||||
}
|
||||
dfClauses.push(cl.join(" and "));
|
||||
});
|
||||
if (dfClauses.length > 0) {
|
||||
qb.addFilterClause(dfClauses.join(" or "), ...dfParams);
|
||||
}
|
||||
}
|
||||
|
||||
const SORT_FIELDS: Record<string, string[]> = {
|
||||
|
|
|
@ -130,10 +130,6 @@ class SearchQueryComponents {
|
|||
* Supported search syntax:
|
||||
* - Negative query `-word`
|
||||
* - Exact query `"word"`
|
||||
*
|
||||
* The last word in a search query is prefix-matched (i.e.
|
||||
* the search returns all results starting with the given characters).
|
||||
* This allows for meaningful results in Search-as-you-type applications.
|
||||
*/
|
||||
export function parseSearchQuery(q: string): SearchQueryComponents {
|
||||
const regexpParts = /(-)?(?:"([^"]*)"|([^"\s]+))(?:\s|$)/g;
|
||||
|
|
|
@ -77,13 +77,6 @@ function dateDiffInDays(a: Date, b: Date): number {
|
|||
return Math.round((ts2 - ts1) / MS_PER_DAY);
|
||||
}
|
||||
|
||||
/** Format a date with a human-readable format
|
||||
* - If the date is within +/- 3 days, output a textual format ("heute", "morgen", "vor 2 Tagen")
|
||||
* - Otherwise, format it as DD.MM.YYYY (or DD.MM.YYYY, hh:mm if `time=true`)
|
||||
*
|
||||
* @param [time=false] Enable time display
|
||||
* @param [cap=false] Enable capitalized format
|
||||
*/
|
||||
export function humanDate(date: Date | string, time = false, cap = false): string {
|
||||
const now = new Date();
|
||||
const dt = coerceDate(date);
|
||||
|
@ -103,7 +96,7 @@ export function humanDate(date: Date | string, time = false, cap = false): strin
|
|||
if (diffDays !== 0) {
|
||||
if (diffDays === 1) return outstr("morgen");
|
||||
if (diffDays === -1) return outstr("gestern");
|
||||
return outstr(intl.format(diffDays, "day"));
|
||||
return intl.format(diffDays, "day");
|
||||
}
|
||||
|
||||
if (time) {
|
||||
|
|
|
@ -158,11 +158,7 @@ export function defaultVisitUrl(): string {
|
|||
}, URL_VISIT);
|
||||
}
|
||||
|
||||
export async function moveEntryTodoDate(
|
||||
id:number,
|
||||
nTodoDays: number,
|
||||
init?: TRPCClientInit,
|
||||
): Promise<Date | null> {
|
||||
export async function moveEntryTodoDate(id: number, nTodoDays: number, init?: TRPCClientInit) {
|
||||
if (nTodoDays > 0) {
|
||||
const entry = await trpc(init).entry.get.query(id);
|
||||
const newDate = new Date();
|
||||
|
@ -175,7 +171,5 @@ export async function moveEntryTodoDate(
|
|||
date: utcDateToYMD(newDate),
|
||||
},
|
||||
});
|
||||
return newDate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { getContext } from "svelte";
|
||||
import { derived, writable, type Writable } from "svelte/store";
|
||||
|
||||
import type { SavedFilter } from "$lib/shared/model";
|
||||
|
@ -7,6 +6,4 @@ import type { SavedFilter } from "$lib/shared/model";
|
|||
export const screenWidth = writable(0);
|
||||
export const screenWidthSmall = derived(screenWidth, ($mainWidth) => $mainWidth < 500);
|
||||
|
||||
// Context key: "savedFilters"
|
||||
export type SavedFilters = Writable<Record<string, SavedFilter[]>>;
|
||||
export const getSavedFilters: () => SavedFilters = () => getContext("savedFilters");
|
||||
export const savedFilters: Writable<Record<string, SavedFilter[]>> = writable({});
|
||||
|
|
|
@ -3,22 +3,16 @@
|
|||
import type { LayoutData } from "./$types";
|
||||
|
||||
import { mdiAccount, mdiHome } from "@mdi/js";
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { defaultFilterUrl, defaultVisitUrl } from "$lib/shared/util";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import NavLink from "$lib/components/ui/NavLink.svelte";
|
||||
import type { SavedFilters } from "$lib/stores";
|
||||
import { savedFilters } from "$lib/stores";
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
const savedFilters: SavedFilters = writable();
|
||||
|
||||
$: savedFilters.set(data.savedFilters);
|
||||
|
||||
setContext("savedFilters", savedFilters);
|
||||
</script>
|
||||
|
||||
<div class="navbar-outer">
|
||||
|
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
import { defaultFilterUrl } from "$lib/shared/util";
|
||||
|
||||
import { getSavedFilters } from "$lib/stores";
|
||||
import { savedFilters } from "$lib/stores";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const savedFilters = getSavedFilters();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { superValidate, message } from "sveltekit-superforms";
|
|||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { humanDate, loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||
|
||||
import { SchemaNewExecution } from "./schema";
|
||||
|
||||
|
@ -22,21 +22,18 @@ export const actions: Actions = {
|
|||
const done = todoDays === null;
|
||||
const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0;
|
||||
|
||||
if (form.data.text.length > 0) {
|
||||
await loadWrap(async () => {
|
||||
await trpc(event).entry.newExecution.mutate({
|
||||
id,
|
||||
old_execution_id: form.data.old_execution_id,
|
||||
execution: { text: form.data.text, done: todoDays === null },
|
||||
});
|
||||
}
|
||||
const newTodoDate = await moveEntryTodoDate(id, nTodoDays, event);
|
||||
await moveEntryTodoDate(id, nTodoDays, event);
|
||||
});
|
||||
|
||||
if (newTodoDate) {
|
||||
return message(form, `Eintrag auf ${humanDate(newTodoDate)} verschoben`);
|
||||
if (nTodoDays > 0) {
|
||||
return message(form, `Eintrag um ${nTodoDays} Tage in die Zukunft verschoben`);
|
||||
}
|
||||
if (form.data.text.length > 0) {
|
||||
return message(form, done ? "Eintrag erledigt" : "Eintrag mit Notiz versehen");
|
||||
}
|
||||
return { form };
|
||||
return message(form, done ? "Eintrag erledigt" : "Eintrag mit Notiz versehen");
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
|
@ -32,7 +33,7 @@
|
|||
label="Eintrag erledigen"
|
||||
bind:value={$form.text}
|
||||
>
|
||||
<div class="row c-vlight gap-2 flex-wrap">
|
||||
<div class="row c-vlight gap-2">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Erledigt</button>
|
||||
<EntryTodoButton />
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { superValidate } from "sveltekit-superforms";
|
|||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { loadWrap, moveEntryTodoDate } from "$lib/shared/util";
|
||||
|
||||
import { SchemaNewExecution } from "../schema";
|
||||
|
||||
|
@ -29,6 +29,9 @@ export const actions: Actions = {
|
|||
old_execution_id: form.data.old_execution_id,
|
||||
});
|
||||
|
||||
const nTodoDays = todoDays ? parseInt(todoDays.toString()) : 0;
|
||||
await moveEntryTodoDate(id, nTodoDays, event);
|
||||
|
||||
redirect(302, `/entry/${id}`);
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { superformConfig } from "$lib/shared/util";
|
||||
|
||||
import EntryBody from "$lib/components/entry/EntryBody.svelte";
|
||||
import EntryTodoButton from "$lib/components/ui/EntryTodoButton.svelte";
|
||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||
|
||||
import { SchemaNewExecution } from "../schema";
|
||||
|
@ -31,9 +32,7 @@
|
|||
>
|
||||
<div class="row c-vlight gap-2">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Speichern</button>
|
||||
<button name="todo" class="join-item btn btn-sm" type="submit" value="0">
|
||||
Notiz
|
||||
</button>
|
||||
<EntryTodoButton />
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
<input name="old_execution_id" type="hidden" bind:value={$form.old_execution_id} />
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<tbody>
|
||||
{#each data.rooms as room (room.id)}
|
||||
<tr>
|
||||
<td><a class="btn btn-sm btn-id" href="/room/{room.id}">{room.name}</a></td>
|
||||
<td><a class="btn btn-sm" href="/room/{room.id}">{room.name}</a></td>
|
||||
<td>{room.station.name}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
{#each data.stations as station (station.id)}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-id" href="/station/{station.id}">{station.name}</a>
|
||||
<a class="btn btn-sm" href="/station/{station.id}">{station.name}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
|
8
src/routes/(app)/test/+page.svelte
Normal file
8
src/routes/(app)/test/+page.svelte
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button class="btn" on:click={() => toast.push({ msg: "Hello" })}>Ok</button>
|
||||
<button class="btn" on:click={() => toast.push({ msg: "Error", classes: ["toast-error"] })}>Error</button>
|
||||
</div>
|
|
@ -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<boolean> {
|
||||
const cookies = await page.context().cookies();
|
||||
return cookies.findIndex((c) => c.name === AUTH_COOKIE) !== -1;
|
||||
}
|
||||
|
||||
async function newSessionToken(user: Account): Promise<string> {
|
||||
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<object, {
|
||||
account: Account,
|
||||
}>({
|
||||
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";
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
58
tests/helpers/login.ts
Normal file
58
tests/helpers/login.ts
Normal file
|
@ -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<boolean> {
|
||||
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<string> {
|
||||
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",
|
||||
}]);
|
||||
}
|
|
@ -23,8 +23,7 @@ export default defineConfig({
|
|||
createViteLicensePlugin({
|
||||
additionalFiles: {
|
||||
"oss-licenses.html": (packages) => {
|
||||
let res = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
let res = `<html>
|
||||
<head>
|
||||
<title>Visitenbuch - Lizenzen</title>
|
||||
</head>
|
||||
|
@ -32,12 +31,6 @@ export default defineConfig({
|
|||
<h1>Open-Source-Lizenzen</h1>
|
||||
<a href="./oss-licenses.json">JSON-formatted license list</a>
|
||||
`;
|
||||
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 += `<div class="package">\n`;
|
||||
res += `<h3><a href="https://www.npmjs.com/package/${escapeHTML(p.name)}" target="_blank" rel="noopener noreferrer">${escapeHTML(p.name)}</a></h3>\n`;
|
||||
res += `<h3><a href="https://www.npmjs.com/package/${p.name}" target="_blank" rel="noopener noreferrer">${p.name}</a></h3>\n`;
|
||||
res += `<table>\n`;
|
||||
res += `<tr><td>Version:</td><td>${escapeHTML(p.version)}</td></tr>\n`;
|
||||
if (aut) res += `<tr><td>Author:</td><td>${escapeHTML(aut)}</td></tr>\n`;
|
||||
res += `<tr><td>License:</td><td>${escapeHTML(p.license)}</td></tr>\n`;
|
||||
if (repoUrl) res += `<tr><td>Repository:</td><td><a href="${escapeHTML(repoUrl)}" target="_blank" rel="noopener noreferrer">${escapeHTML(repoUrl)}</a></td></tr>\n`;
|
||||
else if (rp) res += `<tr><td>Repository:</td><td>${escapeHTML(rp)}</td></tr>\n`;
|
||||
res += `<tr><td>Version:</td><td>${p.version}</td></tr>\n`;
|
||||
if (aut) res += `<tr><td>Author:</td><td>${aut}</td></tr>\n`;
|
||||
res += `<tr><td>License:</td><td>${p.license}</td></tr>\n`;
|
||||
if (repoUrl) res += `<tr><td>Repository:</td><td><a href="${repoUrl}" target="_blank" rel="noopener noreferrer">${repoUrl}</a></td></tr>\n`;
|
||||
else if (rp) res += `<tr><td>Repository:</td><td>${rp}</td></tr>\n`;
|
||||
res += `</table>\n`;
|
||||
res += "</div>";
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue