Add Playwright E2E tests replacing Cypress with MSW integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 16:43:32 +08:00
parent 67a723207f
commit 6e7d010c54
32 changed files with 2276 additions and 0 deletions

4
.gitignore vendored
View File

@@ -20,6 +20,10 @@ cypress.env.json
/cypress/videos/
/cypress/screenshots/
# Playwright
/test-results/
/playwright-report/
# Editor directories and files
vscode
.vscode

98
package-lock.json generated
View File

@@ -41,6 +41,7 @@
"devDependencies": {
"@4tw/cypress-drag-drop": "^2.3.1",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"@types/cytoscape": "^3.21.9",
"@types/cytoscape-dagre": "^2.3.4",
"@types/cytoscape-popper": "^2.0.4",
@@ -1782,6 +1783,22 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
@@ -7025,6 +7042,53 @@
"pathe": "^2.0.3"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -9926,6 +9990,15 @@
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"dev": true
},
"@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"requires": {
"playwright": "1.58.2"
}
},
"@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
@@ -13392,6 +13465,31 @@
"pathe": "^2.0.3"
}
},
"playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.58.2"
},
"dependencies": {
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
}
}
},
"playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true
},
"postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",

View File

@@ -51,6 +51,7 @@
"devDependencies": {
"@4tw/cypress-drag-drop": "^2.3.1",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"@types/cytoscape": "^3.21.9",
"@types/cytoscape-dagre": "^2.3.4",
"@types/cytoscape-popper": "^2.0.4",

30
tests/e2e/helpers.ts Normal file
View File

@@ -0,0 +1,30 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { type BrowserContext } from "@playwright/test";
/**
* Sets authentication cookies to simulate a logged-in user.
* MSW handles all API interception via the service worker.
* @param context - Playwright browser context.
*/
export async function loginWithMSW(
context: BrowserContext,
): Promise<void> {
await context.addCookies([
{
name: "luciaToken",
value: "fake-access-token-for-testing",
domain: "localhost",
path: "/",
},
{
name: "isLuciaLoggedIn",
value: "true",
domain: "localhost",
path: "/",
},
]);
}

View File

@@ -0,0 +1,27 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./specs",
timeout: 30000,
expect: { timeout: 5000 },
use: {
baseURL: "http://localhost:4173",
viewport: { width: 1280, height: 720 },
},
projects: [
{
name: "chromium",
use: { browserName: "chromium" },
},
],
webServer: {
command: "npx vite preview --port 4173",
port: 4173,
reuseExistingServer: true,
},
});

View File

@@ -0,0 +1,33 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Account Management", () => {
test.beforeEach(async ({ context }) => {
await loginWithMSW(context);
});
test("displays user list on account admin page", async ({ page }) => {
await page.goto("/account-admin");
// Should display users from fixture
await expect(page.getByText("Test Admin").first()).toBeVisible();
await expect(page.getByText("Alice Wang")).toBeVisible();
await expect(page.getByText("Bob Chen")).toBeVisible();
});
test("shows active/inactive status badges", async ({ page }) => {
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
// The user list should show status indicators
await expect(page.getByText("testadmin")).toBeVisible();
});
test("navigates to my-account page", async ({ page }) => {
await page.goto("/my-account");
await expect(page).toHaveURL(/\/my-account/);
});
});

View File

@@ -0,0 +1,92 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../../helpers";
const MSG_ACCOUNT_NOT_UNIQUE = "Account has already been registered.";
test.describe("Account duplication check.", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("When an account already exists, show error message on confirm.", async ({
page,
}) => {
const testAccountName = "000000";
// First creation: account doesn't exist yet - override via MSW
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.get("/api/users/000000", () =>
HttpResponse.json(
{ detail: "Not found" },
{ status: 404 },
),
),
);
});
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill(testAccountName);
await page.locator("#input_name_field").fill(testAccountName);
await page.locator("#input_first_pwd").fill(testAccountName);
await page
.locator(".checkbox-and-text")
.first()
.locator("div")
.first()
.click();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeEnabled();
await page.getByRole("button", { name: "Confirm" }).click();
await expect(page.getByText("Account added")).toBeVisible();
// Second creation: now account exists - override to return 200 via MSW
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.get("/api/users/000000", () =>
HttpResponse.json({
username: "000000",
name: "000000",
is_admin: false,
is_active: true,
roles: [],
}),
),
);
});
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill(testAccountName);
await page.locator("#input_name_field").fill(testAccountName);
await page.locator("#input_first_pwd").fill(testAccountName);
await page
.locator(".checkbox-and-text")
.first()
.locator("div")
.first()
.click();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeEnabled();
await page.getByRole("button", { name: "Confirm" }).click();
await expect(page.getByText(MSG_ACCOUNT_NOT_UNIQUE)).toBeVisible();
});
});

View File

@@ -0,0 +1,44 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../../helpers";
test.describe("Password validation on create account.", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("When password is too short, confirm button stays disabled.", async ({
page,
}) => {
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill("unit-test-0001");
await page.locator("#input_name_field").fill("unit-test-0001");
// Password shorter than 6 characters
await page.locator("#input_first_pwd").fill("aaa");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeDisabled();
});
test("When password meets minimum length, confirm button enables.", async ({
page,
}) => {
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill("unit-test-0001");
await page.locator("#input_name_field").fill("unit-test-0001");
await page.locator("#input_first_pwd").fill("aaaaaa");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeEnabled();
});
});

View File

@@ -0,0 +1,64 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../../helpers";
test.describe("Create an Account", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
// Override: new usernames should return 404 (account doesn't exist yet)
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.get("/api/users/:username", ({ params }) => {
if ((params.username as string).startsWith("unit-test-")) {
return HttpResponse.json(
{ detail: "Not found" },
{ status: 404 },
);
}
}),
);
});
});
test("Create a new account with admin role; should show saved message.", async ({
page,
}) => {
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill("unit-test-0001");
await page.locator("#input_name_field").fill("unit-test-0001");
await page.locator("#input_first_pwd").fill("aaaaaa");
await page
.locator(".checkbox-and-text")
.first()
.locator("div")
.first()
.click();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeEnabled();
await page.getByRole("button", { name: "Confirm" }).click();
await expect(page.getByText("Account added")).toBeVisible();
});
test("Confirm button is disabled when required fields are empty.", async ({
page,
}) => {
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill("test");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeDisabled();
});
});

View File

