From eab516c7f34727283ff75412f127ee283bc45e93 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 25 Apr 2026 13:25:23 +0330 Subject: [PATCH] feat(tests, components): enhance Cypress commands and UI elements for improved testing and accessibility - Added new Cypress commands for mocking admin-related API interactions, including list, create, update, delete, and change status functionalities. - Introduced data-cy attributes across various components (e.g., Breadcrumbs, Forms, Tables) to improve testability and accessibility. - Updated existing components to support new data-cy attributes for better integration with Cypress tests. --- cypress/e2e/admins/admins-auth.cy.ts | 37 ++++ cypress/e2e/admins/admins-crud.cy.ts | 168 ++++++++++++++++++ cypress/e2e/admins/admins-filters.cy.ts | 84 +++++++++ cypress/e2e/admins/admins-list.cy.ts | 140 +++++++++++++++ cypress/e2e/auth/conditional-layout.cy.ts | 74 ++++++++ cypress/e2e/auth/sign-in-page.cy.ts | 1 + .../e2e/auth/sign-in-theme-persistence.cy.ts | 53 ++++++ cypress/support/admin-fixtures.ts | 30 ++++ cypress/support/commands.ts | 110 ++++++++++++ cypress/support/index.d.ts | 29 +++ src/components/Breadcrumbs/Breadcrumb.tsx | 17 +- .../FormElements/InputGroup/index.tsx | 3 + src/components/FormElements/select.tsx | 4 + src/components/HOC/EmptyListWrapper.tsx | 10 +- .../Tables/admins/AdminListFilters.tsx | 5 + .../admins/ChangeStatusModalContent.tsx | 1 + src/components/Tables/admins/index.tsx | 31 ++-- src/components/forms/admins/CreateAdmin.tsx | 8 + src/components/ui/ConfirmationModal.tsx | 4 + src/components/ui/FormFooter.tsx | 2 + src/components/ui/ListHeader.tsx | 4 + src/components/ui/ListWrapper.tsx | 11 +- src/components/ui/TableSkeleton.tsx | 6 +- src/components/ui/modal.tsx | 5 + 24 files changed, 814 insertions(+), 23 deletions(-) create mode 100644 cypress/e2e/admins/admins-auth.cy.ts create mode 100644 cypress/e2e/admins/admins-crud.cy.ts create mode 100644 cypress/e2e/admins/admins-filters.cy.ts create mode 100644 cypress/e2e/admins/admins-list.cy.ts create mode 100644 cypress/e2e/auth/conditional-layout.cy.ts create mode 100644 cypress/e2e/auth/sign-in-theme-persistence.cy.ts create mode 100644 cypress/support/admin-fixtures.ts diff --git a/cypress/e2e/admins/admins-auth.cy.ts b/cypress/e2e/admins/admins-auth.cy.ts new file mode 100644 index 0000000..d0bbb7f --- /dev/null +++ b/cypress/e2e/admins/admins-auth.cy.ts @@ -0,0 +1,37 @@ +/// +export {}; + +Cypress.env("AUTO_LOGIN", false); + +describe("admins management - authentication and route protection", () => { + after(() => { + Cypress.env("AUTO_LOGIN", true); + }); + + beforeEach(() => { + cy.clearCookies(); + cy.clearLocalStorage(); + }); + + it("redirects unauthorized users to login page", () => { + cy.visit("/admins"); + cy.location("pathname").should("eq", "/auth/sign-in"); + }); + + it("handles expired token by redirecting to login after refresh failure", () => { + cy.setAuthCookies(); + cy.intercept("GET", "**/api/proxy/users**", { + statusCode: 401, + body: { error: { message: "Token expired" } }, + }).as("getAdmins401"); + cy.intercept("POST", "**/api/proxy/profile/refresh-token", { + statusCode: 401, + body: { error: { message: "Refresh token expired" } }, + }).as("refresh401"); + + cy.visit("/admins"); + cy.wait("@getAdmins401"); + cy.wait("@refresh401"); + cy.location("pathname").should("eq", "/auth/sign-in"); + }); +}); diff --git a/cypress/e2e/admins/admins-crud.cy.ts b/cypress/e2e/admins/admins-crud.cy.ts new file mode 100644 index 0000000..edafb5e --- /dev/null +++ b/cypress/e2e/admins/admins-crud.cy.ts @@ -0,0 +1,168 @@ +/// +import { makeAdmin } from "../../support/admin-fixtures"; + +describe("admins management - create, edit, delete, status", () => { + beforeEach(() => { + cy.setAuthCookies(); + cy.mockAdminsList({ users: [makeAdmin()] }); + }); + + it("validates create form required fields and invalid email", () => { + cy.visit("/admins/create"); + cy.getByCy("admin-form-submit-button").click(); + + cy.contains("This field is required").should("be.visible"); + cy.getByCy("admin-form-email-input").type("invalid-email"); + cy.getByCy("admin-form-submit-button").click(); + cy.contains("Email address is invalid").should("be.visible"); + }); + + it("handles duplicate username response on create", () => { + cy.mockCreateAdmin({ + statusCode: 409, + body: { error: { message: "Username already exists" } }, + alias: "createAdminConflict", + }); + + cy.visit("/admins/create"); + cy.getByCy("admin-form-full-name-input").type("Create Admin Name"); + cy.getByCy("admin-form-username-input").type("existinguser"); + cy.getByCy("admin-form-email-input").type("existing-user@example.com"); + cy.getByCy("admin-form-password-input").type("Secret123!"); + cy.getByCy("admin-form-submit-button").click(); + + cy.wait("@createAdminConflict"); + cy.contains("Username already exists").should("be.visible"); + cy.location("pathname").should("eq", "/admins/create"); + }); + + it("creates admin successfully and shows new row + success toast", () => { + cy.mockCreateAdmin({ alias: "createAdminSuccess" }); + cy.mockAdminsList({ + alias: "getAdminsAfterCreate", + users: [makeAdmin({}, 1), makeAdmin({ id: "507f1f77bcf86cd799439077", full_name: "New Admin" }, 7)], + }); + + cy.visit("/admins/create"); + cy.getByCy("admin-form-full-name-input").type("New Admin"); + cy.getByCy("admin-form-username-input").type("newadmin"); + cy.getByCy("admin-form-email-input").type("newadmin@example.com"); + cy.getByCy("admin-form-password-input").type("Secret123!"); + cy.getByCy("admin-form-submit-button").click(); + + cy.wait("@createAdminSuccess"); + cy.location("pathname").should("eq", "/admins"); + cy.wait("@getAdminsAfterCreate"); + cy.contains("New Admin").should("be.visible"); + cy.contains(/success|created/i).should("be.visible"); + }); + + it("opens edit form prefilled and updates admin successfully", () => { + const admin = makeAdmin( + { + id: "507f1f77bcf86cd799439055", + full_name: "Old Name", + username: "oldnameadmin", + }, + 5, + ); + cy.mockAdminsList({ users: [admin] }); + cy.intercept("GET", `**/api/proxy/users/${admin.id}`, { + statusCode: 200, + body: { success: true, message: "ok", data: admin }, + }).as("getAdminDetails"); + cy.mockUpdateAdmin(admin.id, { alias: "updateAdminSuccess" }); + + cy.visit("/admins"); + cy.contains("Old Name").should("be.visible"); + cy.getByCy(`admins-row-edit-${admin.id}`).click(); + cy.wait("@getAdminDetails"); + cy.location("pathname").should("eq", `/admins/edit/${admin.id}`); + cy.getByCy("admin-form-full-name-input").should("have.value", "Old Name"); + + cy.getByCy("admin-form-full-name-input").clear().type("Updated Name"); + cy.mockAdminsList({ + alias: "getAdminsAfterUpdate", + users: [makeAdmin({ ...admin, full_name: "Updated Name" }, 5)], + }); + cy.getByCy("admin-form-submit-button").click(); + + cy.wait("@updateAdminSuccess"); + cy.location("pathname").should("eq", "/admins"); + cy.wait("@getAdminsAfterUpdate"); + cy.contains("Updated Name").should("be.visible"); + }); + + it("shows delete confirmation and supports cancel + confirm", () => { + const admin = makeAdmin({ id: "507f1f77bcf86cd799439066", full_name: "Delete Me" }, 6); + cy.mockAdminsList({ users: [admin] }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + cy.getByCy(`admins-row-delete-${admin.id}`).click(); + cy.getByCy("confirmation-modal").should("be.visible"); + cy.getByCy("confirmation-modal-cancel").click(); + cy.getByCy("confirmation-modal").should("not.exist"); + + cy.mockDeleteAdmin(admin.id, { alias: "deleteAdminSuccess" }); + cy.mockAdminsList({ alias: "getAdminsAfterDelete", users: [] }); + cy.getByCy(`admins-row-delete-${admin.id}`).click(); + cy.getByCy("confirmation-modal-confirm").click(); + + cy.wait("@deleteAdminSuccess"); + cy.wait("@getAdminsAfterDelete"); + cy.getByCy("empty-list-message").should("be.visible"); + }); + + it("changes admin status and persists after refresh", () => { + const admin = makeAdmin({ id: "507f1f77bcf86cd799439088", status: "inactive" }, 8); + cy.mockAdminsList({ users: [admin] }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + cy.getByCy(`admins-row-manage-status-${admin.id}`).click(); + cy.getByCy("modal-content").should("be.visible"); + cy.getByCy("admins-change-status-select").select("active"); + + cy.mockChangeAdminStatus(admin.id, { alias: "statusUpdated" }); + cy.mockAdminsList({ + alias: "getAdminsAfterStatusChange", + users: [makeAdmin({ ...admin, status: "active" }, 8)], + }); + + cy.getByCy("modal-confirm-button").click(); + cy.wait("@statusUpdated"); + cy.wait("@getAdminsAfterStatusChange"); + cy.getByCy(`admins-table-row-${admin.id}`).within(() => { + cy.getByCy("admins-row-status").should("contain", "active"); + }); + + cy.reload(); + cy.wait("@getAdminsAfterStatusChange"); + cy.getByCy(`admins-table-row-${admin.id}`).within(() => { + cy.getByCy("admins-row-status").should("contain", "active"); + }); + }); + + it("prevents duplicate create submission while request is pending", () => { + cy.intercept("POST", "**/api/proxy/users", (req) => { + req.reply({ + delay: 1400, + statusCode: 201, + body: { success: true, message: "created", data: {} }, + }); + }).as("slowCreateAdmin"); + + cy.visit("/admins/create"); + cy.getByCy("admin-form-full-name-input").type("Slow Admin"); + cy.getByCy("admin-form-username-input").type("slowadmin"); + cy.getByCy("admin-form-email-input").type("slowadmin@example.com"); + cy.getByCy("admin-form-password-input").type("Secret123!"); + cy.getByCy("admin-form-submit-button").click(); + cy.getByCy("admin-form-submit-button").should("be.disabled"); + cy.getByCy("admin-form-submit-button").find("div.animate-spin").should("exist"); + + cy.wait("@slowCreateAdmin"); + cy.get("@slowCreateAdmin.all").should("have.length", 1); + }); +}); diff --git a/cypress/e2e/admins/admins-filters.cy.ts b/cypress/e2e/admins/admins-filters.cy.ts new file mode 100644 index 0000000..a45b62f --- /dev/null +++ b/cypress/e2e/admins/admins-filters.cy.ts @@ -0,0 +1,84 @@ +/// +import { makeAdmin } from "../../support/admin-fixtures"; + +describe("admins management - filters", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("opens filter modal and filters by search term", () => { + const defaultUsers = [ + makeAdmin({ full_name: "John Doe", username: "johndoe", email: "john@example.com" }, 1), + ]; + const filteredUsers = [ + makeAdmin({ full_name: "Ali Reza", username: "alireza", email: "ali@example.com" }, 2), + ]; + cy.intercept("GET", "**/api/proxy/users**", (req) => { + const isFiltered = req.query.search === "Ali"; + req.alias = isFiltered ? "getAdminsFilteredBySearch" : "getAdminsDefault"; + const users = isFiltered ? filteredUsers : defaultUsers; + req.reply({ + statusCode: 200, + body: { + success: true, + message: "ok", + data: { users }, + meta: { offset: 0, limit: 10, total: users.length }, + }, + }); + }); + + cy.visit("/admins"); + cy.wait("@getAdminsDefault"); + cy.contains("John Doe").should("be.visible"); + cy.getByCy("admins-filter-button").click(); + cy.getByCy("admins-filter-form").should("be.visible"); + cy.getByCy("admins-filter-search-input").type("Ali"); + cy.getByCy("admins-filter-submit-button").click(); + + cy.wait("@getAdminsFilteredBySearch"); + cy.contains("Ali Reza").should("be.visible"); + cy.contains("John Doe").should("not.exist"); + }); + + it("filters by status and clear resets results", () => { + const allUsers = [ + makeAdmin({ status: "active", full_name: "Active Admin" }, 1), + makeAdmin({ status: "inactive", full_name: "Inactive Admin" }, 2), + ]; + const inactiveUsers = [makeAdmin({ status: "inactive", full_name: "Inactive Admin" }, 3)]; + cy.intercept("GET", "**/api/proxy/users**", (req) => { + const isInactiveFilter = req.query.status === "inactive"; + req.alias = isInactiveFilter + ? "getAdminsFilteredByStatus" + : "getAdminsDefaultByStatus"; + req.reply({ + statusCode: 200, + body: { + success: true, + message: "ok", + data: { users: isInactiveFilter ? inactiveUsers : allUsers }, + meta: { offset: 0, limit: 10, total: isInactiveFilter ? 1 : 2 }, + }, + }); + }); + + cy.visit("/admins"); + cy.wait("@getAdminsDefaultByStatus"); + cy.getByCy("admins-filter-button").click(); + cy.getByCy("admins-filter-status-select").select("inactive"); + cy.getByCy("admins-filter-submit-button").click(); + + cy.wait("@getAdminsFilteredByStatus"); + cy.location("search").should("contain", "status=inactive"); + cy.contains("Inactive Admin").should("be.visible"); + cy.contains("Active Admin").should("not.exist"); + + cy.getByCy("admins-filter-button").click(); + cy.getByCy("admins-filter-status-select-clear").click(); + cy.getByCy("admins-filter-submit-button").click(); + cy.wait("@getAdminsDefaultByStatus"); + cy.location("search").should("not.contain", "status=inactive"); + cy.contains("Active Admin").should("be.visible"); + }); +}); diff --git a/cypress/e2e/admins/admins-list.cy.ts b/cypress/e2e/admins/admins-list.cy.ts new file mode 100644 index 0000000..7389daf --- /dev/null +++ b/cypress/e2e/admins/admins-list.cy.ts @@ -0,0 +1,140 @@ +/// +import { ADMIN_COLUMNS, makeAdmin } from "../../support/admin-fixtures"; + +describe("admins management - list and resilience", () => { + beforeEach(() => { + cy.setAuthCookies(); + }); + + it("renders admins page, breadcrumb, actions, and rows from API", () => { + const admins = [makeAdmin({}, 1), makeAdmin({ status: "inactive" }, 2)]; + cy.mockAdminsList({ users: admins }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + + cy.getByCy("breadcrumb-link-dashboard").should("be.visible"); + cy.getByCy("breadcrumb-current-admins").should("contain", "Admins"); + cy.getByCy("admins-create-button").should("be.visible"); + cy.getByCy("admins-filter-button").should("be.visible"); + cy.getByCy("admins-table").should("be.visible"); + cy.get('[data-cy^="admins-table-row-"]').should("have.length", admins.length); + }); + + it("shows all expected table headers and backend row data", () => { + const admin = makeAdmin({ + full_name: "John Carter", + email: "john.carter@example.com", + username: "jcarter", + status: "active", + }); + cy.mockAdminsList({ users: [admin] }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + + ADMIN_COLUMNS.forEach((column, index) => { + cy.getByCy(`admins-table-header-${index}`).should("contain", column); + }); + + cy.getByCy(`admins-table-row-${admin.id}`).within(() => { + cy.getByCy("admins-row-full-name").should("contain", admin.full_name); + cy.getByCy("admins-row-email").should("contain", admin.email); + cy.getByCy("admins-row-username").should("contain", admin.username); + cy.getByCy("admins-row-status").should("contain", "active"); + }); + }); + + it("shows empty state when API returns no admins", () => { + cy.mockAdminsList({ users: [], total: 0 }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + + cy.getByCy("empty-list-message").should("contain", "No admins were found"); + cy.get('[data-cy^="admins-table-row-"]').should("have.length", 0); + }); + + it("shows loading skeleton while list request is in flight", () => { + cy.mockAdminsList({ delay: 1500, users: [makeAdmin()] }); + + cy.visit("/admins"); + cy.getByCy("table-skeleton").should("be.visible"); + cy.wait("@getAdmins"); + cy.getByCy("table-skeleton").should("not.exist"); + }); + + it("handles API 500 and displays an error toast", () => { + cy.intercept("GET", "**/api/proxy/users**", { + statusCode: 500, + body: { error: { message: "Internal server error" } }, + }).as("getAdmins500"); + + cy.visit("/admins"); + cy.wait("@getAdmins500"); + cy.contains("Internal server error").should("be.visible"); + }); + + it("handles forbidden request with proper feedback", () => { + cy.intercept("GET", "**/api/proxy/users**", { + statusCode: 403, + body: { error: { message: "Forbidden" } }, + }).as("getAdmins403"); + + cy.visit("/admins"); + cy.wait("@getAdmins403"); + cy.contains("Forbidden").should("be.visible"); + }); + + it("handles network failure without UI crash", () => { + cy.intercept("GET", "**/api/proxy/users**", { forceNetworkError: true }).as( + "getAdminsNetworkError", + ); + + cy.visit("/admins"); + cy.wait("@getAdminsNetworkError"); + cy.getByCy("admins-page").should("be.visible"); + cy.contains(/network|fetch|request/i).should("be.visible"); + }); + + it("supports responsive usability on mobile viewport", () => { + cy.viewport("iphone-x"); + cy.mockAdminsList({ users: [makeAdmin()] }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + + cy.getByCy("admins-filter-button").should("be.visible"); + cy.getByCy("admins-create-button").should("be.visible"); + cy.getByCy("admins-table").should("be.visible"); + }); + + it("validates pagination request behavior when pagination controls exist", () => { + cy.mockAdminsList({ users: Array.from({ length: 10 }).map((_, i) => makeAdmin({}, i + 1)) }); + + cy.visit("/admins"); + cy.wait("@getAdmins"); + + cy.get("body").then(($body) => { + if ($body.find('[data-cy="pagination-next"]').length > 0) { + cy.intercept( + { method: "GET", url: "**/api/proxy/users**", query: { offset: "10" } }, + { + statusCode: 200, + body: { + success: true, + message: "ok", + data: { + users: [makeAdmin({ id: "507f1f77bcf86cd799439099", full_name: "Page 2 Admin" })], + }, + meta: { offset: 10, limit: 10, total: 11 }, + }, + }, + ).as("getAdminsPage2"); + + cy.getByCy("pagination-next").click(); + cy.wait("@getAdminsPage2"); + } + }); + }); +}); diff --git a/cypress/e2e/auth/conditional-layout.cy.ts b/cypress/e2e/auth/conditional-layout.cy.ts new file mode 100644 index 0000000..9ad2c76 --- /dev/null +++ b/cypress/e2e/auth/conditional-layout.cy.ts @@ -0,0 +1,74 @@ +/// +export {}; + +Cypress.env("AUTO_LOGIN", false); + +const AUTH_COOKIE_KEYS = { + accessToken: "TENDER_PANEL_token", + refreshToken: "TENDER_PANEL_refresh_token", + user: "TENDER_PANEL_user", +} as const; + +const seedValidAuthCookies = () => { + cy.setCookie(AUTH_COOKIE_KEYS.accessToken, "valid-e2e-token"); + cy.setCookie(AUTH_COOKIE_KEYS.refreshToken, "valid-e2e-refresh-token"); + cy.setCookie( + AUTH_COOKIE_KEYS.user, + encodeURIComponent( + JSON.stringify({ + id: "507f1f77bcf86cd799439011", + username: "e2e-admin", + email: "e2e-admin@example.com", + role: "admin", + }), + ), + ); +}; + +describe("conditional layout auth routing", () => { + after(() => { + Cypress.env("AUTO_LOGIN", true); + }); + + beforeEach(() => { + cy.clearCookies(); + cy.clearLocalStorage(); + }); + + it("shows dashboard page when user has a valid token", () => { + seedValidAuthCookies(); + + cy.visit("/"); + + cy.location("pathname").should("eq", "/"); + cy.contains("Opportunity Lens").should("be.visible"); + cy.contains("Dashboard").should("be.visible"); + }); + + it("redirects unauthenticated user to login page for protected route", () => { + cy.visit("/admins"); + + cy.location("pathname").should("eq", "/auth/sign-in"); + cy.contains("Welcome back").should("be.visible"); + cy.get('input[name="username"]').should("be.visible"); + }); + + it("redirects authenticated user away from login page to dashboard", () => { + seedValidAuthCookies(); + + cy.visit("/auth/sign-in"); + + cy.location("pathname").should("eq", "/"); + cy.contains("Opportunity Lens").should("be.visible"); + }); + + it("redirects to login when token exists but user cookie is invalid", () => { + cy.setCookie(AUTH_COOKIE_KEYS.accessToken, "invalid-session-token"); + cy.setCookie(AUTH_COOKIE_KEYS.user, "{invalid-json"); + + cy.visit("/admins"); + + cy.location("pathname").should("eq", "/auth/sign-in"); + cy.contains("Welcome back").should("be.visible"); + }); +}); diff --git a/cypress/e2e/auth/sign-in-page.cy.ts b/cypress/e2e/auth/sign-in-page.cy.ts index ecf4335..a3cb8f9 100644 --- a/cypress/e2e/auth/sign-in-page.cy.ts +++ b/cypress/e2e/auth/sign-in-page.cy.ts @@ -106,4 +106,5 @@ describe("sign-in page", () => { .should("exist"); cy.get('button[type="submit"]').should("not.contain", "Sign in"); }); + }); diff --git a/cypress/e2e/auth/sign-in-theme-persistence.cy.ts b/cypress/e2e/auth/sign-in-theme-persistence.cy.ts new file mode 100644 index 0000000..e6b644e --- /dev/null +++ b/cypress/e2e/auth/sign-in-theme-persistence.cy.ts @@ -0,0 +1,53 @@ +/// +export {}; + +Cypress.env("AUTO_LOGIN", false); + +describe("sign-in theme persistence", () => { + after(() => { + Cypress.env("AUTO_LOGIN", true); + }); + + beforeEach(() => { + cy.clearCookies(); + cy.clearLocalStorage(); + + cy.intercept("POST", "**/api/proxy/profile/login*", (req) => { + req.reply({ + delay: 2500, + statusCode: 200, + body: { + success: true, + message: "ok", + data: { + access_token: "e2e-access-token", + refresh_token: "e2e-refresh-token", + expires_at: Date.now() + 60_000, + user: { + id: "507f1f77bcf86cd799439011", + username: "tester", + email: "tester@example.com", + role: "admin", + }, + }, + }, + }); + }).as("loginRequest"); + + cy.visit("/auth/sign-in"); + cy.location("pathname").should("eq", "/auth/sign-in"); + }); + + it("keeps selected theme after user logs in and lands on dashboard", () => { + cy.contains("button", "Switch to dark mode").click(); + cy.get("html").should("have.class", "dark"); + + cy.get('input[name="username"]').type("tester"); + cy.get('input[name="password"]').type("Secret123!"); + cy.contains('button[type="submit"]', "Sign in").click(); + + cy.wait("@loginRequest"); + cy.location("pathname").should("eq", "/"); + cy.get("html").should("have.class", "dark"); + }); +}); diff --git a/cypress/support/admin-fixtures.ts b/cypress/support/admin-fixtures.ts new file mode 100644 index 0000000..b415c7e --- /dev/null +++ b/cypress/support/admin-fixtures.ts @@ -0,0 +1,30 @@ +export type AdminRecord = { + id: string; + full_name: string; + email: string; + username: string; + status: "active" | "inactive"; + role?: string; +}; + +export const ADMIN_COLUMNS = [ + "row", + "full name", + "email", + "username", + "status", + "actions", +]; + +export const makeAdmin = ( + overrides: Partial = {}, + index = 1, +): AdminRecord => ({ + id: `507f1f77bcf86cd7994390${(10 + index).toString().padStart(2, "0")}`, + full_name: `E2E Admin ${index}`, + email: `e2e-admin-${index}@example.com`, + username: `e2e-admin-${index}`, + status: "active", + role: "admin", + ...overrides, +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 723dc94..04c1701 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -6,6 +6,16 @@ const AUTH_COOKIE_KEYS = { user: "TENDER_PANEL_user", } as const; +const createAdminUser = (overrides: Partial> = {}) => ({ + id: "507f1f77bcf86cd799439012", + full_name: "E2E Admin One", + username: "e2e-admin-one", + email: "e2e-admin-one@example.com", + role: "admin", + status: "active", + ...overrides, +}); + Cypress.Commands.add("loginAsAdmin", () => { cy.session( "admin-session", @@ -35,3 +45,103 @@ Cypress.Commands.add("loginAsAdmin", () => { }, ); }); + +Cypress.Commands.add("getByCy", (value: string) => cy.get(`[data-cy="${value}"]`)); + +Cypress.Commands.add("setAuthCookies", (userOverrides = {}) => { + cy.setCookie(AUTH_COOKIE_KEYS.accessToken, "e2e-access-token"); + cy.setCookie(AUTH_COOKIE_KEYS.refreshToken, "e2e-refresh-token"); + cy.setCookie( + AUTH_COOKIE_KEYS.user, + encodeURIComponent( + JSON.stringify( + createAdminUser({ + id: "507f1f77bcf86cd799439011", + ...userOverrides, + }), + ), + ), + ); +}); + +Cypress.Commands.add("mockAdminsList", (options = {}) => { + const users = options.users ?? [createAdminUser()]; + const offset = options.offset ?? 0; + const limit = options.limit ?? 10; + const total = options.total ?? users.length; + const statusCode = options.statusCode ?? 200; + const delay = options.delay ?? 0; + const query = options.query; + + cy.intercept( + { + method: "GET", + url: "**/api/proxy/users**", + query, + }, + (req) => { + req.reply({ + delay, + statusCode, + body: { + success: statusCode < 400, + message: statusCode < 400 ? "ok" : "failed", + data: { users }, + meta: { offset, limit, total }, + }, + }); + }, + ).as(options.alias ?? "getAdmins"); +}); + +Cypress.Commands.add("mockCreateAdmin", (options = {}) => { + const statusCode = options.statusCode ?? 201; + cy.intercept("POST", "**/api/proxy/users", { + statusCode, + body: options.body ?? { + success: statusCode < 400, + message: options.message ?? "Admin created successfully", + data: {}, + }, + delay: options.delay ?? 0, + }).as(options.alias ?? "createAdmin"); +}); + +Cypress.Commands.add("mockUpdateAdmin", (id: string, options = {}) => { + const statusCode = options.statusCode ?? 200; + cy.intercept("PUT", `**/api/proxy/users/${id}`, { + statusCode, + body: options.body ?? { + success: statusCode < 400, + message: options.message ?? "Admin updated successfully", + data: {}, + }, + delay: options.delay ?? 0, + }).as(options.alias ?? "updateAdmin"); +}); + +Cypress.Commands.add("mockDeleteAdmin", (id: string, options = {}) => { + const statusCode = options.statusCode ?? 200; + cy.intercept("DELETE", `**/api/proxy/users/${id}`, { + statusCode, + body: options.body ?? { + success: statusCode < 400, + message: options.message ?? "Admin deleted successfully", + data: {}, + }, + delay: options.delay ?? 0, + }).as(options.alias ?? "deleteAdmin"); +}); + +Cypress.Commands.add("mockChangeAdminStatus", (id: string, options = {}) => { + const statusCode = options.statusCode ?? 200; + cy.intercept("PUT", `**/api/proxy/users/${id}/status`, { + statusCode, + body: options.body ?? { + success: statusCode < 400, + message: options.message ?? "Status changed successfully", + data: {}, + }, + delay: options.delay ?? 0, + }).as(options.alias ?? "changeAdminStatus"); +}); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index cbb066d..80831ab 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -2,8 +2,37 @@ declare global { namespace Cypress { + interface AdminListMockOptions { + users?: Record[]; + offset?: number; + limit?: number; + total?: number; + statusCode?: number; + delay?: number; + alias?: string; + query?: Record; + } + + interface MutationMockOptions { + statusCode?: number; + delay?: number; + alias?: string; + message?: string; + body?: Record; + } + interface Chainable { loginAsAdmin(): Chainable; + getByCy(value: string): Chainable>; + setAuthCookies(userOverrides?: Partial>): Chainable; + mockAdminsList(options?: AdminListMockOptions): Chainable; + mockCreateAdmin(options?: MutationMockOptions): Chainable; + mockUpdateAdmin(id: string, options?: MutationMockOptions): Chainable; + mockDeleteAdmin(id: string, options?: MutationMockOptions): Chainable; + mockChangeAdminStatus( + id: string, + options?: MutationMockOptions, + ): Chainable; } } } diff --git a/src/components/Breadcrumbs/Breadcrumb.tsx b/src/components/Breadcrumbs/Breadcrumb.tsx index 493d7ac..6a5a5ea 100644 --- a/src/components/Breadcrumbs/Breadcrumb.tsx +++ b/src/components/Breadcrumbs/Breadcrumb.tsx @@ -7,26 +7,37 @@ interface BreadcrumbProps { const Breadcrumb = ({ items }: BreadcrumbProps) => { const currentPage = items[items.length - 1]; + const toDataCySegment = (value: string) => + value.toLowerCase().replace(/[^a-z0-9]+/g, "-"); return ( -
+

{currentPage.name}

-