Add Playwright E2E tests replacing Cypress with MSW integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
30
tests/e2e/helpers.ts
Normal file
30
tests/e2e/helpers.ts
Normal 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: "/",
|
||||
},
|
||||
]);
|
||||
}
|
||||
27
tests/e2e/playwright.config.ts
Normal file
27
tests/e2e/playwright.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
33
tests/e2e/specs/accountAdmin.spec.ts
Normal file
33
tests/e2e/specs/accountAdmin.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
92
tests/e2e/specs/accountAdmin/accountDuplicationCheck.spec.ts
Normal file
92
tests/e2e/specs/accountAdmin/accountDuplicationCheck.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
44
tests/e2e/specs/accountAdmin/confirmPasswordMessage.spec.ts
Normal file
44
tests/e2e/specs/accountAdmin/confirmPasswordMessage.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
64
tests/e2e/specs/accountAdmin/createAccount.spec.ts
Normal file
64
tests/e2e/specs/accountAdmin/createAccount.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
39
tests/e2e/specs/accountAdmin/deleteAccount.spec.ts
Normal file
39
tests/e2e/specs/accountAdmin/deleteAccount.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
38
tests/e2e/specs/accountAdmin/editAccount.spec.ts
Normal file
38
tests/e2e/specs/accountAdmin/editAccount.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
109
tests/e2e/specs/accountCrud.spec.ts
Normal file
109
tests/e2e/specs/accountCrud.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
45
tests/e2e/specs/accountInfo.spec.ts
Normal file
45
tests/e2e/specs/accountInfo.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
125
tests/e2e/specs/compare.spec.ts
Normal file
125
tests/e2e/specs/compare.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
80
tests/e2e/specs/discoverConformance.spec.ts
Normal file
80
tests/e2e/specs/discoverConformance.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
64
tests/e2e/specs/discoverMap.spec.ts
Normal file
64
tests/e2e/specs/discoverMap.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
94
tests/e2e/specs/discoverPerformance.spec.ts
Normal file
94
tests/e2e/specs/discoverPerformance.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
120
tests/e2e/specs/discoverTabs.spec.ts
Normal file
120
tests/e2e/specs/discoverTabs.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
162
tests/e2e/specs/edgeCases.spec.ts
Normal file
162
tests/e2e/specs/edgeCases.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
88
tests/e2e/specs/fileOperations.spec.ts
Normal file
88
tests/e2e/specs/fileOperations.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
71
tests/e2e/specs/files.spec.ts
Normal file
71
tests/e2e/specs/files.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
63
tests/e2e/specs/filesCompare.spec.ts
Normal file
63
tests/e2e/specs/filesCompare.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
104
tests/e2e/specs/filesToDiscover.spec.ts
Normal file
104
tests/e2e/specs/filesToDiscover.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
91
tests/e2e/specs/login.spec.ts
Normal file
91
tests/e2e/specs/login.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
72
tests/e2e/specs/logout.spec.ts
Normal file
72
tests/e2e/specs/logout.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
104
tests/e2e/specs/myAccount.spec.ts
Normal file
104
tests/e2e/specs/myAccount.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
77
tests/e2e/specs/navigation.spec.ts
Normal file
77
tests/e2e/specs/navigation.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
44
tests/e2e/specs/notFound404.spec.ts
Normal file
44
tests/e2e/specs/notFound404.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
56
tests/e2e/specs/pageAdmin.spec.ts
Normal file
56
tests/e2e/specs/pageAdmin.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
69
tests/e2e/specs/pasteUrlLoginRedirect.spec.ts
Normal file
69
tests/e2e/specs/pasteUrlLoginRedirect.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
167
tests/e2e/specs/sweetAlertModals.spec.ts
Normal file
167
tests/e2e/specs/sweetAlertModals.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user