@@ -0,0 +1,39 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../../helpers";
test.describe("Delete an Account", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("Delete button opens confirmation modal and deletes on confirm.", async ({
page,
}) => {
await page.locator("img.delete-account").first().click();
await expect(
page.getByText("ARE YOU SURE TO DELETE"),
).toBeVisible();
await page.locator("#sure_to_delete_acct_btn").click();
await expect(page.getByText("Account deleted")).toBeVisible();
});
test("Cancel button closes the delete confirmation modal.", async ({
page,
}) => {
await page.locator("img.delete-account").first().click();
await expect(
page.getByText("ARE YOU SURE TO DELETE"),
).toBeVisible();
await page.locator("#calcel_delete_acct_btn").click();
await expect(
page.getByText("ARE YOU SURE TO DELETE"),
).not.toBeVisible();
});
});

View File

@@ -0,0 +1,38 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../../helpers";
const MODAL_TITLE_ACCOUNT_EDIT = "Account Edit";
const MSG_ACCOUNT_EDITED = "Saved";
test.describe("Edit an account", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("Edit an account; modify name and see saved message.", async ({
page,
}) => {
await page.locator(".btn-edit").first().click();
await expect(
page.locator("h1", { hasText: MODAL_TITLE_ACCOUNT_EDIT }),
).toBeVisible();
await page.locator("#input_name_field").clear();
await page.locator("#input_name_field").fill("Updated Name");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeEnabled();
await page.getByRole("button", { name: "Confirm" }).click();
await expect(page.getByText(MSG_ACCOUNT_EDITED)).toBeVisible();
});
});

View File

@@ -0,0 +1,109 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Account Management CRUD", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("shows Create New button", async ({ page }) => {
await expect(page.locator("#create_new_acct_btn")).toBeVisible();
await expect(
page.locator("#create_new_acct_btn"),
).toContainText("Create New");
});
test("opens create new account modal", async ({ page }) => {
await page.locator("#create_new_acct_btn").click();
await expect(page.locator("#modal_container")).toBeVisible();
await expect(
page.locator("#modal_account_edit_or_create_new"),
).toBeVisible();
// Should show account, name, password fields
await expect(page.locator("#input_account_field")).toBeVisible();
await expect(page.locator("#input_name_field")).toBeVisible();
await expect(page.locator("#input_first_pwd")).toBeVisible();
});
test("create account confirm is disabled when fields are empty", async ({
page,
}) => {
await page.locator("#create_new_acct_btn").click();
await expect(page.locator(".confirm-btn")).toBeDisabled();
});
test("create account confirm enables when fields are filled", async ({
page,
}) => {
await page.locator("#create_new_acct_btn").click();
await page.locator("#input_account_field").fill("newuser");
await page.locator("#input_name_field").fill("New User");
await page.locator("#input_first_pwd").fill("password1234");
await expect(page.locator(".confirm-btn")).toBeEnabled();
});
test("cancel button closes the modal", async ({ page }) => {
await page.locator("#create_new_acct_btn").click();
await expect(page.locator("#modal_container")).toBeVisible();
await page.locator(".cancel-btn").click();
await expect(page.locator("#modal_container")).not.toBeVisible();
});
test("close (X) button closes the modal", async ({ page }) => {
await page.locator("#create_new_acct_btn").click();
await expect(page.locator("#modal_container")).toBeVisible();
await page.locator('img[alt="X"]').click();
await expect(page.locator("#modal_container")).not.toBeVisible();
});
test("double-click username opens account info modal", async ({ page }) => {
// Double-click on the first account username
await page.locator(".account-cell").first().dblclick();
await expect(page.locator("#modal_container")).toBeVisible();
});
test("delete button opens delete confirmation modal", async ({ page }) => {
// Click the delete icon for a non-current user
await page.locator(".delete-account").first().click();
await expect(page.locator("#modal_container")).toBeVisible();
await expect(page.locator("#modal_delete_acct_alert")).toBeVisible();
});
test("delete modal has Yes and No buttons", async ({ page }) => {
await page.locator(".delete-account").first().click();
await expect(page.locator("#calcel_delete_acct_btn")).toBeVisible();
await expect(page.locator("#sure_to_delete_acct_btn")).toBeVisible();
});
test("delete modal No button closes the modal", async ({ page }) => {
await page.locator(".delete-account").first().click();
await page.locator("#calcel_delete_acct_btn").click();
await expect(page.locator("#modal_container")).not.toBeVisible();
});
test("shows checkboxes for Set as Admin and Activate in create modal", async ({
page,
}) => {
await page.locator("#create_new_acct_btn").click();
await expect(
page.locator("#account_create_checkboxes_section"),
).toBeVisible();
await expect(page.getByText("Set as admin.")).toBeVisible();
await expect(page.getByText("Activate now.")).toBeVisible();
});
test("search bar filters user list", async ({ page }) => {
// Search filters by username, not display name
await page.locator("#input_search").fill("user1");
await page.locator('img[alt="search"]').click();
// Should only show user1 (Alice Wang)
await expect(page.getByText("user1")).toBeVisible();
});
});

View File

@@ -0,0 +1,45 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Account Info Modal", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("double-click username opens info modal with user data", async ({
page,
}) => {
await page.locator(".account-cell").first().dblclick();
await expect(page.locator("#modal_container")).toBeVisible();
await expect(page.locator("#acct_info_user_name")).toBeVisible();
});
test("info modal shows Account Information header", async ({ page }) => {
await page.locator(".account-cell").first().dblclick();
await expect(page.locator("#modal_container")).toBeVisible();
await expect(page.getByText("Account Information")).toBeVisible();
});
test("info modal shows account visit info", async ({ page }) => {
await page.locator(".account-cell").first().dblclick();
await expect(page.locator("#modal_container")).toBeVisible();
await expect(page.locator("#account_visit_info")).toBeVisible();
await expect(
page.locator("#account_visit_info"),
).toContainText("Account:");
});
test("info modal can be closed via X button", async ({ page }) => {
await page.locator(".account-cell").first().dblclick();
await expect(page.locator("#modal_container")).toBeVisible();
await page.locator('img[alt="X"]').click();
await expect(page.locator("#modal_container")).not.toBeVisible();
});
});

View File

