test(e2e): playwright smoke — login/create/edit/save happy path

This commit is contained in:
Claude
2026-08-28 16:37:26 +00:00
parent 03de45cf8e
commit 294bd57317
4 changed files with 84 additions and 0 deletions

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@ frontend/dist/
frontend/.vite/ frontend/.vite/
frontend/playwright-report/ frontend/playwright-report/
frontend/test-results/ frontend/test-results/
frontend/.pw-data/
# SPA build output — .gitkeep tracks the folder, everything inside is generated # SPA build output — .gitkeep tracks the folder, everything inside is generated
src/VolcanicCore.Api/wwwroot/* src/VolcanicCore.Api/wwwroot/*

13
frontend/e2e/fixtures.ts Normal file
View File

@@ -0,0 +1,13 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default async function globalSetup() {
const dataDir = path.resolve(__dirname, "../.pw-data");
// Shell out to `rm -rf` — Node's fs.rm can trip ENOTEMPTY on overlay
// filesystems (rootless containers, docker layered volumes) where the
// dir looks empty via readdir but rmdir still refuses.
spawnSync("rm", ["-rf", dataDir], { stdio: "ignore" });
}

View File

@@ -0,0 +1,27 @@
import { test, expect } from "@playwright/test";
test("login → create workflow → drop node → save", async ({ page }) => {
await page.goto("/login");
await page.getByLabel(/password/i).fill("hunter2");
await page.getByRole("button", { name: /sign in/i }).click();
await expect(page).toHaveURL(/\/workflows$/);
await page.getByRole("button", { name: /new workflow/i }).click();
await page.getByPlaceholder(/name/i).fill("Smoke Test");
await page.getByRole("button", { name: /^create$/i }).click();
await expect(page.getByText("Smoke Test")).toBeVisible();
await page.getByText("Smoke Test").click();
// On editor. Drag the Webhook tile onto the canvas.
const tile = page.locator("aside").filter({ hasText: /Nodes/i }).getByText("Webhook");
const canvas = page.locator(".react-flow__pane");
await tile.dragTo(canvas);
await expect(page.locator(".react-flow__node")).toHaveCount(1);
await page.getByRole("button", { name: /save/i }).click();
// No visible confirmation — reload and assert the node persisted.
await page.reload();
await expect(page.locator(".react-flow__node")).toHaveCount(1);
});

View File

@@ -0,0 +1,43 @@
import { defineConfig, devices } from "@playwright/test";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dataDir = path.resolve(__dirname, "./.pw-data");
const apiPort = 5099;
const uiPort = 5173;
export default defineConfig({
testDir: "./e2e",
timeout: 60_000,
fullyParallel: false,
workers: 1,
reporter: "list",
use: {
baseURL: `http://127.0.0.1:${uiPort}`,
trace: "off",
...devices["Desktop Chrome"],
},
webServer: [
{
// Fresh DB per run — the data dir is wiped in globalSetup.
command: `dotnet run --project ../src/VolcanicCore.Api --urls http://127.0.0.1:${apiPort}`,
cwd: __dirname,
env: {
ADMIN_PASSWORD: "hunter2",
DATA_DIRECTORY: dataDir,
},
url: `http://127.0.0.1:${apiPort}/api/health`,
timeout: 120_000,
reuseExistingServer: false,
},
{
command: `pnpm dev --host 127.0.0.1 --port ${uiPort}`,
cwd: __dirname,
url: `http://127.0.0.1:${uiPort}`,
timeout: 60_000,
reuseExistingServer: false,
},
],
globalSetup: "./e2e/fixtures.ts",
});