Compare commits
10 Commits
97748bea60
...
f828bd0423
| Author | SHA1 | Date | |
|---|---|---|---|
| f828bd0423 | |||
| 6e7d010c54 | |||
| 67a723207f | |||
| 3d1de913f8 | |||
| b978071f94 | |||
| 3918755b7c | |||
| 7e052f0d36 | |||
| 0ff03ec0ef | |||
| 0af0ff39d4 | |||
| aeb6d207c5 |
+3
-4
@@ -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
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../support/**/*",
|
||||
"/node_modules/cypress",
|
||||
"cypress/**/*.js",],
|
||||
"experimentalShadowDomSupport": true
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
Binary file not shown.
|
@@ -1 +0,0 @@
|
||||
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,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,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,4 +0,0 @@
|
||||
{
|
||||
"username": " test ",
|
||||
"password": " test "
|
||||
}
|
||||
@@ -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" });
|
||||
});
|
||||
@@ -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.
|
||||
@@ -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");
|
||||
}
|
||||
Generated
+754
-3229
File diff suppressed because it is too large
Load Diff
+6
-10
@@ -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",
|
||||
|
||||
@@ -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
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
@@ -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 });
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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 });
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
}),
|
||||
];
|
||||
@@ -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 });
|
||||
}),
|
||||
];
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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: "/",
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user