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:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Vendored
+29
@@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div
|
||||
className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
data-cy="breadcrumb"
|
||||
>
|
||||
<h2 className="text-[16px] font-bold leading-[30px] text-dark dark:text-white">
|
||||
{currentPage.name}
|
||||
</h2>
|
||||
|
||||
<nav>
|
||||
<nav data-cy="breadcrumb-nav">
|
||||
<ol className="flex items-center gap-2">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
const dataCySegment = toDataCySegment(item.name);
|
||||
|
||||
return isLast ? (
|
||||
<li key={index} className="font-medium text-sm text-primary">
|
||||
<li
|
||||
key={index}
|
||||
className="font-medium text-sm text-primary"
|
||||
data-cy={`breadcrumb-current-${dataCySegment}`}
|
||||
>
|
||||
{item.name}
|
||||
</li>
|
||||
) : (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<Link
|
||||
href={item.href}
|
||||
data-cy={`breadcrumb-link-${dataCySegment}`}
|
||||
className="font-medium text-sm hover:text-primary"
|
||||
>
|
||||
{item.name}
|
||||
|
||||
@@ -30,6 +30,7 @@ type InputGroupProps<T extends FieldValues> = {
|
||||
height?: "sm" | "default";
|
||||
autoComplete?: HTMLInputAutoCompleteAttribute;
|
||||
defaultValue?: string;
|
||||
dataCy?: string;
|
||||
register?: UseFormRegister<T>;
|
||||
errors?: FieldErrors<T>;
|
||||
} & Partial<UseFormRegisterReturn>;
|
||||
@@ -49,6 +50,7 @@ const InputGroup = <T extends FieldValues>({
|
||||
register,
|
||||
errors,
|
||||
autoComplete,
|
||||
dataCy,
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
@@ -84,6 +86,7 @@ const InputGroup = <T extends FieldValues>({
|
||||
name={name}
|
||||
ref={ref}
|
||||
autoComplete={autoComplete}
|
||||
data-cy={dataCy}
|
||||
onChange={(e) => {
|
||||
onChange?.(e);
|
||||
handleChange?.(e);
|
||||
|
||||
@@ -25,6 +25,7 @@ type PropsType<T extends FieldValues> = {
|
||||
clearable?: boolean;
|
||||
onClear?: () => void;
|
||||
value?: string;
|
||||
dataCy?: string;
|
||||
} & (
|
||||
| { placeholder?: string; defaultValue?: string }
|
||||
| { placeholder: string; defaultValue?: string }
|
||||
@@ -48,6 +49,7 @@ export function Select<T extends FieldValues>({
|
||||
clearable = false,
|
||||
onClear,
|
||||
value: controlledValue,
|
||||
dataCy,
|
||||
...props
|
||||
}: PropsType<T>) {
|
||||
const id = useId();
|
||||
@@ -107,6 +109,7 @@ export function Select<T extends FieldValues>({
|
||||
onChange?.(e);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
data-cy={dataCy}
|
||||
className={cn(
|
||||
"w-full appearance-none rounded-xl border border-stroke/70 bg-white/90 px-5.5 py-3 shadow-[0_1px_2px_rgba(16,24,40,0.04),0_8px_20px_rgba(16,24,40,0.04)] outline-none backdrop-blur-[1px] transition-all duration-200 focus:border-primary/70 focus:bg-white focus:shadow-[0_0_0_2px_rgba(59,130,246,0.14),0_10px_24px_rgba(59,130,246,0.08)] active:border-primary/70 dark:border-dark-3/80 dark:bg-dark-2/85 dark:focus:border-primary/80 dark:focus:shadow-[0_0_0_2px_rgba(59,130,246,0.2),0_10px_24px_rgba(15,23,42,0.45)] [&>option]:text-dark-5 dark:[&>option]:text-dark-6",
|
||||
isOptionSelected && "text-dark dark:text-white",
|
||||
@@ -133,6 +136,7 @@ export function Select<T extends FieldValues>({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
data-cy={dataCy ? `${dataCy}-clear` : undefined}
|
||||
className="absolute right-10 top-1/2 -translate-y-1/2 text-dark-5 transition hover:scale-110 hover:text-dark dark:text-dark-6 dark:hover:text-white"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -22,9 +22,13 @@ function EmptyListWrapper<T>({
|
||||
|
||||
if (!list.length) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="text-center" colSpan={columns.length * 100}>
|
||||
<h2>{emptyMessage}</h2>
|
||||
<TableRow data-cy="empty-list-row">
|
||||
<TableCell
|
||||
className="text-center"
|
||||
colSpan={columns.length * 100}
|
||||
data-cy="empty-list-cell"
|
||||
>
|
||||
<h2 data-cy="empty-list-message">{emptyMessage}</h2>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ const AdminListFilters = ({
|
||||
<form
|
||||
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
|
||||
onSubmit={handleFilterSubmit(search)}
|
||||
data-cy="admins-filter-form"
|
||||
>
|
||||
<Select
|
||||
{...filterRegister("status")}
|
||||
@@ -43,6 +44,7 @@ const AdminListFilters = ({
|
||||
clearable
|
||||
value={watch("status")}
|
||||
onClear={() => setValue("status", undefined)}
|
||||
dataCy="admins-filter-status-select"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("search")}
|
||||
@@ -50,6 +52,7 @@ const AdminListFilters = ({
|
||||
label="search"
|
||||
placeholder="search"
|
||||
type="search"
|
||||
dataCy="admins-filter-search-input"
|
||||
/>
|
||||
{/* <InputGroup
|
||||
{...filterRegister("email")}
|
||||
@@ -86,6 +89,7 @@ const AdminListFilters = ({
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
data-cy="admins-filter-submit-button"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||
>
|
||||
{isMutating ? (
|
||||
@@ -96,6 +100,7 @@ const AdminListFilters = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-cy="admins-filter-close-button"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||
onClick={() => setIsFilterModalOpen(false)}
|
||||
>
|
||||
|
||||
@@ -35,6 +35,7 @@ const ChangeStatusModalContent: FC<IProps> = ({ status, setStatus }) => {
|
||||
name="status"
|
||||
defaultValue={status}
|
||||
placeholder="Please select new Status"
|
||||
dataCy="admins-change-status-select"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -51,16 +51,21 @@ const AdminsTable = () => {
|
||||
} = useAdminsPresenter();
|
||||
|
||||
return (
|
||||
<ListWrapper>
|
||||
<ListWrapper data-cy="admins-page">
|
||||
<ListHeader
|
||||
onFilter={() => setIsFilterModalOpen(true)}
|
||||
createButtonText="Create Admin"
|
||||
dataCyPrefix="admins"
|
||||
/>
|
||||
<Table>
|
||||
<Table data-cy="admins-table">
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column} colSpan={100}>
|
||||
{columns.map((column, index) => (
|
||||
<TableHead
|
||||
key={column}
|
||||
colSpan={100}
|
||||
data-cy={`admins-table-header-${index}`}
|
||||
>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
@@ -77,31 +82,33 @@ const AdminsTable = () => {
|
||||
<TableRow
|
||||
key={item?.id || index}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
data-cy={`admins-table-row-${item?.id ?? index}`}
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-number">
|
||||
{getPaginatedRowNumber({ index })}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-full-name">
|
||||
{item?.full_name}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-email">
|
||||
{item?.email}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-username">
|
||||
{item?.username}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<TableCell className="text-start" colSpan={100} data-cy="admins-row-status">
|
||||
<Status status={item?.status ?? ""}>{item?.status}</Status>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<TableCell className="text-start xl:pr-7.5" data-cy="admins-row-actions">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
data-tooltip-id="delete"
|
||||
data-tooltip-content="Delete"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-red-500"
|
||||
data-cy={`admins-row-delete-${item?.id ?? index}`}
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsModalOpen(true);
|
||||
@@ -116,6 +123,7 @@ const AdminsTable = () => {
|
||||
data-tooltip-content="Change status"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-yellow-dark"
|
||||
data-cy={`admins-row-manage-status-${item?.id ?? index}`}
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsChangeStatusModalOpen(true);
|
||||
@@ -132,6 +140,7 @@ const AdminsTable = () => {
|
||||
data-tooltip-content="Edit"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-primary"
|
||||
data-cy={`admins-row-edit-${item?.id ?? index}`}
|
||||
onClick={() => router.push(`/admins/edit/${item.id}`)}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
|
||||
@@ -151,6 +160,8 @@ const AdminsTable = () => {
|
||||
onConfirm={() => {
|
||||
changeStatus({ id: currentUser?.id!, credentials: { status } });
|
||||
}}
|
||||
confirmText="Save"
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<ChangeStatusModalContent
|
||||
status={currentUser?.status as AdminStatus}
|
||||
|
||||
@@ -38,6 +38,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
onSubmit={handleSubmit((data) => {
|
||||
onSubmit(data);
|
||||
})}
|
||||
data-cy={editMode ? "admin-edit-form" : "admin-create-form"}
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
@@ -62,6 +63,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
required
|
||||
type="text"
|
||||
name="full_name"
|
||||
dataCy="admin-form-full-name-input"
|
||||
/>
|
||||
{errors.full_name && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
@@ -96,6 +98,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
},
|
||||
})}
|
||||
name="username"
|
||||
dataCy="admin-form-username-input"
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
@@ -122,6 +125,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
},
|
||||
})}
|
||||
name="email"
|
||||
dataCy="admin-form-email-input"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
@@ -152,6 +156,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
},
|
||||
})}
|
||||
name="password"
|
||||
dataCy="admin-form-password-input"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
@@ -180,6 +185,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
},
|
||||
})}
|
||||
name="phone"
|
||||
dataCy="admin-form-phone-input"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
@@ -203,6 +209,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
},
|
||||
})}
|
||||
name="department"
|
||||
dataCy="admin-form-department-input"
|
||||
/>
|
||||
{errors.department && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
@@ -226,6 +233,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
},
|
||||
})}
|
||||
name="position"
|
||||
dataCy="admin-form-position-input"
|
||||
/>
|
||||
{errors.position && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
|
||||
@@ -99,10 +99,12 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
data-cy="confirmation-modal"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 bg-dark/70 bg-opacity-75 transition-opacity"
|
||||
onClick={onCancel}
|
||||
data-cy="confirmation-modal-overlay"
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
@@ -139,6 +141,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${currentVariant.confirmBtn} dark:ring-offset-dark-2`}
|
||||
disabled={isMutating > 0}
|
||||
onClick={onConfirm}
|
||||
data-cy="confirmation-modal-confirm"
|
||||
>
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
@@ -150,6 +153,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2 sm:mt-0 sm:w-auto"
|
||||
onClick={onCancel}
|
||||
data-cy="confirmation-modal-cancel"
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
|
||||
@@ -24,6 +24,7 @@ const FormFooter = ({ onCancel }: IProps) => {
|
||||
shape="rounded"
|
||||
size="small"
|
||||
type="submit"
|
||||
data-cy="admin-form-submit-button"
|
||||
disabled={isMutating > 0}
|
||||
className="mt-6 w-fit rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
/>
|
||||
@@ -32,6 +33,7 @@ const FormFooter = ({ onCancel }: IProps) => {
|
||||
shape="rounded"
|
||||
size="small"
|
||||
type="button"
|
||||
data-cy="admin-form-cancel-button"
|
||||
variant="error"
|
||||
onClick={onCancel ?? router.back}
|
||||
className="mt-6 w-fit rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
|
||||
@@ -10,6 +10,7 @@ interface IProps {
|
||||
createButtonText?: string;
|
||||
filterButtonText?: string;
|
||||
onFilter?: () => void;
|
||||
dataCyPrefix?: string;
|
||||
}
|
||||
|
||||
const ListHeader = ({
|
||||
@@ -18,6 +19,7 @@ const ListHeader = ({
|
||||
createButtonText = "Create",
|
||||
filterButtonText = "Filter",
|
||||
onFilter,
|
||||
dataCyPrefix = "list-header",
|
||||
}: IProps) => {
|
||||
const pathName = usePathname();
|
||||
const router = useRouter();
|
||||
@@ -35,6 +37,7 @@ const ListHeader = ({
|
||||
shape="rounded"
|
||||
size="small"
|
||||
onClick={onFilter}
|
||||
data-cy={`${dataCyPrefix}-filter-button`}
|
||||
className="shadow-theme-xs mb-5 self-end rounded-xl border-stroke/70 bg-white/80 px-5 text-sm font-semibold text-dark transition-all duration-300 hover:border-primary/35 hover:text-primary dark:border-dark-3 dark:bg-dark-2 dark:text-white dark:hover:bg-white/[0.08]"
|
||||
/>
|
||||
</IsVisible>
|
||||
@@ -45,6 +48,7 @@ const ListHeader = ({
|
||||
shape="rounded"
|
||||
size="small"
|
||||
onClick={() => router.push(`${pathName}/create`)}
|
||||
data-cy={`${dataCyPrefix}-create-button`}
|
||||
className="shadow-theme-xs mb-5 self-end rounded-xl bg-gradient-to-r from-primary to-primary/85 text-sm font-semibold transition-all duration-300 hover:opacity-95"
|
||||
/>
|
||||
</IsVisible>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
type ListWrapperProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const ListWrapper = ({ children }: ListWrapperProps) => {
|
||||
const ListWrapper = ({ children, ...props }: ListWrapperProps) => {
|
||||
return (
|
||||
<div className="flex flex-col rounded-xl border border-stroke/70 bg-white bg-[linear-gradient(225deg,rgba(87,80,241,0.18)_0%,rgba(87,80,241,0)_10%),linear-gradient(45deg,rgba(87,80,241,0.18)_0%,rgba(87,80,241,0)_10%)] px-7.5 pb-4 pt-7.5 dark:border-dark-3 dark:bg-gray-dark dark:bg-[linear-gradient(225deg,rgba(87,80,241,0.26)_0%,rgba(87,80,241,0)_10%),linear-gradient(45deg,rgba(87,80,241,0.26)_0%,rgba(87,80,241,0)_10%)]">
|
||||
<div
|
||||
{...props}
|
||||
className="flex flex-col rounded-xl border border-stroke/70 bg-white bg-[linear-gradient(225deg,rgba(87,80,241,0.18)_0%,rgba(87,80,241,0)_10%),linear-gradient(45deg,rgba(87,80,241,0.18)_0%,rgba(87,80,241,0)_10%)] px-7.5 pb-4 pt-7.5 dark:border-dark-3 dark:bg-gray-dark dark:bg-[linear-gradient(225deg,rgba(87,80,241,0.26)_0%,rgba(87,80,241,0)_10%),linear-gradient(45deg,rgba(87,80,241,0.26)_0%,rgba(87,80,241,0)_10%)]"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,11 +13,11 @@ const TableSkeleton = ({
|
||||
cellColSpan = 100,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<TableBody>
|
||||
<TableBody data-cy="table-skeleton">
|
||||
{Array.from({ length }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableRow key={i} data-cy={`table-skeleton-row-${i}`}>
|
||||
{Array.from({ length: column }).map((_, j) => (
|
||||
<TableCell key={j} colSpan={cellColSpan}>
|
||||
<TableCell key={j} colSpan={cellColSpan} data-cy="table-skeleton-cell">
|
||||
<Skeleton className="h-8 rounded-lg" />
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
@@ -50,14 +50,17 @@ const Modal: React.FC<ModalProps> = ({
|
||||
<div
|
||||
className="fixed inset-0 z-999999 flex items-center justify-center bg-dark/50 dark:bg-black/50"
|
||||
onClick={onClose}
|
||||
data-cy="modal-overlay"
|
||||
>
|
||||
<div
|
||||
className={`max-h-[90vh] max-w-[90vw] overflow-auto rounded-lg bg-white p-5 shadow-4 dark:bg-dark-2 ${classNames}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-cy="modal-content"
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="float-right mb-2.5 cursor-pointer border-none bg-transparent p-0 text-2xl text-dark-5 hover:text-primary dark:text-gray-5 dark:hover:text-primary"
|
||||
data-cy="modal-close-button"
|
||||
>
|
||||
<CrossIcon />
|
||||
</button>
|
||||
@@ -70,6 +73,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
shape="rounded"
|
||||
className="mt-6 w-fit capitalize"
|
||||
onClick={onConfirm}
|
||||
data-cy="modal-confirm-button"
|
||||
label={
|
||||
isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
@@ -85,6 +89,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
shape="rounded"
|
||||
className="mt-6 w-fit capitalize"
|
||||
onClick={onClose}
|
||||
data-cy="modal-cancel-button"
|
||||
label={cancelText}
|
||||
/>
|
||||
</IsVisible>
|
||||
|
||||
Reference in New Issue
Block a user