From 43d9dcc31a185ccb41205900bfde2e8f1b98264d Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 20 Nov 2025 19:21:11 +0100 Subject: [PATCH] more-robust-ui-tests (#1158) Co-authored-by: Philipp Hofer Reviewed-on: https://git.hofer.link/Ruderverein-Donau-Linz/rowt/pulls/1158 Co-authored-by: Philipp Hofer Co-committed-by: Philipp Hofer --- frontend/tests/cox.spec.ts | 180 +++++++++++++------------------------ frontend/tests/helpers.ts | 29 ++++++ frontend/tests/log.spec.ts | 108 ++-------------------- reset_test_data.sh | 19 ++++ 4 files changed, 116 insertions(+), 220 deletions(-) create mode 100644 frontend/tests/helpers.ts create mode 100755 reset_test_data.sh diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts index 89da881..f31c0a1 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, login } 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,101 @@ 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.waitForURL("/auth"); + await login(page, "rower", "rower"); - 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.waitForURL("/auth"); + await login(page, "cox", "cox"); - 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..ce590d3 --- /dev/null +++ b/frontend/tests/helpers.ts @@ -0,0 +1,29 @@ +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(); + + // 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(); + 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") + ]); +} 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(); }); 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