e2e: fix flaky tests by adding page readiness chaeck
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled

This commit is contained in:
Nicolas Meienberger 2026-02-17 20:13:19 +01:00
parent 85e2272a5c
commit 289832d58b
5 changed files with 38 additions and 12 deletions

View file

@ -4,6 +4,7 @@ import { apiClientMiddleware } from "~/middleware/api-client";
import type { QueryClient } from "@tanstack/react-query"; import type { QueryClient } from "@tanstack/react-query";
import { Toaster } from "~/client/components/ui/sonner"; import { Toaster } from "~/client/components/ui/sonner";
import { useServerEvents } from "~/client/hooks/use-server-events"; import { useServerEvents } from "~/client/hooks/use-server-events";
import { useEffect } from "react";
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
server: { server: {
@ -34,6 +35,12 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
function RootLayout() { function RootLayout() {
useServerEvents(); useServerEvents();
useEffect(() => {
document.body.setAttribute("data-app-ready", "true");
return () => {
document.body.removeAttribute("data-app-ready");
};
}, []);
return ( return (
<html lang="en"> <html lang="en">

View file

@ -2,6 +2,7 @@ import fs from "fs";
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { resetDatabase } from "./helpers/db"; import { resetDatabase } from "./helpers/db";
import path from "node:path"; import path from "node:path";
import { gotoAndWaitForAppReady } from "./helpers/page";
const authFile = path.join(process.cwd(), "./playwright/.auth/user.json"); const authFile = path.join(process.cwd(), "./playwright/.auth/user.json");
@ -15,7 +16,7 @@ test.beforeAll(async () => {
}); });
test("should redirect to onboarding", async ({ page }) => { test("should redirect to onboarding", async ({ page }) => {
await page.goto("/"); await gotoAndWaitForAppReady(page, "/");
await page.waitForURL(/onboarding/); await page.waitForURL(/onboarding/);
@ -23,7 +24,7 @@ test("should redirect to onboarding", async ({ page }) => {
}); });
test("user can register a new account", async ({ page }) => { test("user can register a new account", async ({ page }) => {
await page.goto("/onboarding"); await gotoAndWaitForAppReady(page, "/onboarding");
await page.getByRole("textbox", { name: "Email" }).click(); await page.getByRole("textbox", { name: "Email" }).click();
await page.getByRole("textbox", { name: "Email" }).fill("test@test.com"); await page.getByRole("textbox", { name: "Email" }).fill("test@test.com");
@ -39,7 +40,7 @@ test("user can register a new account", async ({ page }) => {
}); });
test("user can download recovery key", async ({ page }) => { test("user can download recovery key", async ({ page }) => {
await page.goto("/login"); await gotoAndWaitForAppReady(page, "/login");
await page.getByRole("textbox", { name: "Username" }).fill("test"); await page.getByRole("textbox", { name: "Username" }).fill("test");
await page.getByRole("textbox", { name: "Password" }).fill("password"); await page.getByRole("textbox", { name: "Password" }).fill("password");
@ -69,7 +70,7 @@ test("user can download recovery key", async ({ page }) => {
}); });
test("can't create another admin user after initial setup", async ({ page }) => { test("can't create another admin user after initial setup", async ({ page }) => {
await page.goto("/onboarding"); await gotoAndWaitForAppReady(page, "/onboarding");
await page.getByRole("textbox", { name: "Email" }).click(); await page.getByRole("textbox", { name: "Email" }).click();
await page.getByRole("textbox", { name: "Email" }).fill("test@test.com"); await page.getByRole("textbox", { name: "Email" }).fill("test@test.com");
@ -85,7 +86,7 @@ test("can't create another admin user after initial setup", async ({ page }) =>
}); });
test("can login after initial setup", async ({ page }) => { test("can login after initial setup", async ({ page }) => {
await page.goto("/login"); await gotoAndWaitForAppReady(page, "/login");
await page.getByRole("textbox", { name: "Username" }).fill("test"); await page.getByRole("textbox", { name: "Username" }).fill("test");
await page.getByRole("textbox", { name: "Password" }).fill("password"); await page.getByRole("textbox", { name: "Password" }).fill("password");

View file

@ -1,6 +1,7 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import path from "node:path"; import path from "node:path";
import fs from "node:fs"; import fs from "node:fs";
import { gotoAndWaitForAppReady } from "./helpers/page";
test.describe.configure({ mode: "serial" }); test.describe.configure({ mode: "serial" });
@ -16,7 +17,7 @@ test.beforeAll(() => {
}); });
test("can backup & restore a file", async ({ page }) => { test("can backup & restore a file", async ({ page }) => {
await page.goto("/"); await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes"); await expect(page).toHaveURL("/volumes");
// 0. Create a test file in /test-data // 0. Create a test file in /test-data
@ -75,17 +76,24 @@ test("can backup & restore a file", async ({ page }) => {
}); });
test("deleting a volume cascades and removes its backup schedule", async ({ page }) => { test("deleting a volume cascades and removes its backup schedule", async ({ page }) => {
await page.goto("/backups"); await gotoAndWaitForAppReady(page, "/backups");
await page.getByText("Test Backup", { exact: true }).first().click(); await page.getByText("Test Backup", { exact: true }).first().click();
await expect(page.getByRole("link", { name: "Test Volume" })).toBeVisible(); const volumeLink = page.locator("main").getByRole("link", { name: "Test Volume", exact: true }).first();
await page.getByRole("link", { name: "Test Volume" }).click(); await expect(volumeLink).toBeVisible();
await volumeLink.click();
await expect(page).toHaveURL(/\/volumes\/[^/?#]+/);
await expect(page.getByText("Volume Configuration", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Delete" })).toBeVisible();
await page.getByRole("button", { name: "Delete" }).click(); await expect(async () => {
await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("heading", { name: "Delete volume?" })).toBeVisible();
}).toPass({ timeout: 10000 });
await expect(page.getByText("All backup schedules associated with this volume will also be removed.")).toBeVisible(); await expect(page.getByText("All backup schedules associated with this volume will also be removed.")).toBeVisible();
await page.getByRole("button", { name: "Delete volume" }).click(); await page.getByRole("button", { name: "Delete volume" }).click();
await expect(page.getByText("Volume deleted successfully")).toBeVisible(); await expect(page.getByText("Volume deleted successfully")).toBeVisible();
await page.goto("/backups"); await gotoAndWaitForAppReady(page, "/backups");
await expect(page.getByText("Test Backup", { exact: true })).toHaveCount(0); await expect(page.getByText("Test Backup", { exact: true })).toHaveCount(0);
}); });

10
e2e/helpers/page.ts Normal file
View file

@ -0,0 +1,10 @@
import type { Page } from "@playwright/test";
export const waitForAppReady = async (page: Page) => {
await page.waitForFunction(() => document.body?.getAttribute("data-app-ready") === "true");
};
export const gotoAndWaitForAppReady = async (page: Page, url: string) => {
await page.goto(url, { waitUntil: "domcontentloaded" });
await waitForAppReady(page);
};

View file

@ -11,7 +11,7 @@ export default defineConfig({
use: { use: {
baseURL: `http://${process.env.SERVER_IP}:4096`, baseURL: `http://${process.env.SERVER_IP}:4096`,
video: "retain-on-failure", video: "retain-on-failure",
trace: "on-first-retry", trace: "retain-on-failure",
}, },
projects: [ projects: [
{ {