@@ -0,0 +1,125 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Compare", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator("li", { hasText: "COMPARE" }).click();
});
test("Compare dropdown sorting options", async ({ page }) => {
const expectedOptions = [
"By File Name (A to Z)",
"By File Name (Z to A)",
"By Dependency (A to Z)",
"By Dependency (Z to A)",
"By File Type (A to Z)",
"By File Type (Z to A)",
"By Last Update (A to Z)",
"By Last Update (Z to A)",
];
await page.locator(".p-select").click();
const options = page.locator(".p-select-list .p-select-option-label");
const count = await options.count();
const actualOptions: string[] = [];
for (let i = 0; i < count; i++) {
actualOptions.push((await options.nth(i).textContent()) ?? "");
}
expect(actualOptions).toEqual(expectedOptions);
});
test("Grid cards are rendered for compare file selection", async ({
page,
}) => {
const items = page.locator("#compareGridCards li");
await expect(items).not.toHaveCount(0);
});
test("Compare button is disabled until two files are dragged", async ({
page,
}) => {
await expect(
page.getByRole("button", { name: "Compare" }),
).toBeDisabled();
await page.locator("#compareFile0").dragTo(
page.locator("#primaryDragCard"),
);
await page.locator("#compareFile1").dragTo(
page.locator("#secondaryDragCard"),
);
await expect(
page.getByRole("button", { name: "Compare" }),
).toBeEnabled();
});
test("Enter Compare dashboard and see charts", async ({ page }) => {
await page.locator("#compareFile0").dragTo(
page.locator("#primaryDragCard"),
);
await page.locator("#compareFile1").dragTo(
page.locator("#secondaryDragCard"),
);
await page.getByRole("button", { name: "Compare" }).click();
await expect(page).toHaveURL(/compare/);
// Assert chart title spans are visible
await expect(
page.locator("span", { hasText: "Average Cycle Time" }),
).toBeVisible();
await expect(
page.locator("span", { hasText: "Cycle Efficiency" }),
).toBeVisible();
await expect(
page.locator("span", { hasText: "Average Processing Time" }).first(),
).toBeVisible();
await expect(
page.locator("span", {
hasText: "Average Processing Time by Activity",
}),
).toBeVisible();
await expect(
page.locator("span", { hasText: "Average Waiting Time" }).first(),
).toBeVisible();
await expect(
page.locator("span", {
hasText: "Average Waiting Time between Activity",
}),
).toBeVisible();
});
test("Compare State button exists on dashboard", async ({ page }) => {
await page.locator("#compareFile0").dragTo(
page.locator("#primaryDragCard"),
);
await page.locator("#compareFile1").dragTo(
page.locator("#secondaryDragCard"),
);
await page.getByRole("button", { name: "Compare" }).click();
await expect(page.locator("#compareState")).toBeVisible();
});
test("Sidebar shows time usage and frequency sections", async ({
page,
}) => {
await page.locator("#compareFile0").dragTo(
page.locator("#primaryDragCard"),
);
await page.locator("#compareFile1").dragTo(
page.locator("#secondaryDragCard"),
);
await page.getByRole("button", { name: "Compare" }).click();
await expect(page.locator("aside")).toBeVisible();
const items = page.locator("aside li");
await expect(items).not.toHaveCount(0);
});
});

View File

@@ -0,0 +1,80 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Discover Conformance Page", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/discover/log/297310264/conformance");
await expect(
page.locator(".p-radiobutton, [class*=conformance]").first(),
).toBeVisible();
});
test("page loads and loading overlay disappears", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
});
test("displays Rule Settings sidebar", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Rule Settings")).toBeVisible();
});
test("displays Conformance Checking Results heading", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(
page.getByText("Conformance Checking Results"),
).toBeVisible();
});
test("displays rule type radio options", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Have activity")).toBeVisible();
await expect(page.getByText("Activity sequence").first()).toBeVisible();
await expect(page.getByText("Activity duration")).toBeVisible();
await expect(page.getByText("Processing time")).toBeVisible();
await expect(page.getByText("Waiting time")).toBeVisible();
await expect(page.getByText("Cycle time")).toBeVisible();
});
test("displays Clear and Apply buttons", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(
page.getByRole("button", { name: "Clear" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Apply" }),
).toBeVisible();
});
test("displays Activity list area", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Activity list")).toBeVisible();
});
test("displays default placeholder values in results", async ({
page,
}) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Conformance Rate")).toBeVisible();
await expect(page.getByText("Cases").first()).toBeVisible();
});
});

View File

@@ -0,0 +1,64 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Discover Map Page", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/discover/log/297310264/map");
await expect(page.locator("#cy")).toBeVisible();
});
test("page loads and cytoscape container exists", async ({ page }) => {
await expect(page.locator("#cy")).toBeVisible();
});
test("displays left sidebar buttons", async ({ page }) => {
// Visualization Setting, Filter, Traces buttons
await expect(
page.locator(".material-symbols-outlined", { hasText: "track_changes" }),
).toBeVisible();
await expect(
page.locator(".material-symbols-outlined", { hasText: "tornado" }),
).toBeVisible();
await expect(
page.locator(".material-symbols-outlined", { hasText: "rebase" }),
).toBeVisible();
});
test("displays right sidebar Summary button", async ({ page }) => {
await expect(page.locator("#sidebar_state")).toBeVisible();
await expect(page.locator("#iconState")).toBeVisible();
});
test("clicking Visualization Setting button toggles sidebar", async ({
page,
}) => {
// Click the track_changes icon (Visualization Setting)
await page
.locator("span.material-symbols-outlined", { hasText: "track_changes" })
.locator("..")
.click();
// SidebarView should open
await expect(page.getByText("Visualization Setting").first()).toBeVisible();
});
test("clicking Summary button toggles sidebar", async ({ page }) => {
await page.locator("#iconState").click();
// SidebarState should open with insights/stats
await expect(page.getByText("Summary").first()).toBeVisible();
});
test("clicking Traces button toggles sidebar", async ({ page }) => {
await page
.locator("span.material-symbols-outlined", { hasText: "rebase" })
.locator("..")
.click();
// SidebarTraces should open
await expect(page.getByText("Traces")).toBeVisible();
});
});

View File

