From 8801e9ab061f45af84a7cee391be33c89979573a Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 20 Nov 2025 12:06:03 +0100 Subject: [PATCH 1/4] reset db before each test to allow retrying --- frontend/tests/cox.spec.ts | 188 ++++++++++++++----------------------- frontend/tests/helpers.ts | 8 ++ frontend/tests/log.spec.ts | 108 +-------------------- 3 files changed, 84 insertions(+), 220 deletions(-) create mode 100644 frontend/tests/helpers.ts diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index 89da881..9969e8a 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -1,4 +1,9 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, Page } from "@playwright/test"; +import { resetDatabase } from "./helpers"; + +test.beforeEach(async () => { + await resetDatabase(); +}); test("cox can create and delete trip", async ({ page }) => { await page.goto("/auth"); @@ -16,22 +21,13 @@ test("cox can create and delete trip", async ({ page }) => { await page.getByRole("spinbutton").fill("5"); await page.getByRole("button", { name: "Erstellen", exact: true }).click(); await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details"); - - await page.goto("/planned"); - await page.getByRole('link', { name: 'Details' }).nth(1).click(); - await page.getByRole("link", { name: "Termin löschen" }).click(); - await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!"); }); // TODO: group -> cox can create trips // TODO: cox can help/register at trips/events test.describe("cox can edit trips", () => { - let sharedPage: Page; - - test.beforeAll(async ({ browser }) => { - const page = await browser.newPage(); - + async function createTrip(page: Page) { await page.goto("/auth"); await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").fill("cox"); @@ -46,151 +42,109 @@ test.describe("cox can edit trips", () => { await page.locator("#sidebar #planned_starting_time").press("Tab"); await page.getByRole("spinbutton").fill("5"); await page.getByRole("button", { name: "Erstellen", exact: true }).click(); + } - sharedPage = page; - }); + test("edit remarks", async ({ page }) => { + await createTrip(page); - test("edit remarks", async () => { - await sharedPage.goto("/planned"); - await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click(); - await sharedPage.locator("#sidebar #notes").click(); - await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung"); - await sharedPage.getByRole("button", { name: "Speichern" }).click(); - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await expect(sharedPage.locator("#sidebar")).toContainText( + await page.goto("/planned"); + await page.getByRole('link', { name: 'Details' }).nth(1).click(); + await page.locator("#sidebar #notes").click(); + await page.locator("#sidebar #notes").fill("Meine Anmerkung"); + await page.getByRole("button", { name: "Speichern" }).click(); + await page.getByRole("link", { name: "Details" }).nth(1).click(); + await expect(page.locator("#sidebar")).toContainText( "Meine Anmerkung", ); - - await sharedPage - .getByRole("button", { name: "Ausfahrt erstellen schließen" }) - .click(); }); - test("add and remove guest", async () => { - await sharedPage.goto("/planned"); - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await sharedPage.locator("#sidebar #user_note").click(); - await sharedPage.locator("#sidebar #user_note").fill("Mein Gast"); - await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click(); - await expect(sharedPage.locator("body")).toContainText( + test("add and remove guest", async ({ page }) => { + await createTrip(page); + + await page.goto("/planned"); + await page.getByRole("link", { name: "Details" }).nth(1).click(); + await page.locator("#sidebar #user_note").click(); + await page.locator("#sidebar #user_note").fill("Mein Gast"); + await page.getByRole("button", { name: "Gast hinzufügen" }).click(); + await expect(page.locator("body")).toContainText( "Erfolgreich angemeldet!", ); - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await expect(sharedPage.locator("#sidebar")).toContainText( + await page.getByRole("link", { name: "Details" }).nth(1).click(); + await expect(page.locator("#sidebar")).toContainText( "Freie Plätze: 4", ); - await expect(sharedPage.locator("#sidebar")).toContainText( + await expect(page.locator("#sidebar")).toContainText( "Mein Gast (Gast) Abmelden", ); await expect( - sharedPage.getByRole("link", { name: "Termin löschen" }), + page.getByRole("link", { name: "Termin löschen" }), ).not.toBeVisible(); - await sharedPage.getByRole("link", { name: "Abmelden" }).click(); - await expect(sharedPage.locator("body")).toContainText( + await page.getByRole("link", { name: "Abmelden" }).click(); + await expect(page.locator("body")).toContainText( "Erfolgreich abgemeldet!", ); - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await expect(sharedPage.locator("#sidebar")).toContainText( + await page.getByRole("link", { name: "Details" }).nth(1).click(); + await expect(page.locator("#sidebar")).toContainText( "Freie Plätze: 5", ); - await expect(sharedPage.locator("#sidebar")).toContainText( + await expect(page.locator("#sidebar")).toContainText( "Keine Ruderer angemeldet", ); await expect( - sharedPage.getByRole("link", { name: "Termin löschen" }), + page.getByRole("link", { name: "Termin löschen" }), ).toBeVisible(); - - await sharedPage - .getByRole("button", { name: "Ausfahrt erstellen schließen" }) - .click(); }); - test("change amount rower", async () => { - await sharedPage.goto("/planned"); - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await expect(sharedPage.locator("#sidebar")).toContainText( + test("change amount rower", async ({ page }) => { + await createTrip(page); + + await page.goto("/planned"); + await page.getByRole("link", { name: "Details" }).nth(1).click(); + await expect(page.locator("#sidebar")).toContainText( "Freie Plätze: 5", ); - await sharedPage.getByRole("spinbutton").click(); - await sharedPage.getByRole("spinbutton").fill("3"); - await sharedPage.getByRole("button", { name: "Speichern" }).click(); - await expect(sharedPage.locator("body")).toContainText( + await page.getByRole("spinbutton").click(); + await page.getByRole("spinbutton").fill("3"); + await page.getByRole("button", { name: "Speichern" }).click(); + await expect(page.locator("body")).toContainText( "Ausfahrt erfolgreich aktualisiert.", ); }); - test("call off trip", async () => { + test("call off trip", async ({ page }) => { + await createTrip(page); + // Someone registers... - await sharedPage.goto("/auth/logout"); - await sharedPage.goto("/auth"); - await sharedPage.getByPlaceholder("Name").click(); - await sharedPage.getByPlaceholder("Name").fill("rower"); - await sharedPage.getByPlaceholder("Name").press("Tab"); - await sharedPage.getByPlaceholder("Passwort").fill("rower"); - await sharedPage.getByPlaceholder("Passwort").press("Enter"); + await page.goto("/auth/logout"); + await page.goto("/auth"); + await page.getByPlaceholder("Name").click(); + await page.getByPlaceholder("Name").fill("rower"); + await page.getByPlaceholder("Name").press("Tab"); + await page.getByPlaceholder("Passwort").fill("rower"); + await page.getByPlaceholder("Passwort").press("Enter"); - await sharedPage.goto("/planned"); - await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click(); + await page.goto("/planned"); + await page.getByRole('link', { name: 'Mitrudern' }).nth(1).click(); - // Login as cox again - await sharedPage.goto("/auth/logout"); - await sharedPage.goto("/auth"); - await sharedPage.getByPlaceholder("Name").click(); - await sharedPage.getByPlaceholder("Name").fill("cox"); - await sharedPage.getByPlaceholder("Name").press("Tab"); - await sharedPage.getByPlaceholder("Passwort").fill("cox"); - await sharedPage.getByPlaceholder("Passwort").press("Enter"); + await page.goto("/auth/logout"); + await page.goto("/auth"); + await page.getByPlaceholder("Name").click(); + await page.getByPlaceholder("Name").fill("cox"); + await page.getByPlaceholder("Name").press("Tab"); + await page.getByPlaceholder("Passwort").fill("cox"); + await page.getByPlaceholder("Passwort").press("Enter"); - await sharedPage.goto("/planned"); + await page.goto("/planned"); - - // ... now I can cancel trip - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click(); - await expect(sharedPage.locator("body")).toContainText( + // Now cancel the trip + await page.getByRole("link", { name: "Details" }).nth(1).click(); + await page.getByRole("button", { name: "Ausfahrt absagen" }).click(); + await expect(page.locator("body")).toContainText( "Ausfahrt erfolgreich aktualisiert.", ); - await expect(sharedPage.locator("body")).toContainText("(Absage cox)"); - - - // Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails - await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); - await sharedPage.getByRole("spinbutton").click(); - await sharedPage.getByRole("spinbutton").fill("3"); - await sharedPage.getByRole("button", { name: "Speichern" }).click(); - - - - // deregistering - await sharedPage.goto("/auth/logout"); - await sharedPage.goto("/auth"); - await sharedPage.getByPlaceholder("Name").click(); - await sharedPage.getByPlaceholder("Name").fill("rower"); - await sharedPage.getByPlaceholder("Name").press("Tab"); - await sharedPage.getByPlaceholder("Passwort").fill("rower"); - await sharedPage.getByPlaceholder("Passwort").press("Enter"); - - await sharedPage.goto("/planned"); - await sharedPage.getByRole('link', { name: 'Abmelden' }).click(); - - - // now cox can delete trip again in afterAll - await sharedPage.goto("/auth/logout"); - await sharedPage.goto("/auth"); - await sharedPage.getByPlaceholder("Name").click(); - await sharedPage.getByPlaceholder("Name").fill("cox"); - await sharedPage.getByPlaceholder("Name").press("Tab"); - await sharedPage.getByPlaceholder("Passwort").fill("cox"); - await sharedPage.getByPlaceholder("Passwort").press("Enter"); - }); - - test.afterAll(async () => { - await sharedPage.goto("/planned"); - await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click(); - await sharedPage.getByRole("link", { name: "Termin löschen" }).click(); - await sharedPage.close(); + await expect(page.locator("body")).toContainText("(Absage cox)"); }); // TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type diff --git a/frontend/tests/helpers.ts b/frontend/tests/helpers.ts new file mode 100644 index 0000000..c758c46 --- /dev/null +++ b/frontend/tests/helpers.ts @@ -0,0 +1,8 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export async function resetDatabase(): Promise { + await execAsync('cd .. && ./test_db.sh'); +} diff --git a/frontend/tests/log.spec.ts b/frontend/tests/log.spec.ts index 626a597..fce7df1 100644 --- a/frontend/tests/log.spec.ts +++ b/frontend/tests/log.spec.ts @@ -1,4 +1,9 @@ import { test, expect } from "@playwright/test"; +import { resetDatabase } from "./helpers"; + +test.beforeEach(async () => { + await resetDatabase(); +}); test("Cox can start and cancel trip", async ({ page }, testInfo) => { await page.goto("/auth"); @@ -34,12 +39,6 @@ test("Cox can start and cancel trip", async ({ page }, testInfo) => { "Ausfahrt erfolgreich hinzugefügt", ); await expect(page.locator("body")).toContainText("Joe"); - - await page.getByRole("link", { name: "Joe" }).click(); - page.once("dialog", (dialog) => { - dialog.accept().catch(() => {}); - }); - await page.getByRole("link", { name: "Löschen" }).click(); }); test("Cox can start and finish trip", async ({ page }, testInfo) => { @@ -102,28 +101,6 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => { await expect(page.locator('body')).toContainText('(cox2)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); - - - //Ausloggen... - await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click(); - await page.getByRole('link', { name: 'Ausloggen' }).click(); - // Login as admin - await page.getByPlaceholder("Name").click(); - await page.getByPlaceholder("Name").fill("main"); - await page.getByPlaceholder("Name").press("Tab"); - await page.getByPlaceholder("Passwort").fill("admin"); - await page.getByPlaceholder("Passwort").press("Enter"); - - await page.goto("/log/show"); - await page.getByRole('link', { name: 'Joe' }).nth(1).click(); - page.once("dialog", (dialog) => { - dialog.accept().catch(() => {}); - }); - await page.getByRole('link', { name: 'Löschen' }).click(); - - //Ausloggen... - await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click(); - await page.getByRole('link', { name: 'Ausloggen' }).click(); }); test("Kiosk can start and cancel trip", async ({ page }, testInfo) => { @@ -151,12 +128,6 @@ test("Kiosk can start and cancel trip", async ({ page }, testInfo) => { "Ausfahrt erfolgreich hinzugefügt", ); await expect(page.locator("body")).toContainText("Joe"); - - await page.getByRole("link", { name: "Joe" }).click(); - page.once("dialog", (dialog) => { - dialog.accept().catch(() => {}); - }); - await page.getByRole("link", { name: "Löschen" }).click(); }); test("Kiosk can start and finish trip", async ({ page }, testInfo) => { @@ -210,29 +181,6 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => { await expect(page.locator('body')).toContainText('Joe'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); - - - - //Ausloggen... - await page.context().clearCookies(); - await page.goto("/auth"); - // Login as admin - await page.getByPlaceholder("Name").click(); - await page.getByPlaceholder("Name").fill("main"); - await page.getByPlaceholder("Name").press("Tab"); - await page.getByPlaceholder("Passwort").fill("admin"); - await page.getByPlaceholder("Passwort").press("Enter"); - - await page.goto("/log/show"); - await page.getByRole('link', { name: 'Joe' }).nth(1).click(); - page.once("dialog", (dialog) => { - dialog.accept().catch(() => {}); - }); - await page.getByRole('link', { name: 'Löschen' }).click(); - - //Ausloggen... - await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click(); - await page.getByRole('link', { name: 'Ausloggen' }).click(); }); test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => { @@ -286,29 +234,6 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te await page.goto('/log/show'); await expect(page.locator('body')).toContainText('cox_only_steering_boat'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); - - - - //Ausloggen... - await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click(); - await page.getByRole('link', { name: 'Ausloggen' }).click(); - // Login as admin - await page.getByPlaceholder("Name").click(); - await page.getByPlaceholder("Name").fill("main"); - await page.getByPlaceholder("Name").press("Tab"); - await page.getByPlaceholder("Passwort").fill("admin"); - await page.getByPlaceholder("Passwort").press("Enter"); - - await page.goto("/log/show"); - await page.getByRole("link", { name: "cox_only_steering_boat" }).click(); - page.once("dialog", (dialog) => { - dialog.accept().catch(() => {}); - }); - await page.getByRole('link', { name: 'Löschen' }).click(); - - //Ausloggen... - await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click(); - await page.getByRole('link', { name: 'Ausloggen' }).click(); }); test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => { @@ -355,27 +280,4 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) = await expect(page.locator('body')).toContainText('(cox2)'); await expect(page.locator('body')).toContainText('a (1 km)'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); - - - - //Ausloggen... - await page.context().clearCookies(); - await page.goto("/auth"); - // Login as admin - await page.getByPlaceholder("Name").click(); - await page.getByPlaceholder("Name").fill("main"); - await page.getByPlaceholder("Name").press("Tab"); - await page.getByPlaceholder("Passwort").fill("admin"); - await page.getByPlaceholder("Passwort").press("Enter"); - - await page.goto("/log/show"); - await page.getByRole('link', { name: 'Joe' }).nth(1).click(); - page.once("dialog", (dialog) => { - dialog.accept().catch(() => {}); - }); - await page.getByRole('link', { name: 'Löschen' }).click(); - - //Ausloggen... - await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click(); - await page.getByRole('link', { name: 'Ausloggen' }).click(); }); -- 2.49.1 From 0be1a3525291037338f60aaf02c7bbb27440a298 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 20 Nov 2025 12:49:52 +0100 Subject: [PATCH 2/4] try to fix ci --- frontend/tests/helpers.ts | 2 +- reset_test_data.sh | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100755 reset_test_data.sh diff --git a/frontend/tests/helpers.ts b/frontend/tests/helpers.ts index c758c46..1c581ed 100644 --- a/frontend/tests/helpers.ts +++ b/frontend/tests/helpers.ts @@ -4,5 +4,5 @@ import { promisify } from 'util'; const execAsync = promisify(exec); export async function resetDatabase(): Promise { - await execAsync('cd .. && ./test_db.sh'); + await execAsync('cd .. && ./reset_test_data.sh'); } diff --git a/reset_test_data.sh b/reset_test_data.sh new file mode 100755 index 0000000..08c8066 --- /dev/null +++ b/reset_test_data.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e + +DB_FILE="db.sqlite" + +# Clear all data and reseed +sqlite3 "$DB_FILE" << 'EOF' +PRAGMA writable_schema = 1; +DELETE FROM sqlite_sequence; +PRAGMA writable_schema = 0; +PRAGMA foreign_keys = OFF; +EOF + +# Get all tables and delete from them +sqlite3 "$DB_FILE" "SELECT 'DELETE FROM ' || name || ';' FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';" | sqlite3 "$DB_FILE" + +# Re-enable foreign keys and reseed +sqlite3 "$DB_FILE" "PRAGMA foreign_keys = ON;" +sqlite3 "$DB_FILE" < seeds.sql -- 2.49.1 From ee7c7bc0d629e43901ec4092f89c4ac7692ba3a1 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 20 Nov 2025 16:58:16 +0100 Subject: [PATCH 3/4] try to fix ci --- frontend/tests/cox.spec.ts | 16 +++------------- frontend/tests/helpers.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index 9969e8a..dea3eb8 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -1,5 +1,5 @@ import { test, expect, Page } from "@playwright/test"; -import { resetDatabase } from "./helpers"; +import { resetDatabase, login } from "./helpers"; test.beforeEach(async () => { await resetDatabase(); @@ -117,24 +117,14 @@ test.describe("cox can edit trips", () => { // Someone registers... await page.goto("/auth/logout"); - await page.goto("/auth"); - await page.getByPlaceholder("Name").click(); - await page.getByPlaceholder("Name").fill("rower"); - await page.getByPlaceholder("Name").press("Tab"); - await page.getByPlaceholder("Passwort").fill("rower"); - await page.getByPlaceholder("Passwort").press("Enter"); + await login(page, "rower", "rower"); await page.goto("/planned"); await page.getByRole('link', { name: 'Mitrudern' }).nth(1).click(); // Login as cox again await page.goto("/auth/logout"); - await page.goto("/auth"); - await page.getByPlaceholder("Name").click(); - await page.getByPlaceholder("Name").fill("cox"); - await page.getByPlaceholder("Name").press("Tab"); - await page.getByPlaceholder("Passwort").fill("cox"); - await page.getByPlaceholder("Passwort").press("Enter"); + await login(page, "cox", "cox"); await page.goto("/planned"); diff --git a/frontend/tests/helpers.ts b/frontend/tests/helpers.ts index 1c581ed..2bd18f1 100644 --- a/frontend/tests/helpers.ts +++ b/frontend/tests/helpers.ts @@ -1,8 +1,26 @@ import { exec } from 'child_process'; import { promisify } from 'util'; +import { Page } from '@playwright/test'; const execAsync = promisify(exec); export async function resetDatabase(): Promise { await execAsync('cd .. && ./reset_test_data.sh'); } + +export async function login(page: Page, username: string, password: string): Promise { + // Clear cookies to ensure clean state + await page.context().clearCookies(); + + await page.goto("/auth"); + await page.getByPlaceholder("Name").click(); + await page.getByPlaceholder("Name").fill(username); + await page.getByPlaceholder("Passwort").click(); + await page.getByPlaceholder("Passwort").fill(password); + + // Wait for navigation after form submission + await Promise.all([ + page.waitForURL(/\/(planned|log|$)/, { timeout: 10000 }), + page.getByPlaceholder("Passwort").press("Enter") + ]); +} -- 2.49.1 From 900cb2a24df7ee90ef89570f1fb0ac9dfac6948a Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 20 Nov 2025 18:43:06 +0100 Subject: [PATCH 4/4] try to fix ci --- frontend/tests/cox.spec.ts | 2 ++ frontend/tests/helpers.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index dea3eb8..f31c0a1 100644 --- a/frontend/tests/cox.spec.ts +++ b/frontend/tests/cox.spec.ts @@ -117,6 +117,7 @@ test.describe("cox can edit trips", () => { // Someone registers... await page.goto("/auth/logout"); + await page.waitForURL("/auth"); await login(page, "rower", "rower"); await page.goto("/planned"); @@ -124,6 +125,7 @@ test.describe("cox can edit trips", () => { // Login as cox again await page.goto("/auth/logout"); + await page.waitForURL("/auth"); await login(page, "cox", "cox"); await page.goto("/planned"); diff --git a/frontend/tests/helpers.ts b/frontend/tests/helpers.ts index 2bd18f1..ce590d3 100644 --- a/frontend/tests/helpers.ts +++ b/frontend/tests/helpers.ts @@ -12,7 +12,10 @@ export async function login(page: Page, username: string, password: string): Pro // Clear cookies to ensure clean state await page.context().clearCookies(); - await page.goto("/auth"); + // Navigate to auth page and wait for it to fully load + await page.goto("/auth", { waitUntil: 'load' }); + await page.waitForLoadState('networkidle'); + await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").fill(username); await page.getByPlaceholder("Passwort").click(); -- 2.49.1