Compare commits

...

10 Commits

Author SHA1 Message Date
imacat f828bd0423 Remove Cypress and update scripts to use Playwright for E2E testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:43:38 +08:00
imacat 6e7d010c54 Add Playwright E2E tests replacing Cypress with MSW integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:43:18 +08:00
imacat 67a723207f Remove old Cypress fixture files now served by MSW handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:30:38 +08:00
imacat 3d1de913f8 Migrate Cypress E2E from cy.intercept to MSW service worker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:30:22 +08:00
imacat b978071f94 Add conditional MSW browser worker startup for E2E testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 08:24:33 +08:00
imacat 3918755b7c Migrate Vitest store tests from vi.mock to MSW request handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 07:48:53 +08:00
imacat 7e052f0d36 Add MSW infrastructure: handlers, fixtures, request-log, and Vitest setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 07:38:02 +08:00
imacat 0ff03ec0ef Suppress expected console.error output in error-path tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:05:04 +08:00
imacat 0af0ff39d4 Remove unused cytoscape-spread and split build into manual chunks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:39:05 +08:00
imacat aeb6d207c5 Export store interfaces and fix dotenv code block to resolve TypeDoc warnings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:26:29 +08:00
110 changed files with 4273 additions and 5561 deletions
+3 -4
View File
@@ -15,10 +15,9 @@ coverage
*.local
/dist
# Cypress
cypress.env.json
/cypress/videos/
/cypress/screenshots/
# Playwright
/test-results/
/playwright-report/
# Editor directories and files
vscode
+1 -1
View File
@@ -54,7 +54,7 @@ Copy `.env` to `.env.local` and set the backend API URL:
cp .env .env.local
```
```dotenv
```sh
# .env.local
VUE_APP_API_URL = "http://localhost:8000"
```
-25
View File
@@ -1,25 +0,0 @@
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/8/12
// imacat.yang@dsp.im (imacat), 2026/3/5
/**
* @module cypress.config
* Cypress E2E test configuration with viewport
* settings and base URL.
*/
const { defineConfig } = require("cypress");
module.exports = defineConfig({
defaultCommandTimeout: 6000,
viewportWidth: 1280,
viewportHeight: 720,
e2e: {
baseUrl: "http://localhost:4173",
specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}",
},
includeShadowDom: true,
env: {},
});
-34
View File
@@ -1,34 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Account Management", () => {
beforeEach(() => {
loginWithFixtures();
});
it("displays user list on account admin page", () => {
cy.visit("/account-admin");
cy.wait("@getUsers");
// Should display users from fixture
cy.contains("Test Admin").should("exist");
cy.contains("Alice Wang").should("exist");
cy.contains("Bob Chen").should("exist");
});
it("shows active/inactive status badges", () => {
cy.visit("/account-admin");
cy.wait("@getUsers");
// The user list should show status indicators
cy.contains("testadmin").should("exist");
});
it("navigates to my-account page", () => {
cy.visit("/my-account");
cy.wait("@getMyAccount");
cy.url().should("include", "/my-account");
});
});
@@ -1,64 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/07/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../../support/intercept";
const MSG_ACCOUNT_NOT_UNIQUE = "Account has already been registered.";
describe("Account duplication check.", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("When an account already exists, show error message on confirm.", () => {
const testAccountName = "000000";
// First creation: account doesn't exist yet
cy.intercept("GET", "/api/users/000000", {
statusCode: 404,
body: { detail: "Not found" },
}).as("checkNewUser");
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type(testAccountName);
cy.get("#input_name_field").type(testAccountName);
cy.get("#input_first_pwd").type(testAccountName);
cy.get(".checkbox-and-text").first().find("div").first().click();
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.wait("@postUser");
cy.contains("Account added").should("be.visible");
// Second creation: now account exists — override to return 200
cy.intercept("GET", "/api/users/000000", {
statusCode: 200,
body: {
username: "000000",
name: "000000",
is_admin: false,
is_active: true,
roles: [],
},
}).as("checkExistingUser");
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type(testAccountName);
cy.get("#input_name_field").type(testAccountName);
cy.get("#input_first_pwd").type(testAccountName);
cy.get(".checkbox-and-text").first().find("div").first().click();
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.contains(MSG_ACCOUNT_NOT_UNIQUE).should("be.visible");
});
});
@@ -1,36 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/07/02
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../../support/intercept";
describe("Password validation on create account.", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("When password is too short, confirm button stays disabled.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type("unit-test-0001");
cy.get("#input_name_field").type("unit-test-0001");
// Password shorter than 6 characters
cy.get("#input_first_pwd").type("aaa");
cy.contains("button", "Confirm").should("be.disabled");
});
it("When password meets minimum length, confirm button enables.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type("unit-test-0001");
cy.get("#input_name_field").type("unit-test-0001");
cy.get("#input_first_pwd").type("aaaaaa");
cy.contains("button", "Confirm").should("be.enabled");
});
});
@@ -1,42 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/07/02
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../../support/intercept";
describe("Create an Account", () => {
beforeEach(() => {
loginWithFixtures();
// Override: new usernames should return 404 (account doesn't exist yet)
cy.intercept("GET", "/api/users/unit-test-*", {
statusCode: 404,
body: { detail: "Not found" },
}).as("checkNewUser");
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("Create a new account with admin role; should show saved message.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type("unit-test-0001");
cy.get("#input_name_field").type("unit-test-0001");
cy.get("#input_first_pwd").type("aaaaaa");
cy.get(".checkbox-and-text").first().find("div").first().click();
cy.contains("button", "Confirm")
.should("be.visible")
.and("be.enabled")
.click();
cy.wait("@postUser");
cy.contains("Account added").should("be.visible");
});
it("Confirm button is disabled when required fields are empty.", () => {
cy.contains("button", "Create New").should("be.visible").click();
cy.get("#input_account_field").type("test");
cy.contains("button", "Confirm").should("be.disabled");
});
});
@@ -1,30 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/07/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../../support/intercept";
describe("Delete an Account", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("Delete button opens confirmation modal and deletes on confirm.", () => {
cy.get("img.delete-account").first().click();
cy.contains("ARE YOU SURE TO DELETE").should("be.visible");
cy.get("#sure_to_delete_acct_btn").click();
cy.wait("@deleteUser");
cy.contains("Account deleted").should("be.visible");
});
it("Cancel button closes the delete confirmation modal.", () => {
cy.get("img.delete-account").first().click();
cy.contains("ARE YOU SURE TO DELETE").should("be.visible");
cy.get("#calcel_delete_acct_btn").click();
cy.contains("ARE YOU SURE TO DELETE").should("not.exist");
});
});
@@ -1,32 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/07/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../../support/intercept";
const MODAL_TITLE_ACCOUNT_EDIT = "Account Edit";
const MSG_ACCOUNT_EDITED = "Saved";
describe("Edit an account", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("Edit an account; modify name and see saved message.", () => {
cy.get(".btn-edit").first().click();
cy.wait("@getUserDetail");
cy.contains("h1", MODAL_TITLE_ACCOUNT_EDIT).should("exist");
cy.get("#input_name_field").clear();
cy.get("#input_name_field").type("Updated Name");
cy.contains("button", "Confirm").should("be.visible").and("be.enabled");
cy.contains("button", "Confirm").click();
cy.wait("@putUser");
cy.contains(MSG_ACCOUNT_EDITED).should("be.visible");
});
});
-96
View File
@@ -1,96 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Account Management CRUD", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("shows Create New button", () => {
cy.get("#create_new_acct_btn").should("exist");
cy.get("#create_new_acct_btn").should("contain", "Create New");
});
it("opens create new account modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#modal_container").should("be.visible");
cy.get("#modal_account_edit_or_create_new").should("be.visible");
// Should show account, name, password fields
cy.get("#input_account_field").should("exist");
cy.get("#input_name_field").should("exist");
cy.get("#input_first_pwd").should("exist");
});
it("create account confirm is disabled when fields are empty", () => {
cy.get("#create_new_acct_btn").click();
cy.get(".confirm-btn").should("be.disabled");
});
it("create account confirm enables when fields are filled", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#input_account_field").type("newuser");
cy.get("#input_name_field").type("New User");
cy.get("#input_first_pwd").type("password1234");
cy.get(".confirm-btn").should("not.be.disabled");
});
it("cancel button closes the modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#modal_container").should("be.visible");
cy.get(".cancel-btn").click();
cy.get("#modal_container").should("not.exist");
});
it("close (X) button closes the modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#modal_container").should("be.visible");
cy.get('img[alt="X"]').click();
cy.get("#modal_container").should("not.exist");
});
it("double-click username opens account info modal", () => {
// Double-click on the first account username
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
});
it("delete button opens delete confirmation modal", () => {
// Click the delete icon for a non-current user
cy.get(".delete-account").first().click();
cy.get("#modal_container").should("be.visible");
cy.get("#modal_delete_acct_alert").should("be.visible");
});
it("delete modal has Yes and No buttons", () => {
cy.get(".delete-account").first().click();
cy.get("#calcel_delete_acct_btn").should("exist");
cy.get("#sure_to_delete_acct_btn").should("exist");
});
it("delete modal No button closes the modal", () => {
cy.get(".delete-account").first().click();
cy.get("#calcel_delete_acct_btn").click();
cy.get("#modal_container").should("not.exist");
});
it("shows checkboxes for Set as Admin and Activate in create modal", () => {
cy.get("#create_new_acct_btn").click();
cy.get("#account_create_checkboxes_section").should("be.visible");
cy.contains("Set as admin.").should("exist");
cy.contains("Activate now.").should("exist");
});
it("search bar filters user list", () => {
// Search filters by username, not display name
cy.get("#input_search").type("user1");
cy.get('img[alt="search"]').click();
// Should only show user1 (Alice Wang)
cy.contains("user1").should("exist");
});
});
-40
View File
@@ -1,40 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Account Info Modal", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("double-click username opens info modal with user data", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.get("#acct_info_user_name").should("exist");
});
it("info modal shows Account Information header", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.contains("Account Information").should("exist");
});
it("info modal shows account visit info", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.get("#account_visit_info").should("exist");
cy.get("#account_visit_info").should("contain", "Account:");
});
it("info modal can be closed via X button", () => {
cy.get(".account-cell").first().dblclick();
cy.get("#modal_container").should("be.visible");
cy.get('img[alt="X"]').click();
cy.get("#modal_container").should("not.exist");
});
});
-90
View File
@@ -1,90 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2024/02/22
// cindy.chang@dsp.im (Cindy Chang), 2024/05/30
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Compare", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
cy.contains("li", "COMPARE").click();
});
it("Compare dropdown sorting options", () => {
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)",
];
cy.get(".p-select").click();
cy.get(".p-select-list")
.find(".p-select-option")
.then(($options) => {
const actualOptions = $options
.map((index, elem) =>
Cypress.$(elem).find(".p-select-option-label").text(),
)
.get();
expect(actualOptions).to.deep.equal(expectedOptions);
});
});
it("Grid cards are rendered for compare file selection", () => {
cy.get("#compareGridCards").find("li").should("have.length.greaterThan", 0);
});
it("Compare button is disabled until two files are dragged", () => {
cy.contains("button", "Compare").should("be.disabled");
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").should("be.enabled");
});
it("Enter Compare dashboard and see charts", () => {
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").click();
cy.wait("@getCompare");
cy.url().should("include", "compare");
// Assert chart title spans are visible
cy.contains("span", "Average Cycle Time").should("exist");
cy.contains("span", "Cycle Efficiency").should("exist");
cy.contains("span", "Average Processing Time").should("exist");
cy.contains("span", "Average Processing Time by Activity").should("exist");
cy.contains("span", "Average Waiting Time").should("exist");
cy.contains("span", "Average Waiting Time between Activity").should(
"exist",
);
});
it("Compare State button exists on dashboard", () => {
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").click();
cy.wait("@getCompare");
cy.get("#compareState").should("exist").and("be.visible");
});
it("Sidebar shows time usage and frequency sections", () => {
cy.get("#compareFile0").drag("#primaryDragCard");
cy.get("#compareFile1").drag("#secondaryDragCard");
cy.contains("button", "Compare").click();
cy.wait("@getCompare");
cy.get("aside").should("exist");
cy.get("aside li").should("have.length.greaterThan", 0);
});
});
-55
View File
@@ -1,55 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Discover Conformance Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/discover/log/297310264/conformance");
cy.wait("@getLogCheckParams");
});
it("page loads and loading overlay disappears", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
});
it("displays Rule Settings sidebar", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Rule Settings").should("be.visible");
});
it("displays Conformance Checking Results heading", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Conformance Checking Results").should("be.visible");
});
it("displays rule type radio options", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Have activity").should("be.visible");
cy.contains("Activity sequence").should("be.visible");
cy.contains("Activity duration").should("be.visible");
cy.contains("Processing time").should("be.visible");
cy.contains("Waiting time").should("be.visible");
cy.contains("Cycle time").should("be.visible");
});
it("displays Clear and Apply buttons", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("button", "Clear").should("be.visible");
cy.contains("button", "Apply").should("exist");
});
it("displays Activity list area", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Activity list").should("be.visible");
});
it("displays default placeholder values in results", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Conformance Rate").should("be.visible");
cy.contains("Cases").should("be.visible");
});
});
-55
View File
@@ -1,55 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Discover Map Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/discover/log/297310264/map");
cy.wait("@getDiscover");
});
it("page loads and cytoscape container exists", () => {
cy.get("#cy").should("exist");
});
it("displays left sidebar buttons", () => {
// Visualization Setting, Filter, Traces buttons
cy.get(".material-symbols-outlined")
.contains("track_changes")
.should("exist");
cy.get(".material-symbols-outlined").contains("tornado").should("exist");
cy.get(".material-symbols-outlined").contains("rebase").should("exist");
});
it("displays right sidebar Summary button", () => {
cy.get("#sidebar_state").should("exist");
cy.get("#iconState").should("exist");
});
it("clicking Visualization Setting button toggles sidebar", () => {
// Click the track_changes icon (Visualization Setting)
cy.contains("span.material-symbols-outlined", "track_changes")
.parent("li")
.click();
// SidebarView should open
cy.contains("Visualization Setting").should("be.visible");
});
it("clicking Summary button toggles sidebar", () => {
cy.get("#iconState").click();
// SidebarState should open with insights/stats
cy.contains("Summary").should("be.visible");
});
it("clicking Traces button toggles sidebar", () => {
cy.contains("span.material-symbols-outlined", "rebase")
.parent("li")
.click();
// SidebarTraces should open
cy.contains("Traces").should("be.visible");
});
});
-66
View File
@@ -1,66 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Discover Performance Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/discover/log/297310264/performance");
cy.wait("@getPerformance");
});
it("page loads and loading overlay disappears", () => {
// Loading overlay should not be visible after data loads
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
});
it("displays Time Usage sidebar section", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Time Usage").should("be.visible");
});
it("displays Frequency sidebar section", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Frequency").should("be.visible");
});
it("displays sidebar navigation items", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Cycle Time & Efficiency").should("be.visible");
cy.contains("Processing Time").should("be.visible");
cy.contains("Waiting Time").should("be.visible");
cy.contains("Number of Cases").should("be.visible");
});
it("displays chart titles", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Average Cycle Time").should("be.visible");
cy.contains("Cycle Efficiency").should("be.visible");
cy.contains("Average Processing Time").should("be.visible");
cy.contains("Average Processing Time by Activity").should("be.visible");
cy.contains("Average Waiting Time").should("be.visible");
});
it("displays frequency chart titles", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("New Cases").should("be.visible");
cy.contains("Number of Cases by Activity").should("be.visible");
});
it("renders canvas elements for charts", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
// Chart.js renders into canvas elements
cy.get("canvas").should("have.length.at.least", 5);
});
it("sidebar navigation scrolls to section", () => {
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
// Click on "Waiting Time" in sidebar
cy.contains("li", "Waiting Time").click();
// The Waiting Time section should be in view
cy.get("#waitingTime").should("exist");
});
});
-94
View File
@@ -1,94 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Discover Tab Navigation", () => {
beforeEach(() => {
loginWithFixtures();
});
describe("navigating from Map page", () => {
beforeEach(() => {
cy.visit("/discover/log/297310264/map");
cy.wait("@getDiscover");
});
it("shows DISCOVER heading and MAP/CONFORMANCE/PERFORMANCE tabs", () => {
cy.get("#nav_bar").contains("DISCOVER").should("be.visible");
cy.get(".nav-item").should("have.length", 3);
cy.get(".nav-item").eq(0).should("contain", "MAP");
cy.get(".nav-item").eq(1).should("contain", "CONFORMANCE");
cy.get(".nav-item").eq(2).should("contain", "PERFORMANCE");
});
it("clicking PERFORMANCE tab navigates to performance page", () => {
cy.get(".nav-item").contains("PERFORMANCE").click();
cy.url().should("include", "/performance");
cy.wait("@getPerformance");
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Time Usage").should("be.visible");
});
it("clicking CONFORMANCE tab navigates to conformance page", () => {
cy.get(".nav-item").contains("CONFORMANCE").click();
cy.url().should("include", "/conformance");
cy.wait("@getLogCheckParams");
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Rule Settings").should("be.visible");
});
it("shows back arrow to return to Files", () => {
cy.get("#backPage").should("exist");
cy.get("#backPage").should("have.attr", "href", "/files");
});
});
describe("navigating from Performance page", () => {
beforeEach(() => {
cy.visit("/discover/log/297310264/performance");
cy.wait("@getPerformance");
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
});
it("clicking MAP tab navigates to map page", () => {
cy.get(".nav-item").contains("MAP").click();
cy.url().should("include", "/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
it("clicking CONFORMANCE tab navigates to conformance page", () => {
cy.get(".nav-item").contains("CONFORMANCE").click();
cy.url().should("include", "/conformance");
cy.wait("@getLogCheckParams");
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Rule Settings").should("be.visible");
});
});
describe("navigating from Conformance page", () => {
beforeEach(() => {
cy.visit("/discover/log/297310264/conformance");
cy.wait("@getLogCheckParams");
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
});
it("clicking MAP tab navigates to map page", () => {
cy.get(".nav-item").contains("MAP").click();
cy.url().should("include", "/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
it("clicking PERFORMANCE tab navigates to performance page", () => {
cy.get(".nav-item").contains("PERFORMANCE").click();
cy.url().should("include", "/performance");
cy.wait("@getPerformance");
cy.get(String.raw`.z-\[9999\]`, { timeout: 10000 }).should("not.exist");
cy.contains("Time Usage").should("be.visible");
});
});
});
-94
View File
@@ -1,94 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Edge Cases", () => {
describe("Empty states", () => {
it("files page handles empty file list", () => {
loginWithFixtures();
// Override files intercept with empty array
cy.intercept("GET", "/api/files", {
statusCode: 200,
body: [],
}).as("getEmptyFiles");
cy.visit("/files");
cy.wait("@getEmptyFiles");
// Table should exist but have no file data
cy.get("table").should("exist");
cy.contains("sample-process.xes").should("not.exist");
});
it("account admin handles empty user list", () => {
loginWithFixtures();
cy.intercept("GET", "/api/users", {
statusCode: 200,
body: [],
}).as("getEmptyUsers");
cy.visit("/account-admin");
cy.wait("@getEmptyUsers");
// Create New button should still work
cy.get("#create_new_acct_btn").should("exist");
});
});
describe("Authentication guard", () => {
it("unauthenticated user is redirected to login", () => {
// No loginWithFixtures - not logged in
cy.visit("/files");
cy.url().should("include", "/login");
});
it("unauthenticated user cannot access account-admin", () => {
cy.visit("/account-admin");
cy.url().should("include", "/login");
});
it("unauthenticated user cannot access my-account", () => {
cy.visit("/my-account");
cy.url().should("include", "/login");
});
});
describe("Login validation", () => {
it("shows error on failed login", () => {
cy.intercept("POST", "/api/oauth/token", {
statusCode: 401,
body: { detail: "Invalid credentials" },
}).as("failedLogin");
cy.visit("/login");
cy.get("#account").type("wrong");
cy.get("#password").type("wrong");
cy.get("form").submit();
cy.wait("@failedLogin");
// Should stay on login page
cy.url().should("include", "/login");
});
});
describe("Account creation validation", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
cy.get("#create_new_acct_btn").click();
});
it("confirm stays disabled with only account field filled", () => {
cy.get("#input_account_field").type("newuser");
cy.get(".confirm-btn").should("be.disabled");
});
it("confirm stays disabled with only name field filled", () => {
cy.get("#input_name_field").type("New User");
cy.get(".confirm-btn").should("be.disabled");
});
it("confirm stays disabled with only password field filled", () => {
cy.get("#input_first_pwd").type("password1234");
cy.get(".confirm-btn").should("be.disabled");
});
});
});
-74
View File
@@ -1,74 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("File Operations", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
});
it("file list table has sortable columns", () => {
// Check that table headers exist with expected columns
cy.get("table").within(() => {
cy.contains("th", "Name").should("exist");
cy.contains("th", "Dependency").should("exist");
cy.contains("th", "File Type").should("exist");
cy.contains("th", "Owner").should("exist");
cy.contains("th", "Last Update").should("exist");
});
});
it("clicking column header sorts the table", () => {
// Click "Name" header to sort
cy.contains("th", "Name").click();
// After sorting, table should still have data
cy.get("table tbody tr").should("have.length.greaterThan", 0);
});
it("table rows show file data from fixture", () => {
cy.get("table tbody").within(() => {
cy.contains("sample-process.xes").should("exist");
cy.contains("filtered-sample").should("exist");
cy.contains("production-log.csv").should("exist");
});
});
it("table shows owner names", () => {
cy.get("table tbody").within(() => {
cy.contains("Test Admin").should("exist");
cy.contains("Alice Wang").should("exist");
});
});
it("table shows file types", () => {
cy.get("table tbody").within(() => {
cy.contains("log").should("exist");
});
});
it("right-click on file row shows context menu", () => {
// PrimeVue DataTable with contextmenu
cy.get("table tbody tr").first().rightclick();
// Context menu behavior depends on implementation
// Just verify the right-click doesn't break anything
cy.get("table tbody tr").should("have.length.greaterThan", 0);
});
it("grid view shows file cards", () => {
// Switch to grid view
cy.get("svg").parent("li.cursor-pointer").last().click();
// Grid cards should be visible
cy.get("li[title]").should("have.length.greaterThan", 0);
});
it("Import button opens upload modal", () => {
cy.get("#import_btn").click();
// Upload modal should appear
cy.get("#import_btn").should("exist");
});
});
-60
View File
@@ -1,60 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Files Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
});
it("displays the file list after login", () => {
cy.wait("@getFiles");
cy.contains("h2", "All Files").should("exist");
// Should display file names from fixture
cy.contains("sample-process.xes").should("exist");
cy.contains("filtered-sample").should("exist");
cy.contains("production-log.csv").should("exist");
});
it("shows Recently Used section", () => {
cy.wait("@getFiles");
cy.contains("h2", "Recently Used").should("exist");
});
it("switches to DISCOVER tab", () => {
cy.wait("@getFiles");
cy.contains(".nav-item", "DISCOVER").click();
// DISCOVER tab shows filtered file types
cy.contains("h2", "All Files").should("exist");
});
it("switches to COMPARE tab and shows drag zones", () => {
cy.wait("@getFiles");
cy.contains(".nav-item", "COMPARE").click();
cy.contains("Performance Comparison").should("exist");
cy.contains("Drag and drop a file here").should("exist");
});
it("shows Import button on FILES tab", () => {
cy.wait("@getFiles");
cy.get("#import_btn").should("contain", "Import");
});
it("can switch between list and grid view", () => {
cy.wait("@getFiles");
// DataTable (list view) should be visible by default
cy.get("table").should("exist");
});
it("double-click file navigates to discover page", () => {
cy.wait("@getFiles");
// Double-click the first file row in the table
// The actual route depends on file type (log→map, log-check→conformance, etc.)
cy.get("table tbody tr").first().dblclick();
cy.url().should("include", "/discover");
});
});
-49
View File
@@ -1,49 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Files Page - COMPARE Tab", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
// Switch to COMPARE tab
cy.contains("li", "COMPARE").click();
});
it("shows Performance Comparison heading", () => {
cy.contains("h2", "Performance Comparison").should("be.visible");
});
it("shows two drag-and-drop slots", () => {
cy.get("#primaryDragCard").should("exist");
cy.get("#secondaryDragCard").should("exist");
});
it("drag slots show placeholder text", () => {
cy.get("#primaryDragCard").should("contain", "Drag and drop a file here");
cy.get("#secondaryDragCard").should("contain", "Drag and drop a file here");
});
it("Compare button is disabled when no files are dragged", () => {
cy.contains("button", "Compare").should("be.disabled");
});
it("shows sorting dropdown", () => {
cy.get(".p-select").should("exist");
});
it("grid cards display file names", () => {
cy.get("#compareGridCards").should("exist");
cy.get("#compareGridCards li").should("have.length.greaterThan", 0);
});
it("clicking sorting dropdown shows sort options", () => {
cy.get(".p-select").click();
cy.get(".p-select-list").should("be.visible");
cy.contains(".p-select-option", "By File Name").should("exist");
});
});
-72
View File
@@ -1,72 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Files to Discover Entry Flow", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
});
describe("double-click table row to enter Discover", () => {
it("double-click log file navigates to Map page", () => {
// Target the Name column (has class .fileName) to avoid matching Dependency column
cy.contains("td.fileName", "sample-process.xes").parent("tr").dblclick();
cy.url().should("include", "/discover/log/1/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
it("double-click filter file navigates to Map page", () => {
cy.contains("td.fileName", "filtered-sample").parent("tr").dblclick();
cy.url().should("include", "/discover/filter/10/map");
cy.wait("@getFilterDiscover");
cy.get("#cy").should("exist");
});
});
describe("double-click grid card to enter Discover", () => {
beforeEach(() => {
// Switch to grid view
cy.get("svg").parent("li.cursor-pointer").last().click();
});
it("double-click log file grid card navigates to Map page", () => {
// Use last() to target the All Files grid section (not Recently Used)
cy.get('li[title="sample-process.xes"]').last().dblclick();
cy.url().should("include", "/discover/log/1/map");
cy.wait("@getDiscover");
cy.get("#cy").should("exist");
});
});
describe("DISCOVER tab filters files", () => {
it("clicking DISCOVER tab shows only Log, Filter, and Rule files", () => {
cy.get(".nav-item").contains("DISCOVER").click();
cy.contains("td.fileName", "sample-process.xes").should("exist");
cy.contains("td.fileName", "filtered-sample").should("exist");
cy.contains("td.fileName", "conformance-check-1").should("exist");
});
});
describe("Navbar state after entering Discover", () => {
it("shows DISCOVER heading and tabs after entering from Files", () => {
cy.contains("td.fileName", "sample-process.xes").parent("tr").dblclick();
cy.url().should("include", "/discover/");
cy.get("#nav_bar").contains("DISCOVER").should("be.visible");
cy.get(".nav-item").contains("MAP").should("exist");
cy.get(".nav-item").contains("CONFORMANCE").should("exist");
cy.get(".nav-item").contains("PERFORMANCE").should("exist");
});
it("shows back arrow pointing to /files", () => {
cy.contains("td.fileName", "sample-process.xes").parent("tr").dblclick();
cy.url().should("include", "/discover/");
cy.get("#backPage").should("have.attr", "href", "/files");
});
});
});
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": [
"./**/*",
"../support/**/*",
"/node_modules/cypress",
"cypress/**/*.js",],
"experimentalShadowDomSupport": true
}
-76
View File
@@ -1,76 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { setupApiIntercepts } from "../support/intercept";
describe("Login Flow", () => {
beforeEach(() => {
setupApiIntercepts();
});
it("renders the login form", () => {
cy.visit("/login");
cy.get("h2").should("contain", "LOGIN");
cy.get("#account").should("exist");
cy.get("#password").should("exist");
cy.get("#login_btn_main_btn").should("be.disabled");
});
it("login button is disabled when fields are empty", () => {
cy.visit("/login");
cy.get("#login_btn_main_btn").should("be.disabled");
// Only username filled — still disabled
cy.get("#account").type("testuser");
cy.get("#login_btn_main_btn").should("be.disabled");
});
it("login button enables when both fields are filled", () => {
cy.visit("/login");
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("#login_btn_main_btn").should("not.be.disabled");
});
it("successful login redirects to /files", () => {
cy.visit("/login");
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("#login_btn_main_btn").click();
cy.wait("@postToken");
cy.url().should("include", "/files");
});
it("failed login shows error message", () => {
// Override the token intercept to return 401
cy.intercept("POST", "/api/oauth/token", {
statusCode: 401,
body: { detail: "Incorrect username or password" },
}).as("postTokenFail");
cy.visit("/login");
cy.get("#account").type("wronguser");
cy.get("#password").type("wrongpass");
cy.get("#login_btn_main_btn").click();
cy.wait("@postTokenFail");
cy.contains("Incorrect account or password").should("be.visible");
});
it("toggles password visibility", () => {
cy.visit("/login");
cy.get("#password").type("secret123");
cy.get("#password").should("have.attr", "type", "password");
// Click the eye icon to show password
cy.get('label[for="passwordt"] span.cursor-pointer').click();
cy.get("#password").should("have.attr", "type", "text");
// Click again to hide
cy.get('label[for="passwordt"] span.cursor-pointer').click();
cy.get("#password").should("have.attr", "type", "password");
});
});
-67
View File
@@ -1,67 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Logout Flow", () => {
beforeEach(() => {
loginWithFixtures();
});
it("shows account menu when head icon is clicked", () => {
cy.visit("/files");
cy.wait("@getFiles");
// Click the head icon to open account menu
cy.get("#acct_mgmt_button").click();
cy.get("#account_menu").should("be.visible");
cy.get("#greeting").should("contain", "Test Admin");
});
it("account menu shows admin management link for admin user", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#acct_mgmt_button").click();
cy.get("#account_menu").should("be.visible");
// Admin user should see account management option
cy.get("#btn_acct_mgmt").should("exist");
});
it("account menu has logout button", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#acct_mgmt_button").click();
cy.get("#btn_logout_in_menu").should("exist");
});
it("clicking My Account navigates to /my-account", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#acct_mgmt_button").click();
cy.get("#btn_mang_ur_acct").click();
cy.url().should("include", "/my-account");
});
it("clicking Account Management navigates to /account-admin", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#acct_mgmt_button").click();
cy.get("#btn_acct_mgmt").click();
cy.url().should("include", "/account-admin");
});
it("logout redirects to login page", () => {
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#acct_mgmt_button").click();
cy.get("#btn_logout_in_menu").click();
cy.url().should("include", "/login");
});
});
-73
View File
@@ -1,73 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("My Account Page", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/my-account");
cy.wait("@getUserDetail");
});
it("displays user name heading", () => {
cy.get("#general_acct_info_user_name").should("exist");
cy.get("#general_acct_info_user_name").should("contain", "Test Admin");
});
it("shows Admin badge for admin user", () => {
cy.contains("Admin").should("exist");
});
it("shows visit count info", () => {
cy.get("#general_account_visit_info").should("exist");
cy.get("#general_account_visit_info").should("contain", "Total visits");
});
it("displays account username (read-only)", () => {
cy.contains("Test Admin").should("exist");
});
it("shows Edit button for name field", () => {
cy.contains("button", "Edit").should("exist");
});
it("clicking Edit shows input field and Save/Cancel buttons", () => {
cy.contains("button", "Edit").first().click();
cy.get("#input_name_field").should("exist");
cy.contains("button", "Save").should("exist");
cy.contains("button", "Cancel").should("exist");
});
it("clicking Cancel reverts name field to read-only", () => {
cy.contains("button", "Edit").first().click();
cy.get("#input_name_field").should("exist");
cy.contains("button", "Cancel").click();
cy.get("#input_name_field").should("not.exist");
});
it("shows Reset button for password field", () => {
cy.contains("button", "Reset").should("exist");
});
it("clicking Reset shows password input and Save/Cancel", () => {
cy.contains("button", "Reset").click();
cy.get('input[type="password"]').should("exist");
cy.contains("button", "Save").should("exist");
cy.contains("button", "Cancel").should("exist");
});
it("clicking Cancel on password field hides the input", () => {
cy.contains("button", "Reset").click();
cy.get('input[type="password"]').should("exist");
// The Cancel button for password is the second one
cy.get(".cancel-btn").click();
cy.get('input[type="password"]').should("not.exist");
});
it("shows Session section", () => {
cy.contains("Session").should("exist");
});
});
-62
View File
@@ -1,62 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures, setupApiIntercepts } from "../support/intercept";
describe("Navigation and Routing", () => {
it("redirects / to /files when logged in", () => {
loginWithFixtures();
cy.visit("/");
cy.url().should("include", "/files");
});
it("shows 404 page for unknown routes", () => {
loginWithFixtures();
cy.visit("/nonexistent-page");
cy.contains("404").should("exist");
});
it("navbar shows correct view name", () => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
cy.get("#nav_bar").should("exist");
cy.get("#nav_bar h2").should("contain", "FILES");
});
it("navbar shows back arrow on non-files pages", () => {
loginWithFixtures();
cy.visit("/discover/log/1/map");
// Back arrow should be visible on discover pages
cy.get("#backPage").should("exist");
});
it("navbar tabs are clickable on discover page", () => {
loginWithFixtures();
cy.visit("/discover/log/1/map");
// Discover navbar should show MAP, CONFORMANCE, PERFORMANCE tabs
cy.contains(".nav-item", "MAP").should("exist");
cy.contains(".nav-item", "CONFORMANCE").should("exist");
cy.contains(".nav-item", "PERFORMANCE").should("exist");
// Click CONFORMANCE tab
cy.contains(".nav-item", "CONFORMANCE").click();
cy.url().should("include", "/conformance");
// Click PERFORMANCE tab
cy.contains(".nav-item", "PERFORMANCE").click();
cy.url().should("include", "/performance");
// Click MAP tab to go back
cy.contains(".nav-item", "MAP").click();
cy.url().should("include", "/map");
});
it("login page is accessible at /login", () => {
setupApiIntercepts();
cy.visit("/login");
cy.get("h2").should("contain", "LOGIN");
});
});
-36
View File
@@ -1,36 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("404 Not Found Page", () => {
it("displays 404 page for non-existent route", () => {
loginWithFixtures();
cy.visit("/this-page-does-not-exist");
cy.contains("404").should("be.visible");
cy.contains("The page you are looking for does not exist.").should(
"be.visible",
);
});
it("has a link back to Files page", () => {
loginWithFixtures();
cy.visit("/some/random/path");
cy.contains("a", "Go to Files")
.should("be.visible")
.should("have.attr", "href", "/files");
});
it("displays 404 for unauthenticated user on invalid route", () => {
cy.visit("/not-a-real-page");
cy.url().then((url) => {
if (url.includes("/login")) {
cy.url().should("include", "/login");
} else {
cy.contains("404").should("be.visible");
}
});
});
});
-38
View File
@@ -1,38 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/06/03
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("Discover page navigation tabs", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
});
it("Double-clicking a log file enters the MAP page.", () => {
cy.contains("td.fileName", "sample-process.xes").dblclick();
cy.url().should("include", "map");
// MAP tab should exist in the navbar
cy.contains(".nav-item", "MAP").should("exist");
});
it("Clicking CONFORMANCE tab switches active page.", () => {
cy.contains("td.fileName", "sample-process.xes").dblclick();
cy.url().should("include", "map");
cy.contains(".nav-item", "CONFORMANCE").click();
cy.url().should("include", "conformance");
cy.contains(".nav-item", "CONFORMANCE").should("have.class", "active");
});
it("Clicking PERFORMANCE tab switches active page.", () => {
cy.contains("td.fileName", "sample-process.xes").dblclick();
cy.url().should("include", "map");
cy.contains(".nav-item", "PERFORMANCE").click();
cy.url().should("include", "performance");
cy.contains(".nav-item", "PERFORMANCE").should("have.class", "active");
});
});
-58
View File
@@ -1,58 +0,0 @@
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/06/11
// imacat.yang@dsp.im (imacat), 2026/03/05
import { setupApiIntercepts } from "../support/intercept";
describe("Paste URL login redirect", () => {
it("After login with return-to param, redirects to the remembered page", () => {
setupApiIntercepts();
// 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);
cy.visit(`/login?return-to=${encodedUrl}`);
// Fill in login form
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("form").submit();
cy.wait("@postToken");
// After login, the app should attempt to redirect to the return-to URL.
// Since window.location.href is used (not router.push), we verify the
// login form disappears and the token cookie is set.
cy.getCookie("luciaToken").should("exist");
});
it("Login without return-to param redirects to /files", () => {
setupApiIntercepts();
cy.visit("/login");
cy.get("#account").type("testadmin");
cy.get("#password").type("password123");
cy.get("form").submit();
cy.wait("@postToken");
cy.url().should("include", "/files");
});
it("Unauthenticated user cannot access inner pages", () => {
setupApiIntercepts();
// Override my-account to return 401 (simulate logged-out state)
cy.intercept("GET", "/api/my-account", {
statusCode: 401,
body: { detail: "Not authenticated" },
}).as("getMyAccountUnauth");
cy.visit("/files");
// Should be redirected to login page
cy.url().should("include", "/login");
cy.get("#account").should("exist");
cy.get("#password").should("exist");
});
});
-116
View File
@@ -1,116 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
import { loginWithFixtures } from "../support/intercept";
describe("SweetAlert2 Modals", () => {
describe("File Context Menu - Rename", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
});
it("right-click on table row shows context menu with Rename", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").should("be.visible");
});
it("right-click context menu shows Download option", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Download").should("be.visible");
});
it("right-click context menu shows Delete option", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").should("be.visible");
});
it("clicking Rename opens SweetAlert rename dialog", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").click();
// SweetAlert popup should appear with RENAME title
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-title").should("contain", "RENAME");
cy.get(".swal2-input").should("exist");
});
it("rename dialog has pre-filled file name", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").click();
cy.get(".swal2-input").should("not.have.value", "");
});
it("rename dialog can be cancelled", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Rename").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-cancel").click();
cy.get(".swal2-popup").should("not.exist");
});
it("clicking Delete opens SweetAlert delete confirmation", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").click();
// SweetAlert popup should appear with CONFIRM DELETION
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-title").should("contain", "CONFIRM DELETION");
});
it("delete confirmation shows file name", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-html-container").should("contain", "delete");
});
it("delete confirmation can be cancelled", () => {
cy.get("table tbody tr").first().rightclick();
cy.contains("Delete").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-cancel").click();
cy.get(".swal2-popup").should("not.exist");
});
});
describe("File Context Menu on Grid View", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/files");
cy.wait("@getFiles");
// Switch to grid view
cy.get("svg").parent("li.cursor-pointer").last().click();
});
it("right-click on grid card shows context menu", () => {
cy.get("li[title]").first().rightclick();
cy.contains("Rename").should("be.visible");
cy.contains("Delete").should("be.visible");
});
it("grid card rename opens SweetAlert dialog", () => {
cy.get("li[title]").first().rightclick();
cy.contains("Rename").click();
cy.get(".swal2-popup").should("be.visible");
cy.get(".swal2-title").should("contain", "RENAME");
});
});
describe("Account Delete Confirmation", () => {
beforeEach(() => {
loginWithFixtures();
cy.visit("/account-admin");
cy.wait("@getUsers");
});
it("delete confirmation Yes button triggers delete API", () => {
cy.get(".delete-account").first().click();
cy.get("#modal_container").should("be.visible");
cy.get("#sure_to_delete_acct_btn").click();
cy.wait("@deleteUser");
// Modal should close after deletion
});
});
});
-5
View File
@@ -1,5 +0,0 @@
時間,案號,居住區域,學區,事件名稱,事件序號,狀態 ,時段,溫度,數量,未知,完成,預期時間
2022/05/13 09:25:21,案一,63,富山,事件甲,事件一,Start,早上,95,27,,TRUE,2022/5/14 09:25:21
2022/05/13 09:25:21,案一,,,事件甲,事件一,Complete,中午,135,442,,false,2022/5/14 09:25:21
2022/05/13 09:30:01,案一,,仁德,事件乙,事件四,Start,中午,110.6,-6, ,,2022/5/14 09:30:01
2022/05/13 09:30:01,案一,,,事件乙,事件四,Complete,晚上,-65,4,,true ,2022/5/14 09:30:01
1 時間 案號 居住區域 學區 事件名稱 事件序號 狀態 時段 溫度 數量 未知 完成 預期時間
2 2022/05/13 09:25:21 案一 63 富山 事件甲 事件一 Start 早上 95 27 TRUE 2022/5/14 09:25:21
3 2022/05/13 09:25:21 案一 事件甲 事件一 Complete 中午 135 442 false 2022/5/14 09:25:21
4 2022/05/13 09:30:01 案一 仁德 事件乙 事件四 Start 中午 110.6 -6 2022/5/14 09:30:01
5 2022/05/13 09:30:01 案一 事件乙 事件四 Complete 晚上 -65 4 true 2022/5/14 09:30:01
Binary file not shown.
1 timestamp,case id,name,instance,status
2 2022/05/13 09:25:21,編號1,步驟a,步驟a#1,start
3 2022/05/13 09:25:21,編號1,步驟a,步驟a#1,complete
4 2022/05/13 09:30:01,編號1,步驟b,步驟b#1,start
5 2022/05/13 09:30:01,編號1,步驟b,ŸSze©%ï0†¤SzŠÇÔ6MíÍρθ ¥‰òºˆ(ä–{.bÁÖwÈN�¥Å÷8ôd¬'›„ £<ŸƒÛ°àRž`\g8u/)!ëÂ˶þÀ§æn{Z\¥DRðíÑdÈ
-1
View File
@@ -1 +0,0 @@
timestamp,case id,name,instance,status
1 timestamp case id name instance status
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

@@ -1,9 +0,0 @@
timestamp,case id,name,status
2022/05/13 09:25:21,c1,a,1,start
2022/05/13 09:25:21,c1,a,1,complete
2022/05/13 09:30:01,c1,b,2,start
2022/05/13 09:30:01,c1,b,2,complete
2022/05/13 09:48:33,c2,a,6,start
2022/05/13 09:48:33,c2,a,6,complete
2022/05/13 09:54:27,c2,c,7,start
2022/05/13 09:54:27,c2,c,7,complete
1 timestamp,case id,name,status
2 2022/05/13 09:25:21,c1,a,1,start
3 2022/05/13 09:25:21,c1,a,1,complete
4 2022/05/13 09:30:01,c1,b,2,start
5 2022/05/13 09:30:01,c1,b,2,complete
6 2022/05/13 09:48:33,c2,a,6,start
7 2022/05/13 09:48:33,c2,a,6,complete
8 2022/05/13 09:54:27,c2,c,7,start
9 2022/05/13 09:54:27,c2,c,7,complete
-30
View File
@@ -1,30 +0,0 @@
CaseID,Activity,Timestamp,Status,Activity_Instance
CID_1,檢傷,2023-01-12 15:32:31,start,1
CID_1,檢傷,2023-01-12 15:32:31,complete,1
CID_1,第一次醫囑,2023-01-12 15:49:01,start,2
CID_1,第一次醫囑,2023-01-12 15:49:01,complete,2
CID_1,出院,2023-01-13 03:32:00,start,3
CID_1,出院,2023-01-13 03:32:00,complete,3
CID_2,檢傷,2023-07-26 08:44:17,start,4
CID_2,檢傷,2023-07-26 08:44:17,complete,4
CID_2,第一次醫囑,2023-07-26 08:48:05,start,5
CID_2,第一次醫囑,2023-07-26 08:48:05,complete,5
CID_2,出院,2023-07-26 17:00:49,start,6
CID_2,出院,2023-07-26 17:00:49,complete,6
CID_3,檢傷,2023-11-27 06:20:48,start,7
CID_3,檢傷,2023-11-27 06:20:48,complete,7
CID_3,第一次醫囑,2023-11-27 06:40:30,start,8
CID_3,第一次醫囑,2023-11-27 06:40:30,complete,8
CID_3,出院,2023-11-27 07:21:53,start,9
CID_3,出院,2023-11-27 07:21:53,complete,9
CID_4,檢傷,2023-11-01 01:20:19,start,10
CID_4,檢傷,2023-11-01 01:20:19,complete,10
CID_4,第一次醫囑,2023-11-01 01:34:54,start,11
CID_4,第一次醫囑,2023-11-01 01:34:54,complete,11
CID_4,住院,2023-11-02 06:36:36,start,12
CID_4,住院,2023-11-10 02:35:39,complete,12
CID_4,出院,2023-11-10 02:35:39,start,13
CID_4,出院,2023-11-10 02:35:39,complete,13
CID_5,檢傷,2023-07-13 02:49:36,start,14
CID_5,檢傷,2023-07-13 02:49:36,complete,14
CID_5,第一次醫囑,2023-07-13 03:07:01,start,15
1 CaseID Activity Timestamp Status Activity_Instance
2 CID_1 檢傷 2023-01-12 15:32:31 start 1
3 CID_1 檢傷 2023-01-12 15:32:31 complete 1
4 CID_1 第一次醫囑 2023-01-12 15:49:01 start 2
5 CID_1 第一次醫囑 2023-01-12 15:49:01 complete 2
6 CID_1 出院 2023-01-13 03:32:00 start 3
7 CID_1 出院 2023-01-13 03:32:00 complete 3
8 CID_2 檢傷 2023-07-26 08:44:17 start 4
9 CID_2 檢傷 2023-07-26 08:44:17 complete 4
10 CID_2 第一次醫囑 2023-07-26 08:48:05 start 5
11 CID_2 第一次醫囑 2023-07-26 08:48:05 complete 5
12 CID_2 出院 2023-07-26 17:00:49 start 6
13 CID_2 出院 2023-07-26 17:00:49 complete 6
14 CID_3 檢傷 2023-11-27 06:20:48 start 7
15 CID_3 檢傷 2023-11-27 06:20:48 complete 7
16 CID_3 第一次醫囑 2023-11-27 06:40:30 start 8
17 CID_3 第一次醫囑 2023-11-27 06:40:30 complete 8
18 CID_3 出院 2023-11-27 07:21:53 start 9
19 CID_3 出院 2023-11-27 07:21:53 complete 9
20 CID_4 檢傷 2023-11-01 01:20:19 start 10
21 CID_4 檢傷 2023-11-01 01:20:19 complete 10
22 CID_4 第一次醫囑 2023-11-01 01:34:54 start 11
23 CID_4 第一次醫囑 2023-11-01 01:34:54 complete 11
24 CID_4 住院 2023-11-02 06:36:36 start 12
25 CID_4 住院 2023-11-10 02:35:39 complete 12
26 CID_4 出院 2023-11-10 02:35:39 start 13
27 CID_4 出院 2023-11-10 02:35:39 complete 13
28 CID_5 檢傷 2023-07-13 02:49:36 start 14
29 CID_5 檢傷 2023-07-13 02:49:36 complete 14
30 CID_5 第一次醫囑 2023-07-13 03:07:01 start 15
-9
View File
@@ -1,9 +0,0 @@
timestamp,case id,name,instance,status
2022/05/13 09:25:21,c1,a,1,start
2022/05/13 09:25:21,c1,a,1,
2022/05/13 09:30:01,c1,b,2,begin
2022/05/13 09:30:01,c1,b,,complete
2022/05/13 09:48:33,,,6,start
2022;05;13 09;48;33,c2,a,6,complete
2022/05/13 09:54:27,c2,c,7,start
,c2,c,7,complete
1 timestamp case id name instance status
2 2022/05/13 09:25:21 c1 a 1 start
3 2022/05/13 09:25:21 c1 a 1
4 2022/05/13 09:30:01 c1 b 2 begin
5 2022/05/13 09:30:01 c1 b complete
6 2022/05/13 09:48:33 6 start
7 2022;05;13 09;48;33 c2 a 6 complete
8 2022/05/13 09:54:27 c2 c 7 start
9 c2 c 7 complete
@@ -1,4 +0,0 @@
{
"username": " test ",
"password": " test "
}
-39
View File
@@ -1,39 +0,0 @@
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/01/31
// imacat.yang@dsp.im (imacat), 2026/03/06
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
// -- This is a parent command --
import "@4tw/cypress-drag-drop";
/**
* Sets authentication cookies to simulate a logged-in user.
*
* @returns {void}
*/
Cypress.Commands.add("login", () => {
cy.setCookie("luciaToken", "fake-access-token-for-testing");
cy.setCookie("isLuciaLoggedIn", "true");
});
// Usage: cy.login()
// -- This is a child command --
// Click a blank area to close an opened modal: cy.closePopup()
/**
* Closes the active popup by clicking the top-left area of the page.
*
* @returns {void}
*/
Cypress.Commands.add("closePopup", () => {
// Trigger a forced click to close modal overlays consistently.
cy.get("body").click({ position: "topLeft" });
});
-26
View File
@@ -1,26 +0,0 @@
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/01/31
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
require("cypress-xpath"); // Enables xpath helpers used in pointer event checks.
-190
View File
@@ -1,190 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/05
/**
* Sets up cy.intercept for all API endpoints using fixture files.
* Call setupApiIntercepts() in beforeEach to mock the entire backend.
*/
export function setupApiIntercepts() {
// Auth
cy.intercept("POST", "/api/oauth/token", {
fixture: "api/token.json",
}).as("postToken");
// User account
cy.intercept("GET", "/api/my-account", {
fixture: "api/my-account.json",
}).as("getMyAccount");
cy.intercept("PUT", "/api/my-account", {
statusCode: 200,
body: { success: true },
}).as("putMyAccount");
// Files
cy.intercept("GET", "/api/files", {
fixture: "api/files.json",
}).as("getFiles");
// Users (account management)
cy.intercept("GET", "/api/users", {
fixture: "api/users.json",
}).as("getUsers");
cy.intercept("POST", "/api/users", {
statusCode: 200,
body: { success: true },
}).as("postUser");
cy.intercept("DELETE", "/api/users/*", {
statusCode: 200,
body: { success: true },
}).as("deleteUser");
cy.intercept("PUT", "/api/users/*", {
statusCode: 200,
body: { success: true },
}).as("putUser");
// User detail (GET /api/users/:username)
cy.intercept("GET", "/api/users/*", {
fixture: "api/user-detail.json",
}).as("getUserDetail");
// User roles
cy.intercept("PUT", "/api/users/*/roles/*", {
statusCode: 200,
body: { success: true },
}).as("putUserRole");
cy.intercept("DELETE", "/api/users/*/roles/*", {
statusCode: 200,
body: { success: true },
}).as("deleteUserRole");
// Filter detail (for fetchFunnel when entering filter from Files)
cy.intercept("GET", /\/api\/filters\/\d+$/, {
statusCode: 200,
body: { rules: [], log: { id: 1 }, name: "filtered-sample" },
}).as("getFilterDetail");
// Discover (map data)
cy.intercept("GET", "/api/logs/*/discover", {
fixture: "api/discover.json",
}).as("getDiscover");
cy.intercept("GET", "/api/filters/*/discover", {
fixture: "api/discover.json",
}).as("getFilterDiscover");
// Performance
cy.intercept("GET", "/api/logs/*/performance", {
fixture: "api/performance.json",
}).as("getPerformance");
cy.intercept("GET", "/api/filters/*/performance", {
fixture: "api/performance.json",
}).as("getFilterPerformance");
// Traces
cy.intercept("GET", "/api/logs/*/traces", {
fixture: "api/traces.json",
}).as("getTraces");
cy.intercept("GET", "/api/filters/*/traces", {
fixture: "api/traces.json",
}).as("getFilterTraces");
// Trace detail (must be after traces list intercepts)
cy.intercept("GET", /\/api\/logs\/.*\/traces\/\d+/, {
fixture: "api/trace-detail.json",
}).as("getTraceDetail");
cy.intercept("GET", /\/api\/filters\/.*\/traces\/\d+/, {
fixture: "api/trace-detail.json",
}).as("getFilterTraceDetail");
// Temp filters
cy.intercept("GET", "/api/temp-filters/*/discover", {
fixture: "api/discover.json",
}).as("getTempFilterDiscover");
cy.intercept("GET", "/api/temp-filters/*/traces", {
fixture: "api/traces.json",
}).as("getTempFilterTraces");
// Filter params
cy.intercept("GET", "/api/filters/params*", {
statusCode: 200,
body: {},
}).as("getFilterParams");
cy.intercept("GET", "/api/filters/has-result*", {
statusCode: 200,
body: false,
}).as("getFilterHasResult");
// Conformance check params
cy.intercept("GET", "/api/log-checks/params*", {
fixture: "api/filter-params.json",
}).as("getLogCheckParams");
cy.intercept("GET", "/api/filter-checks/params*", {
fixture: "api/filter-params.json",
}).as("getFilterCheckParams");
// Compare dashboard
cy.intercept("GET", /\/api\/compare\?datasets=/, {
fixture: "api/compare.json",
}).as("getCompare");
// Dependents (for delete confirmation)
cy.intercept("GET", "/api/logs/*/dependents", {
statusCode: 200,
body: [],
}).as("getLogDependents");
cy.intercept("GET", "/api/filters/*/dependents", {
statusCode: 200,
body: [],
}).as("getFilterDependents");
cy.intercept("GET", "/api/log-checks/*/dependents", {
statusCode: 200,
body: [],
}).as("getLogCheckDependents");
cy.intercept("GET", "/api/filter-checks/*/dependents", {
statusCode: 200,
body: [],
}).as("getFilterCheckDependents");
// Rename
cy.intercept("PUT", "/api/logs/*/rename", {
statusCode: 200,
body: { success: true },
}).as("renameLog");
cy.intercept("PUT", "/api/filters/*/rename", {
statusCode: 200,
body: { success: true },
}).as("renameFilter");
// Deletion
cy.intercept("DELETE", "/api/deletion/*", {
statusCode: 200,
body: { success: true },
}).as("deleteDeletion");
}
/**
* Sets the luciaToken cookie and isLuciaLoggedIn cookie to simulate
* a logged-in state, then sets up all API intercepts.
*/
export function loginWithFixtures() {
setupApiIntercepts();
cy.setCookie("luciaToken", "fake-access-token-for-testing");
cy.setCookie("isLuciaLoggedIn", "true");
}
+754 -3229
View File
File diff suppressed because it is too large Load Diff
+6 -10
View File
@@ -5,13 +5,13 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"build:e2e": "VITE_MSW=true vite build",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest run --coverage",
"cy:run": "cypress run",
"test:unit": "vitest --environment jsdom",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
"test:unit": "vitest run",
"test:e2e": "playwright test --config tests/e2e/playwright.config.ts",
"test:e2e:ui": "playwright test --config tests/e2e/playwright.config.ts --ui",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix",
"docs": "typedoc"
@@ -28,7 +28,6 @@
"cytoscape-dagre": "^2.5.0",
"cytoscape-fcose": "^2.2.0",
"cytoscape-popper": "^4.0.1",
"cytoscape-spread": "^3.0.0",
"decimal.js": "^10.6.0",
"i18next": "^25.8.14",
"i18next-browser-languagedetector": "^8.2.1",
@@ -49,8 +48,8 @@
"vuedraggable": "^4.1.0"
},
"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",
@@ -59,16 +58,13 @@
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/test-utils": "^2.4.6",
"chartjs-plugin-dragdata": "^2.3.1",
"cypress": "^15.11.0",
"cypress-xpath": "^2.0.1",
"eslint": "^10.0.2",
"eslint-plugin-cypress": "^6.1.0",
"eslint-plugin-vue": "^10.8.0",
"jsdom": "^28.1.0",
"msw": "^2.12.14",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"start-server-and-test": "^2.1.5",
"tailwindcss": "^4.2.1",
"ts-node": "^10.9.2",
"typedoc": "^0.28.17",
+349
View File
@@ -0,0 +1,349 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.12.14'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}
+15 -1
View File
@@ -98,4 +98,18 @@ app.component("ContextMenu", ContextMenu);
app.component("Draggable", draggable); // Drag and drop
app.directive("tooltip", Tooltip);
app.mount("#app");
/**
* Starts the MSW service worker when VITE_MSW is set.
* Used for E2E testing with mock API responses.
*/
async function enableMocking() {
if (import.meta.env.VITE_MSW !== "true") return;
const { http, HttpResponse } = await import("msw");
const { worker } = await import("./mocks/browser.js");
await worker.start({ onUnhandledRequest: "bypass" });
// Expose on window so Cypress can add per-test overrides
window.__mswWorker__ = worker;
window.__msw__ = { http, HttpResponse };
}
enableMocking().then(() => app.mount("#app"));
+8
View File
@@ -0,0 +1,8 @@
/**
* MSW worker for browser (Cypress E2E).
* @module mocks/browser
*/
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers/index.js";
export const worker = setupWorker(...handlers);
+20
View File
@@ -0,0 +1,20 @@
/**
* MSW handlers for current user account endpoints.
* @module mocks/handlers/account
*/
import { http, HttpResponse } from "msw";
import myAccountData from "../fixtures/my-account.json";
import { captureRequest } from "../request-log.js";
export const accountHandlers = [
http.get("/api/my-account", ({ request }) => {
captureRequest("GET", "/api/my-account");
return HttpResponse.json(myAccountData);
}),
http.put("/api/my-account", async ({ request }) => {
const body = await request.json();
captureRequest("PUT", "/api/my-account", body);
return HttpResponse.json({ success: true });
}),
];
+15
View File
@@ -0,0 +1,15 @@
/**
* MSW handlers for authentication endpoints.
* @module mocks/handlers/auth
*/
import { http, HttpResponse } from "msw";
import tokenData from "../fixtures/token.json";
import { captureRequest } from "../request-log.js";
export const authHandlers = [
http.post("/api/oauth/token", async ({ request }) => {
const body = await request.text();
captureRequest("POST", "/api/oauth/token", body);
return HttpResponse.json(tokenData);
}),
];
+14
View File
@@ -0,0 +1,14 @@
/**
* MSW handlers for comparison dashboard endpoints.
* @module mocks/handlers/compare
*/
import { http, HttpResponse } from "msw";
import compareData from "../fixtures/compare.json";
import { captureRequest } from "../request-log.js";
export const compareHandlers = [
http.get("/api/compare", ({ request }) => {
captureRequest("GET", "/api/compare");
return HttpResponse.json(compareData);
}),
];
+19
View File
@@ -0,0 +1,19 @@
/**
* MSW handlers for conformance check parameter endpoints.
* @module mocks/handlers/conformance
*/
import { http, HttpResponse } from "msw";
import filterParamsData from "../fixtures/filter-params.json";
import { captureRequest } from "../request-log.js";
export const conformanceHandlers = [
http.get("/api/log-checks/params*", ({ request }) => {
captureRequest("GET", new URL(request.url).pathname);
return HttpResponse.json(filterParamsData);
}),
http.get("/api/filter-checks/params*", ({ request }) => {
captureRequest("GET", new URL(request.url).pathname);
return HttpResponse.json(filterParamsData);
}),
];
+24
View File
@@ -0,0 +1,24 @@
/**
* MSW handlers for process discovery (map) endpoints.
* @module mocks/handlers/discover
*/
import { http, HttpResponse } from "msw";
import discoverData from "../fixtures/discover.json";
import { captureRequest } from "../request-log.js";
export const discoverHandlers = [
http.get("/api/logs/:id/discover", ({ params }) => {
captureRequest("GET", `/api/logs/${params.id}/discover`);
return HttpResponse.json(discoverData);
}),
http.get("/api/filters/:id/discover", ({ params }) => {
captureRequest("GET", `/api/filters/${params.id}/discover`);
return HttpResponse.json(discoverData);
}),
http.get("/api/temp-filters/:id/discover", ({ params }) => {
captureRequest("GET", `/api/temp-filters/${params.id}/discover`);
return HttpResponse.json(discoverData);
}),
];
+14
View File
@@ -0,0 +1,14 @@
/**
* MSW handlers for file management endpoints.
* @module mocks/handlers/files
*/
import { http, HttpResponse } from "msw";
import filesData from "../fixtures/files.json";
import { captureRequest } from "../request-log.js";
export const filesHandlers = [
http.get("/api/files", ({ request }) => {
captureRequest("GET", "/api/files");
return HttpResponse.json(filesData);
}),
];
+27
View File
@@ -0,0 +1,27 @@
/**
* Combined MSW request handlers for all API endpoints.
* @module mocks/handlers
*/
import { authHandlers } from "./auth.js";
import { accountHandlers } from "./account.js";
import { usersHandlers } from "./users.js";
import { filesHandlers } from "./files.js";
import { discoverHandlers } from "./discover.js";
import { performanceHandlers } from "./performance.js";
import { tracesHandlers } from "./traces.js";
import { conformanceHandlers } from "./conformance.js";
import { compareHandlers } from "./compare.js";
import { operationsHandlers } from "./operations.js";
export const handlers = [
...authHandlers,
...accountHandlers,
...usersHandlers,
...filesHandlers,
...discoverHandlers,
...performanceHandlers,
...tracesHandlers,
...conformanceHandlers,
...compareHandlers,
...operationsHandlers,
];
+70
View File
@@ -0,0 +1,70 @@
/**
* MSW handlers for filter detail, filter params, dependents,
* rename, and deletion endpoints.
* @module mocks/handlers/operations
*/
import { http, HttpResponse } from "msw";
import { captureRequest } from "../request-log.js";
export const operationsHandlers = [
// Filter detail
http.get("/api/filters/:id", ({ params }) => {
captureRequest("GET", `/api/filters/${params.id}`);
return HttpResponse.json({
rules: [],
log: { id: 1 },
name: "filtered-sample",
});
}),
// Filter params and has-result
http.get("/api/filters/params*", ({ request }) => {
captureRequest("GET", new URL(request.url).pathname);
return HttpResponse.json({});
}),
http.get("/api/filters/has-result*", ({ request }) => {
captureRequest("GET", new URL(request.url).pathname);
return HttpResponse.json(false);
}),
// Dependents
http.get("/api/logs/:id/dependents", ({ params }) => {
captureRequest("GET", `/api/logs/${params.id}/dependents`);
return HttpResponse.json([]);
}),
http.get("/api/filters/:id/dependents", ({ params }) => {
captureRequest("GET", `/api/filters/${params.id}/dependents`);
return HttpResponse.json([]);
}),
http.get("/api/log-checks/:id/dependents", ({ params }) => {
captureRequest("GET", `/api/log-checks/${params.id}/dependents`);
return HttpResponse.json([]);
}),
http.get("/api/filter-checks/:id/dependents", ({ params }) => {
captureRequest("GET", `/api/filter-checks/${params.id}/dependents`);
return HttpResponse.json([]);
}),
// Rename
http.put("/api/logs/:id/rename", async ({ request, params }) => {
const body = await request.json();
captureRequest("PUT", `/api/logs/${params.id}/rename`, body);
return HttpResponse.json({ success: true });
}),
http.put("/api/filters/:id/rename", async ({ request, params }) => {
const body = await request.json();
captureRequest("PUT", `/api/filters/${params.id}/rename`, body);
return HttpResponse.json({ success: true });
}),
// Deletion
http.delete("/api/deletion/:id", ({ params }) => {
captureRequest("DELETE", `/api/deletion/${params.id}`);
return HttpResponse.json({ success: true });
}),
];
+19
View File
@@ -0,0 +1,19 @@
/**
* MSW handlers for performance metrics endpoints.
* @module mocks/handlers/performance
*/
import { http, HttpResponse } from "msw";
import performanceData from "../fixtures/performance.json";
import { captureRequest } from "../request-log.js";
export const performanceHandlers = [
http.get("/api/logs/:id/performance", ({ params }) => {
captureRequest("GET", `/api/logs/${params.id}/performance`);
return HttpResponse.json(performanceData);
}),
http.get("/api/filters/:id/performance", ({ params }) => {
captureRequest("GET", `/api/filters/${params.id}/performance`);
return HttpResponse.json(performanceData);
}),
];
+35
View File
@@ -0,0 +1,35 @@
/**
* MSW handlers for trace list and detail endpoints.
* @module mocks/handlers/traces
*/
import { http, HttpResponse } from "msw";
import tracesData from "../fixtures/traces.json";
import traceDetailData from "../fixtures/trace-detail.json";
import { captureRequest } from "../request-log.js";
export const tracesHandlers = [
http.get("/api/logs/:logId/traces/:traceId", ({ params }) => {
captureRequest("GET", `/api/logs/${params.logId}/traces/${params.traceId}`);
return HttpResponse.json(traceDetailData);
}),
http.get("/api/filters/:filterId/traces/:traceId", ({ params }) => {
captureRequest("GET", `/api/filters/${params.filterId}/traces/${params.traceId}`);
return HttpResponse.json(traceDetailData);
}),
http.get("/api/logs/:id/traces", ({ params }) => {
captureRequest("GET", `/api/logs/${params.id}/traces`);
return HttpResponse.json(tracesData);
}),
http.get("/api/filters/:id/traces", ({ params }) => {
captureRequest("GET", `/api/filters/${params.id}/traces`);
return HttpResponse.json(tracesData);
}),
http.get("/api/temp-filters/:id/traces", ({ params }) => {
captureRequest("GET", `/api/temp-filters/${params.id}/traces`);
return HttpResponse.json(tracesData);
}),
];
+49
View File
@@ -0,0 +1,49 @@
/**
* MSW handlers for user management (account admin) endpoints.
* @module mocks/handlers/users
*/
import { http, HttpResponse } from "msw";
import usersData from "../fixtures/users.json";
import userDetailData from "../fixtures/user-detail.json";
import { captureRequest } from "../request-log.js";
export const usersHandlers = [
http.get("/api/users", ({ request }) => {
captureRequest("GET", "/api/users");
return HttpResponse.json(usersData);
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
captureRequest("POST", "/api/users", body);
return HttpResponse.json({ success: true });
}),
// User roles (must be before /api/users/:username to match first)
http.put("/api/users/:username/roles/:role", async ({ request, params }) => {
captureRequest("PUT", `/api/users/${params.username}/roles/${params.role}`);
return HttpResponse.json({ success: true });
}),
http.delete("/api/users/:username/roles/:role", ({ request, params }) => {
captureRequest("DELETE", `/api/users/${params.username}/roles/${params.role}`);
return HttpResponse.json({ success: true });
}),
// User CRUD (after roles so roles match first)
http.get("/api/users/:username", ({ request, params }) => {
captureRequest("GET", `/api/users/${params.username}`);
return HttpResponse.json(userDetailData);
}),
http.put("/api/users/:username", async ({ request, params }) => {
const body = await request.json();
captureRequest("PUT", `/api/users/${params.username}`, body);
return HttpResponse.json({ success: true });
}),
http.delete("/api/users/:username", ({ request, params }) => {
captureRequest("DELETE", `/api/users/${params.username}`);
return HttpResponse.json({ success: true });
}),
];
+8
View File
@@ -0,0 +1,8 @@
/**
* MSW server for Node.js (Vitest).
* @module mocks/node
*/
import { setupServer } from "msw/node";
import { handlers } from "./handlers/index.js";
export const server = setupServer(...handlers);
+48
View File
@@ -0,0 +1,48 @@
/**
* Request logging utility for MSW handlers.
* Captures request details for test assertions,
* replacing vi.fn() call verification.
* @module mocks/request-log
*/
/** @type {Array<{method: string, url: string, body: any}>} */
let log = [];
/**
* Records a request in the log. Called from MSW handlers.
* @param {string} method - HTTP method.
* @param {string} url - Request URL path.
* @param {any} [body] - Parsed request body.
*/
export function captureRequest(method, url, body = undefined) {
log.push({ method, url, body });
}
/**
* Returns a copy of all logged requests.
* @returns {Array<{method: string, url: string, body: any}>}
*/
export function getRequests() {
return [...log];
}
/**
* Clears the request log. Call in afterEach.
*/
export function clearRequests() {
log = [];
}
/**
* Finds logged requests matching the given method and URL pattern.
* @param {string} method - HTTP method to match.
* @param {string|RegExp} urlPattern - URL string or regex to match.
* @returns {Array<{method: string, url: string, body: any}>}
*/
export function findRequest(method, urlPattern) {
return log.filter((r) => {
if (r.method !== method) return false;
if (typeof urlPattern === "string") return r.url === urlPattern;
return urlPattern.test(r.url);
});
}
-2
View File
@@ -10,7 +10,6 @@
*/
import cytoscape from "cytoscape";
import spread from "cytoscape-spread";
import dagre from "cytoscape-dagre";
import fcose from "cytoscape-fcose";
import cola from "cytoscape-cola";
@@ -48,7 +47,6 @@ const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => {
// Register layout algorithms
cytoscape.use(dagre);
cytoscape.use(spread);
cytoscape.use(fcose);
cytoscape.use(cola);
+2 -2
View File
@@ -15,7 +15,7 @@ import apiError from '@/module/apiError';
import { useLoginStore } from '@/stores/login';
import { JUST_CREATE_ACCOUNT_HOT_DURATION_MINS } from '@/constants/constants';
interface User {
export interface User {
username: string;
detail: Record<string, any>;
name?: string;
@@ -28,7 +28,7 @@ interface User {
isDetailHovered?: boolean;
}
interface EditDetail {
export interface EditDetail {
newUsername?: string;
username?: string;
password: string;
+3 -3
View File
@@ -15,17 +15,17 @@ import { defineStore } from 'pinia';
import { SAVE_KEY_NAME } from '@/constants/constants.js';
interface Position {
export interface Position {
x: number;
y: number;
}
interface Node {
export interface Node {
id: string;
position: Position;
}
interface NodePositions {
export interface NodePositions {
[direction: string]: {
[graphId: string]: Node[];
}
+30
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: "/",
},
]);
}
+27
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,
},
});
+33
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/);
});
});
@@ -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();
});
});
@@ -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();
});
});
@@ -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();
});
});
@@ -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();
});
});
@@ -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
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();
});
});
+45
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();
});
});
+125
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);
});
});
@@ -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
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();
});
});
@@ -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
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();
});
});
});
+162
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();
});
});
});
+88
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();
});
});
+71
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/);
});
});
+63
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();
});
});
+104
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");
});
});
});
+91
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");
});
});
+72
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/);
});
});
+104
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();
});
});
+77
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();
});
});
+44
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();
}
});
});
+56
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/);
});
});
@@ -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();
});
});

Some files were not shown because too many files have changed in this diff Show More