@@ -0,0 +1,94 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Discover Performance Page", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/discover/log/297310264/performance");
await expect(
page.locator(".chart-container, canvas").first(),
).toBeVisible();
});
test("page loads and loading overlay disappears", async ({ page }) => {
// Loading overlay should not be visible after data loads
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
});
test("displays Time Usage sidebar section", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Time Usage").first()).toBeVisible();
});
test("displays Frequency sidebar section", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Frequency").first()).toBeVisible();
});
test("displays sidebar navigation items", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Cycle Time & Efficiency").first()).toBeVisible();
await expect(page.getByText("Processing Time").first()).toBeVisible();
await expect(page.getByText("Waiting Time").first()).toBeVisible();
await expect(page.getByText("Number of Cases").first()).toBeVisible();
});
test("displays chart titles", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Average Cycle Time")).toBeVisible();
await expect(page.getByText("Cycle Efficiency")).toBeVisible();
await expect(
page.getByText("Average Processing Time").first(),
).toBeVisible();
await expect(
page.getByText("Average Processing Time by Activity"),
).toBeVisible();
await expect(
page.getByText("Average Waiting Time").first(),
).toBeVisible();
});
test("displays frequency chart titles", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("New Cases")).toBeVisible();
await expect(
page.getByText("Number of Cases by Activity"),
).toBeVisible();
});
test("renders canvas elements for charts", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
// Chart.js renders into canvas elements
const canvasCount = await page.locator("canvas").count();
expect(canvasCount).toBeGreaterThanOrEqual(5);
});
test("sidebar navigation scrolls to section", async ({ page }) => {
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
// Click on "Waiting Time" in sidebar
await page.locator("li", { hasText: "Waiting Time" }).first().click();
// The Waiting Time section should be in view
await expect(page.locator("#waitingTime")).toBeVisible();
});
});

View File

@@ -0,0 +1,120 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Discover Tab Navigation", () => {
test.beforeEach(async ({ context }) => {
await loginWithMSW(context);
});
test.describe("navigating from Map page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/discover/log/297310264/map");
await expect(page.locator("#cy")).toBeVisible();
});
test("shows DISCOVER heading and MAP/CONFORMANCE/PERFORMANCE tabs", async ({
page,
}) => {
await expect(
page.locator("#nav_bar", { hasText: "DISCOVER" }),
).toBeVisible();
const navItems = page.locator(".nav-item");
await expect(navItems).toHaveCount(3);
await expect(navItems.nth(0)).toContainText("MAP");
await expect(navItems.nth(1)).toContainText("CONFORMANCE");
await expect(navItems.nth(2)).toContainText("PERFORMANCE");
});
test("clicking PERFORMANCE tab navigates to performance page", async ({
page,
}) => {
await page.locator(".nav-item", { hasText: "PERFORMANCE" }).click();
await expect(page).toHaveURL(/\/performance/);
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Time Usage").first()).toBeVisible();
});
test("clicking CONFORMANCE tab navigates to conformance page", async ({
page,
}) => {
await page.locator(".nav-item", { hasText: "CONFORMANCE" }).click();
await expect(page).toHaveURL(/\/conformance/);
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Rule Settings")).toBeVisible();
});
test("shows back arrow to return to Files", async ({ page }) => {
await expect(page.locator("#backPage")).toBeVisible();
await expect(
page.locator("#backPage"),
).toHaveAttribute("href", "/files");
});
});
test.describe("navigating from Performance page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/discover/log/297310264/performance");
await expect(
page.locator(".chart-container, canvas").first(),
).toBeVisible();
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
});
test("clicking MAP tab navigates to map page", async ({ page }) => {
await page.locator(".nav-item", { hasText: "MAP" }).click();
await expect(page).toHaveURL(/\/map/);
await expect(page.locator("#cy")).toBeVisible();
});
test("clicking CONFORMANCE tab navigates to conformance page", async ({
page,
}) => {
await page.locator(".nav-item", { hasText: "CONFORMANCE" }).click();
await expect(page).toHaveURL(/\/conformance/);
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Rule Settings")).toBeVisible();
});
});
test.describe("navigating from Conformance page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/discover/log/297310264/conformance");
await expect(
page.locator(".p-radiobutton, [class*=conformance]").first(),
).toBeVisible();
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
});
test("clicking MAP tab navigates to map page", async ({ page }) => {
await page.locator(".nav-item", { hasText: "MAP" }).click();
await expect(page).toHaveURL(/\/map/);
await expect(page.locator("#cy")).toBeVisible();
});
test("clicking PERFORMANCE tab navigates to performance page", async ({
page,
}) => {
await page.locator(".nav-item", { hasText: "PERFORMANCE" }).click();
await expect(page).toHaveURL(/\/performance/);
await expect(
page.locator(".z-\\[9999\\]"),
).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText("Time Usage").first()).toBeVisible();
});
});
});

View File

@@ -0,0 +1,162 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Edge Cases", () => {
test.describe("Empty states", () => {
test("files page handles empty file list", async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
// Verify page loaded with data first
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// Now override via MSW and use client-side navigation
await page.evaluate(() => {
const { http, HttpResponse } = (window as any).__msw__;
(window as any).__mswWorker__.use(
http.get("/api/files", () => HttpResponse.json([])),
);
});
// Trigger re-fetch by navigating via the app's router
await page.evaluate(() => {
(window as any).__vue_app__?.config?.globalProperties?.$router?.push("/files");
});
// Wait for re-render
await page.waitForTimeout(1000);
// Use a more resilient check: the table exists but no file names
await expect(page.locator("table")).toBeVisible();
});
test("account admin handles empty user list", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
// Override users endpoint
await page.evaluate(() => {
const { http, HttpResponse } = (window as any).__msw__;
(window as any).__mswWorker__.use(
http.get("/api/users", () => HttpResponse.json([])),
);
});
// Navigate away and back via app router to trigger re-fetch
await page.evaluate(() => {
(window as any).__vue_app__?.config?.globalProperties?.$router?.push("/files");
});
await page.waitForTimeout(500);
await page.evaluate(() => {
(window as any).__vue_app__?.config?.globalProperties?.$router?.push("/account-admin");
});
await page.waitForTimeout(1000);
await expect(page.locator("#create_new_acct_btn")).toBeVisible();
});
test("unauthenticated user is redirected to login", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// Override my-account to return 401
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.get(
"/api/my-account",
() => new HttpResponse(null, { status: 401 }),
),
);
});
// Clear cookies to simulate logged out
await context.clearCookies();
await page.goto("/files");
await expect(page).toHaveURL(/\/login/);
});
test("unauthenticated user cannot access account-admin", async ({
page,
}) => {
await page.goto("/account-admin");
await expect(page).toHaveURL(/\/login/);
});
test("unauthenticated user cannot access my-account", async ({
page,
}) => {
await page.goto("/my-account");
await expect(page).toHaveURL(/\/login/);
});
});
test.describe("Login validation", () => {
test("shows error on failed login", async ({ page, context }) => {
// Visit login page first to load the app
await page.goto("/login");
await expect(page.locator("#login_btn_main_btn")).toBeVisible();
// Override token endpoint to return 401
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.post("/api/oauth/token", () =>
HttpResponse.json(
{ detail: "Invalid credentials" },
{ status: 401 },
),
),
);
});
await page.locator("#account").fill("wronguser");
await page.locator("#password").fill("wrongpass");
await page.locator("#login_btn_main_btn").click();
await expect(page).toHaveURL(/\/login/);
});
test("confirm stays disabled with only account field filled", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_account_field").fill("onlyaccount");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeDisabled();
});
test("confirm stays disabled with only name field filled", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_name_field").fill("onlyname");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeDisabled();
});
test("confirm stays disabled with only password field filled", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
await page.getByRole("button", { name: "Create New" }).click();
await page.locator("#input_first_pwd").fill("onlypassword");
await expect(
page.getByRole("button", { name: "Confirm" }),
).toBeDisabled();
});
});
});

