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.
This commit is contained in:
AmirReza Jamali
2026-04-25 13:25:23 +03:30
parent 2a4f3e7e4b
commit eab516c7f3
24 changed files with 814 additions and 23 deletions
+37
View File
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
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");
});
});
+168
View File
@@ -0,0 +1,168 @@
/// <reference types="cypress" />
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);
});
});
+84
View File
@@ -0,0 +1,84 @@
/// <reference types="cypress" />
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");
});
});
+140
View File
@@ -0,0 +1,140 @@
/// <reference types="cypress" />
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");
}
});
});
});
+74
View File
@@ -0,0 +1,74 @@
/// <reference types="cypress" />
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");
});
});
+1
View File
@@ -106,4 +106,5 @@ describe("sign-in page", () => {
.should("exist");
cy.get('button[type="submit"]').should("not.contain", "Sign in");
});
});
@@ -0,0 +1,53 @@
/// <reference types="cypress" />
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");
});
});
+30
View File
@@ -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<AdminRecord> = {},
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,
});
+110
View File
@@ -6,6 +6,16 @@ const AUTH_COOKIE_KEYS = {
user: "TENDER_PANEL_user",
} as const;
const createAdminUser = (overrides: Partial<Record<string, any>> = {}) => ({
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");
});
+29
View File
@@ -2,8 +2,37 @@
declare global {
namespace Cypress {
interface AdminListMockOptions {
users?: Record<string, any>[];
offset?: number;
limit?: number;
total?: number;
statusCode?: number;
delay?: number;
alias?: string;
query?: Record<string, string | number>;
}
interface MutationMockOptions {
statusCode?: number;
delay?: number;
alias?: string;
message?: string;
body?: Record<string, any>;
}
interface Chainable {
loginAsAdmin(): Chainable<void>;
getByCy(value: string): Chainable<JQuery<HTMLElement>>;
setAuthCookies(userOverrides?: Partial<Record<string, any>>): Chainable<void>;
mockAdminsList(options?: AdminListMockOptions): Chainable<void>;
mockCreateAdmin(options?: MutationMockOptions): Chainable<void>;
mockUpdateAdmin(id: string, options?: MutationMockOptions): Chainable<void>;
mockDeleteAdmin(id: string, options?: MutationMockOptions): Chainable<void>;
mockChangeAdminStatus(
id: string,
options?: MutationMockOptions,
): Chainable<void>;
}
}
}