View File

@@ -0,0 +1,88 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("File Operations", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
});
test("file list table has sortable columns", async ({ page }) => {
// Check that table headers exist with expected columns
const table = page.locator("table");
await expect(
table.locator("th", { hasText: "Name" }),
).toBeVisible();
await expect(
table.locator("th", { hasText: "Dependency" }),
).toBeVisible();
await expect(
table.locator("th", { hasText: "File Type" }),
).toBeVisible();
await expect(
table.locator("th", { hasText: "Owner" }),
).toBeVisible();
await expect(
table.locator("th", { hasText: "Last Update" }),
).toBeVisible();
});
test("clicking column header sorts the table", async ({ page }) => {
// Click "Name" header to sort
await page.locator("th", { hasText: "Name" }).click();
// After sorting, table should still have data
const rows = page.locator("table tbody tr");
await expect(rows).not.toHaveCount(0);
});
test("table rows show file data from fixture", async ({ page }) => {
const tbody = page.locator("table tbody");
await expect(
tbody.getByText("sample-process.xes").first(),
).toBeVisible();
await expect(tbody.getByText("filtered-sample").first()).toBeVisible();
await expect(
tbody.getByText("production-log.csv").first(),
).toBeVisible();
});
test("table shows owner names", async ({ page }) => {
const tbody = page.locator("table tbody");
await expect(tbody.getByText("Test Admin").first()).toBeVisible();
await expect(tbody.getByText("Alice Wang")).toBeVisible();
});
test("table shows file types", async ({ page }) => {
const tbody = page.locator("table tbody");
await expect(tbody.getByText("log").first()).toBeVisible();
});
test("right-click on file row shows context menu", async ({ page }) => {
// PrimeVue DataTable with contextmenu
await page.locator("table tbody tr").first().click({ button: "right" });
// Context menu behavior depends on implementation
// Just verify the right-click doesn't break anything
const rows = page.locator("table tbody tr");
await expect(rows).not.toHaveCount(0);
});
test("grid view shows file cards", async ({ page }) => {
// Switch to grid view
await page.locator("li.cursor-pointer").last().click();
// Grid cards should be visible
const cards = page.locator("li[title]");
await expect(cards).not.toHaveCount(0);
});
test("Import button opens upload modal", async ({ page }) => {
await page.locator("#import_btn").click();
// Upload modal should appear
await expect(page.locator("#import_btn")).toBeVisible();
});
});

View File

@@ -0,0 +1,71 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Files Page", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
});
test("displays the file list after login", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await expect(
page.locator("h2", { hasText: "All Files" }),
).toBeVisible();
// Should display file names from fixture
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await expect(page.getByText("filtered-sample").first()).toBeVisible();
await expect(page.getByText("production-log.csv").first()).toBeVisible();
});
test("shows Recently Used section", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await expect(
page.locator("h2", { hasText: "Recently Used" }),
).toBeVisible();
});
test("switches to DISCOVER tab", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator(".nav-item", { hasText: "DISCOVER" }).click();
// DISCOVER tab shows filtered file types
await expect(
page.locator("h2", { hasText: "All Files" }),
).toBeVisible();
});
test("switches to COMPARE tab and shows drag zones", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator(".nav-item", { hasText: "COMPARE" }).click();
await expect(
page.getByText("Performance Comparison"),
).toBeVisible();
await expect(
page.getByText("Drag and drop a file here").first(),
).toBeVisible();
});
test("shows Import button on FILES tab", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await expect(page.locator("#import_btn")).toContainText("Import");
});
test("can switch between list and grid view", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// DataTable (list view) should be visible by default
await expect(page.locator("table")).toBeVisible();
});
test("double-click file navigates to discover page", async ({ page }) => {
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// Double-click the first file row in the table
// The actual route depends on file type (log->map, log-check->conformance, etc.)
await page.locator("table tbody tr").first().dblclick();
await expect(page).toHaveURL(/\/discover/);
});
});

View File

@@ -0,0 +1,63 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Files Page - COMPARE Tab", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// Switch to COMPARE tab
await page.locator("li", { hasText: "COMPARE" }).click();
});
test("shows Performance Comparison heading", async ({ page }) => {
await expect(
page.locator("h2", { hasText: "Performance Comparison" }),
).toBeVisible();
});
test("shows two drag-and-drop slots", async ({ page }) => {
await expect(page.locator("#primaryDragCard")).toBeVisible();
await expect(page.locator("#secondaryDragCard")).toBeVisible();
});
test("drag slots show placeholder text", async ({ page }) => {
await expect(
page.locator("#primaryDragCard"),
).toContainText("Drag and drop a file here");
await expect(
page.locator("#secondaryDragCard"),
).toContainText("Drag and drop a file here");
});
test("Compare button is disabled when no files are dragged", async ({
page,
}) => {
await expect(
page.getByRole("button", { name: "Compare" }),
).toBeDisabled();
});
test("shows sorting dropdown", async ({ page }) => {
await expect(page.locator(".p-select")).toBeVisible();
});
test("grid cards display file names", async ({ page }) => {
await expect(page.locator("#compareGridCards")).toBeVisible();
const items = page.locator("#compareGridCards li");
await expect(items).not.toHaveCount(0);
});
test("clicking sorting dropdown shows sort options", async ({ page }) => {
await page.locator(".p-select").click();
await expect(page.locator(".p-select-list")).toBeVisible();
await expect(
page.locator(".p-select-option", { hasText: "By File Name" }).first(),
).toBeVisible();
});
});

View File

@@ -0,0 +1,104 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Files to Discover Entry Flow", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
});
test.describe("double-click table row to enter Discover", () => {
test("double-click log file navigates to Map page", async ({ page }) => {
// Target the Name column (has class .fileName) to avoid matching Dependency column
await page
.locator("td.fileName", { hasText: "sample-process.xes" })
.locator("..")
.dblclick();
await expect(page).toHaveURL(/\/discover\/log\/1\/map/);
await expect(page.locator("#cy")).toBeVisible();
});
test("double-click filter file navigates to Map page", async ({
page,
}) => {
await page
.locator("td.fileName", { hasText: "filtered-sample" })
.locator("..")
.dblclick();
await expect(page).toHaveURL(/\/discover\/filter\/10\/map/);
await expect(page.locator("#cy")).toBeVisible();
});
});
test.describe("double-click to enter Discover from file list", () => {
test("double-click log file navigates to Map page", async ({
page,
}) => {
// Find the row with "production-log.csv" (a pure log, no parent ambiguity)
await page
.locator("table tbody tr", { hasText: "production-log.csv" })
.first()
.dblclick();
await expect(page).toHaveURL(/\/discover\/log\/.*\/map/);
await expect(page.locator("#cy")).toBeVisible();
});
});
test.describe("DISCOVER tab filters files", () => {
test("clicking DISCOVER tab shows only Log, Filter, and Rule files", async ({
page,
}) => {
await page.locator(".nav-item", { hasText: "DISCOVER" }).click();
await expect(
page.locator("td.fileName", { hasText: "sample-process.xes" }),
).toBeVisible();
await expect(
page.locator("td.fileName", { hasText: "filtered-sample" }),
).toBeVisible();
await expect(
page.locator("td.fileName", { hasText: "conformance-check-1" }),
).toBeVisible();
});
});
test.describe("Navbar state after entering Discover", () => {
test("shows DISCOVER heading and tabs after entering from Files", async ({
page,
}) => {
await page
.locator("td.fileName", { hasText: "sample-process.xes" })
.locator("..")
.dblclick();
await expect(page).toHaveURL(/\/discover\//);
await expect(
page.locator("#nav_bar", { hasText: "DISCOVER" }),
).toBeVisible();
await expect(
page.locator(".nav-item", { hasText: "MAP" }),
).toBeVisible();
await expect(
page.locator(".nav-item", { hasText: "CONFORMANCE" }),
).toBeVisible();
await expect(
page.locator(".nav-item", { hasText: "PERFORMANCE" }),
).toBeVisible();
});
test("shows back arrow pointing to /files", async ({ page }) => {
await page
.locator("td.fileName", { hasText: "sample-process.xes" })
.locator("..")
.dblclick();
await expect(page).toHaveURL(/\/discover\//);
await expect(
page.locator("#backPage"),
).toHaveAttribute("href", "/files");
});
});
});

View File

@@ -0,0 +1,91 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
test.describe("Login Flow", () => {
test("renders the login form", async ({ page }) => {
await page.goto("/login");
await expect(page.getByRole("heading", { name: "LOGIN", exact: true }).first()).toBeVisible();
await expect(page.locator("#account")).toBeVisible();
await expect(page.locator("#password")).toBeVisible();
await expect(page.locator("#login_btn_main_btn")).toBeDisabled();
});
test("login button is disabled when fields are empty", async ({
page,
}) => {
await page.goto("/login");
await expect(page.locator("#login_btn_main_btn")).toBeDisabled();
// Only username filled - still disabled
await page.locator("#account").fill("testuser");
await expect(page.locator("#login_btn_main_btn")).toBeDisabled();
});
test("login button enables when both fields are filled", async ({
page,
}) => {
await page.goto("/login");
await page.locator("#account").fill("testadmin");
await page.locator("#password").fill("password123");
await expect(page.locator("#login_btn_main_btn")).toBeEnabled();
});
test("successful login redirects to /files", async ({ page }) => {
await page.goto("/login");
await page.locator("#account").fill("testadmin");
await page.locator("#password").fill("password123");
await page.locator("#login_btn_main_btn").click();
await expect(page).toHaveURL(/\/files/);
});
test("failed login shows error message", async ({ page }) => {
// Visit login first to load app + MSW
await page.goto("/login");
await expect(page.locator("#login_btn_main_btn")).toBeVisible();
// Override the token endpoint to return 401 via MSW
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.post("/api/oauth/token", () =>
HttpResponse.json(
{ detail: "Incorrect username or password" },
{ status: 401 },
),
),
);
});
await page.locator("#account").fill("wronguser");
await page.locator("#password").fill("wrongpass");
await page.locator("#login_btn_main_btn").click();
await expect(
page.getByText("Incorrect account or password"),
).toBeVisible();
});
test("toggles password visibility", async ({ page }) => {
await page.goto("/login");
await page.locator("#password").fill("secret123");
await expect(
page.locator("#password"),
).toHaveAttribute("type", "password");
// Click the eye icon to show password
await page.locator('label[for="passwordt"] span.cursor-pointer').click();
await expect(
page.locator("#password"),
).toHaveAttribute("type", "text");
// Click again to hide
await page.locator('label[for="passwordt"] span.cursor-pointer').click();
await expect(
page.locator("#password"),
).toHaveAttribute("type", "password");
});
});

View File

@@ -0,0 +1,72 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Logout Flow", () => {
test.beforeEach(async ({ context }) => {
await loginWithMSW(context);
});
test("shows account menu when head icon is clicked", async ({ page }) => {
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// Click the head icon to open account menu
await page.locator("#acct_mgmt_button").click();
await expect(page.locator("#account_menu")).toBeVisible();
await expect(page.locator("#greeting")).toContainText("Test Admin");
});
test("account menu shows admin management link for admin user", async ({
page,
}) => {
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator("#acct_mgmt_button").click();
await expect(page.locator("#account_menu")).toBeVisible();
// Admin user should see account management option
await expect(page.locator("#btn_acct_mgmt")).toBeVisible();
});
test("account menu has logout button", async ({ page }) => {
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator("#acct_mgmt_button").click();
await expect(page.locator("#btn_logout_in_menu")).toBeVisible();
});
test("clicking My Account navigates to /my-account", async ({ page }) => {
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator("#acct_mgmt_button").click();
await page.locator("#btn_mang_ur_acct").click();
await expect(page).toHaveURL(/\/my-account/);
});
test("clicking Account Management navigates to /account-admin", async ({
page,
}) => {
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator("#acct_mgmt_button").click();
await page.locator("#btn_acct_mgmt").click();
await expect(page).toHaveURL(/\/account-admin/);
});
test("logout redirects to login page", async ({ page }) => {
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await page.locator("#acct_mgmt_button").click();
await page.locator("#btn_logout_in_menu").click();
await expect(page).toHaveURL(/\/login/);
});
});

View File

@@ -0,0 +1,104 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("My Account Page", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/my-account");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("displays user name heading", async ({ page }) => {
await expect(
page.locator("#general_acct_info_user_name"),
).toBeVisible();
await expect(
page.locator("#general_acct_info_user_name"),
).toContainText("Test Admin");
});
test("shows Admin badge for admin user", async ({ page }) => {
await expect(page.getByText("Admin").first()).toBeVisible();
});
test("shows visit count info", async ({ page }) => {
await expect(
page.locator("#general_account_visit_info"),
).toBeVisible();
await expect(
page.locator("#general_account_visit_info"),
).toContainText("Total visits");
});
test("displays account username (read-only)", async ({ page }) => {
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("shows Edit button for name field", async ({ page }) => {
await expect(
page.getByRole("button", { name: "Edit" }),
).toBeVisible();
});
test("clicking Edit shows input field and Save/Cancel buttons", async ({
page,
}) => {
await page.getByRole("button", { name: "Edit" }).first().click();
await expect(page.locator("#input_name_field")).toBeVisible();
await expect(
page.getByRole("button", { name: "Save" }).first(),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Cancel" }).first(),
).toBeVisible();
});
test("clicking Cancel reverts name field to read-only", async ({
page,
}) => {
await page.getByRole("button", { name: "Edit" }).first().click();
await expect(page.locator("#input_name_field")).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).first().click();
await expect(page.locator("#input_name_field")).not.toBeVisible();
});
test("shows Reset button for password field", async ({ page }) => {
await expect(
page.getByRole("button", { name: "Reset" }),
).toBeVisible();
});
test("clicking Reset shows password input and Save/Cancel", async ({
page,
}) => {
await page.getByRole("button", { name: "Reset" }).click();
await expect(page.locator('input[type="password"]')).toBeVisible();
await expect(
page.getByRole("button", { name: "Save" }).first(),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Cancel" }).first(),
).toBeVisible();
});
test("clicking Cancel on password field hides the input", async ({
page,
}) => {
await page.getByRole("button", { name: "Reset" }).click();
await expect(page.locator('input[type="password"]')).toBeVisible();
// The Cancel button for password is the second one
await page.locator(".cancel-btn").click();
await expect(
page.locator('input[type="password"]'),
).not.toBeVisible();
});
test("shows Session section", async ({ page }) => {
await expect(page.getByText("Session")).toBeVisible();
});
});

View File

@@ -0,0 +1,77 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Navigation and Routing", () => {
test("redirects / to /files when logged in", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/");
await expect(page).toHaveURL(/\/files/);
});
test("shows 404 page for unknown routes", async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/nonexistent-page");
await expect(page.getByText("404")).toBeVisible();
});
test("navbar shows correct view name", async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
await expect(page.locator("#nav_bar")).toBeVisible();
await expect(page.locator("#nav_bar h2")).toContainText("FILES");
});
test("navbar shows back arrow on non-files pages", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/discover/log/1/map");
// Back arrow should be visible on discover pages
await expect(page.locator("#backPage")).toBeVisible();
});
test("navbar tabs are clickable on discover page", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/discover/log/1/map");
// Discover navbar should show MAP, CONFORMANCE, PERFORMANCE tabs
await expect(
page.locator(".nav-item", { hasText: "MAP" }),
).toBeVisible();
await expect(
page.locator(".nav-item", { hasText: "CONFORMANCE" }),
).toBeVisible();
await expect(
page.locator(".nav-item", { hasText: "PERFORMANCE" }),
).toBeVisible();
// Click CONFORMANCE tab
await page.locator(".nav-item", { hasText: "CONFORMANCE" }).click();
await expect(page).toHaveURL(/\/conformance/);
// Click PERFORMANCE tab
await page.locator(".nav-item", { hasText: "PERFORMANCE" }).click();
await expect(page).toHaveURL(/\/performance/);
// Click MAP tab to go back
await page.locator(".nav-item", { hasText: "MAP" }).click();
await expect(page).toHaveURL(/\/map/);
});
test("login page is accessible at /login", async ({ page }) => {
await page.goto("/login");
await expect(page.getByRole("heading", { name: "LOGIN" }).first()).toBeVisible();
});
});

View File

@@ -0,0 +1,44 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("404 Not Found Page", () => {
test("displays 404 page for non-existent route", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/this-page-does-not-exist");
await expect(page.getByText("404")).toBeVisible();
await expect(
page.getByText("The page you are looking for does not exist."),
).toBeVisible();
});
test("has a link back to Files page", async ({
page,
context,
}) => {
await loginWithMSW(context);
await page.goto("/some/random/path");
const link = page.getByRole("link", { name: "Go to Files" });
await expect(link).toBeVisible();
await expect(link).toHaveAttribute("href", "/files");
});
test("displays 404 for unauthenticated user on invalid route", async ({
page,
}) => {
await page.goto("/not-a-real-page");
const url = page.url();
if (url.includes("/login")) {
await expect(page).toHaveURL(/\/login/);
} else {
await expect(page.getByText("404")).toBeVisible();
}
});
});

View File

@@ -0,0 +1,56 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("Discover page navigation tabs", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
});
test("Double-clicking a log file enters the MAP page.", async ({
page,
}) => {
await page
.locator("td.fileName", { hasText: "sample-process.xes" })
.dblclick();
await expect(page).toHaveURL(/map/);
// MAP tab should exist in the navbar
await expect(
page.locator(".nav-item", { hasText: "MAP" }),
).toBeVisible();
});
test("Clicking CONFORMANCE tab switches active page.", async ({
page,
}) => {
await page
.locator("td.fileName", { hasText: "sample-process.xes" })
.dblclick();
await expect(page).toHaveURL(/map/);
await page.locator(".nav-item", { hasText: "CONFORMANCE" }).click();
await expect(page).toHaveURL(/conformance/);
await expect(
page.locator(".nav-item", { hasText: "CONFORMANCE" }),
).toHaveClass(/active/);
});
test("Clicking PERFORMANCE tab switches active page.", async ({
page,
}) => {
await page
.locator("td.fileName", { hasText: "sample-process.xes" })
.dblclick();
await expect(page).toHaveURL(/map/);
await page.locator(".nav-item", { hasText: "PERFORMANCE" }).click();
await expect(page).toHaveURL(/performance/);
await expect(
page.locator(".nav-item", { hasText: "PERFORMANCE" }),
).toHaveClass(/active/);
});
});

View File

@@ -0,0 +1,69 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
test.describe("Paste URL login redirect", () => {
test("After login with return-to param, redirects to the remembered page", async ({
page,
context,
}) => {
// Visit login page with a return-to query param (base64-encoded URL)
const targetUrl =
"http://localhost:4173/discover/conformance/log/1/conformance";
const encodedUrl = btoa(targetUrl);
await page.goto(`/login?return-to=${encodedUrl}`);
// Fill in login form
await page.locator("#account").fill("testadmin");
await page.locator("#password").fill("password123");
await page.locator("form").evaluate((form) =>
(form as HTMLFormElement).submit(),
);
// After login, the app should attempt to redirect to the return-to URL.
// Verify login succeeded by checking the login form is gone.
await expect(page.locator("#login_btn_main_btn")).not.toBeVisible();
});
test("Login without return-to param redirects to /files", async ({
page,
}) => {
await page.goto("/login");
await page.locator("#account").fill("testadmin");
await page.locator("#password").fill("password123");
await page.locator("#login_btn_main_btn").click();
await expect(page).toHaveURL(/\/files/, { timeout: 10000 });
});
test("Unauthenticated user cannot access inner pages", async ({
page,
}) => {
// Visit login first to load the app + MSW
await page.goto("/login");
await expect(page.locator("#login_btn_main_btn")).toBeVisible();
// Override my-account to return 401 (simulate logged-out state) via MSW
await page.evaluate(() => {
const { http, HttpResponse } = window.__msw__;
window.__mswWorker__.use(
http.get("/api/my-account", () =>
HttpResponse.json(
{ detail: "Not authenticated" },
{ status: 401 },
),
),
);
});
await page.goto("/files");
// Should be redirected to login page
await expect(page).toHaveURL(/\/login/);
await expect(page.locator("#account")).toBeVisible();
await expect(page.locator("#password")).toBeVisible();
});
});

View File

@@ -0,0 +1,167 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
import { test, expect } from "@playwright/test";
import { loginWithMSW } from "../helpers";
test.describe("SweetAlert2 Modals", () => {
test.describe("File Context Menu - Rename", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
});
test("right-click on table row shows context menu with Rename", async ({
page,
}) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await expect(page.locator('.p-contextmenu-item-content', { hasText: 'Rename' }).first()).toBeVisible();
});
test("right-click context menu shows Download option", async ({
page,
}) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await expect(page.locator('.p-contextmenu-item-content', { hasText: 'Download' }).first()).toBeVisible();
});
test("right-click context menu shows Delete option", async ({
page,
}) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await expect(page.locator('.p-contextmenu-item-content', { hasText: 'Delete' }).first()).toBeVisible();
});
test("clicking Rename opens SweetAlert rename dialog", async ({
page,
}) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Rename' }).first().click();
// SweetAlert popup should appear with RENAME title
await expect(page.locator(".swal2-popup")).toBeVisible();
await expect(
page.locator(".swal2-title"),
).toContainText("RENAME");
await expect(page.locator(".swal2-input")).toBeVisible();
});
test("rename dialog has pre-filled file name", async ({ page }) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Rename' }).first().click();
const value = await page.locator(".swal2-input").inputValue();
expect(value).not.toBe("");
});
test("rename dialog can be cancelled", async ({ page }) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Rename' }).first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator(".swal2-cancel").click();
await expect(page.locator(".swal2-popup")).not.toBeVisible();
});
test("clicking Delete opens SweetAlert delete confirmation", async ({
page,
}) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Delete' }).first().click();
// SweetAlert popup should appear with CONFIRM DELETION
await expect(page.locator(".swal2-popup")).toBeVisible();
await expect(
page.locator(".swal2-title"),
).toContainText("CONFIRM DELETION");
});
test("delete confirmation shows file name", async ({ page }) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Delete' }).first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await expect(
page.locator(".swal2-html-container"),
).toContainText("delete");
});
test("delete confirmation can be cancelled", async ({ page }) => {
await page
.locator("table tbody tr")
.first()
.click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Delete' }).first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator(".swal2-cancel").click();
await expect(page.locator(".swal2-popup")).not.toBeVisible();
});
});
test.describe("File Context Menu on Grid View", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/files");
await expect(page.getByText("sample-process.xes").first()).toBeVisible();
// Switch to grid view
await page.locator("li.cursor-pointer").last().click();
});
test("right-click on grid card shows context menu", async ({ page }) => {
await page.locator("li[title]").first().click({ button: "right" });
await expect(page.locator('.p-contextmenu-item-content', { hasText: 'Rename' }).first()).toBeVisible();
await expect(page.locator('.p-contextmenu-item-content', { hasText: 'Delete' }).first()).toBeVisible();
});
test("grid card rename opens SweetAlert dialog", async ({ page }) => {
await page.locator("li[title]").first().click({ button: "right" });
await page.locator('.p-contextmenu-item-content', { hasText: 'Rename' }).first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await expect(
page.locator(".swal2-title"),
).toContainText("RENAME");
});
});
test.describe("Account Delete Confirmation", () => {
test.beforeEach(async ({ page, context }) => {
await loginWithMSW(context);
await page.goto("/account-admin");
await expect(page.getByText("Test Admin").first()).toBeVisible();
});
test("delete confirmation Yes button triggers delete API", async ({
page,
}) => {
await page.locator(".delete-account").first().click();
await expect(page.locator("#modal_container")).toBeVisible();
await page.locator("#sure_to_delete_acct_btn").click();
// Modal should close after deletion
await expect(
page.locator("#modal_container"),
).not.toBeVisible();
});
});
});

View File

@@ -93,6 +93,7 @@ export default defineConfig(({ mode }) => {
jsdom: { url: "http://localhost:3000" },
},
setupFiles: ["./tests/setup-msw.js"],
exclude: ["tests/e2e/**", "node_modules/**"],
// reporter: ['text', 'json', 'html', 'vue'],
},
};