chore: update app version and enhance UI components
- Bumped NEXT_PUBLIC_APP_VERSION to 2.0.3 for the latest release. - Improved Cypress test for customer feedback page to handle delayed route transitions. - Refactored CompanyDetailsPage to enhance layout and added new components for better structure. - Updated TenderDetails to conditionally render location and currency information based on validity checks. - Added data-cy attributes to various form elements and tables for improved testing capabilities.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/// <reference types="cypress" />
|
||||
import { makeCompanyCategory } from "../../support/company-category-fixtures";
|
||||
import {
|
||||
categoryMutationSuccessBody,
|
||||
companyCategoriesListUrl,
|
||||
companyCategoryCreateUrl,
|
||||
companyCategoryEditUrl,
|
||||
} from "../../support/company-category-helpers";
|
||||
import { stubCompanyCategoriesReads } from "../../support/company-category-intercepts";
|
||||
|
||||
describe("company categories create and edit forms", () => {
|
||||
beforeEach(() => {
|
||||
cy.setAuthCookies();
|
||||
});
|
||||
|
||||
it("create: shows required error when name is empty", () => {
|
||||
stubCompanyCategoriesReads(() => []);
|
||||
cy.visit(companyCategoryCreateUrl());
|
||||
cy.getByCy("company-category-form").should("be.visible");
|
||||
cy.contains("h2", "Create Company Category").should("be.visible");
|
||||
cy.getByCy("admin-form-submit-button").click();
|
||||
cy.contains("This field is required").should("be.visible");
|
||||
});
|
||||
|
||||
it("create: shows min length for one-character name", () => {
|
||||
stubCompanyCategoriesReads(() => []);
|
||||
cy.visit(companyCategoryCreateUrl());
|
||||
cy.getByCy("company-category-form-name-input").type("A");
|
||||
cy.getByCy("admin-form-submit-button").click();
|
||||
cy.contains("This field must be at least 2 characters long").should("be.visible");
|
||||
});
|
||||
|
||||
it("create: shows min length when description is too short", () => {
|
||||
stubCompanyCategoriesReads(() => []);
|
||||
cy.visit(companyCategoryCreateUrl());
|
||||
cy.getByCy("company-category-form-name-input").type("Valid Name");
|
||||
cy.getByCy("company-category-form-description-input").type("short");
|
||||
cy.getByCy("admin-form-submit-button").click();
|
||||
cy.contains("This field must be at least 10 characters long").should("be.visible");
|
||||
});
|
||||
|
||||
it("create: submits and redirects to list with success toast", () => {
|
||||
let categories: ReturnType<typeof makeCompanyCategory>[] = [];
|
||||
stubCompanyCategoriesReads(() => categories);
|
||||
|
||||
cy.intercept("POST", "**/api/proxy/company-categories", (req) => {
|
||||
categories = [
|
||||
makeCompanyCategory(
|
||||
{
|
||||
id: "507f1f77bcf86cd799439099",
|
||||
name: "New Category",
|
||||
description: "A description with enough characters.",
|
||||
},
|
||||
99,
|
||||
),
|
||||
];
|
||||
req.reply({
|
||||
statusCode: 201,
|
||||
body: categoryMutationSuccessBody("Category created"),
|
||||
});
|
||||
}).as("createCategory");
|
||||
|
||||
cy.visit(companyCategoryCreateUrl());
|
||||
cy.getByCy("company-category-form-name-input").type("New Category");
|
||||
cy.getByCy("company-category-form-description-input").type(
|
||||
"A description with enough characters.",
|
||||
);
|
||||
cy.getByCy("admin-form-submit-button").click();
|
||||
cy.wait("@createCategory");
|
||||
// Next.js navigation can be slightly delayed in CI/headless runs.
|
||||
cy.location("pathname", { timeout: 15000 }).should("eq", "/company-categories");
|
||||
cy.contains(/success|created|Category created/i).should("be.visible");
|
||||
cy.contains("New Category").should("be.visible");
|
||||
});
|
||||
|
||||
it("create: disables submit while request is in flight", () => {
|
||||
stubCompanyCategoriesReads(() => []);
|
||||
cy.intercept("POST", "**/api/proxy/company-categories", (req) => {
|
||||
req.reply({
|
||||
delay: 1400,
|
||||
statusCode: 201,
|
||||
body: categoryMutationSuccessBody("Category created"),
|
||||
});
|
||||
}).as("slowCreate");
|
||||
|
||||
cy.visit(companyCategoryCreateUrl());
|
||||
cy.getByCy("company-category-form-name-input").type("Slow Category");
|
||||
cy.getByCy("company-category-form-description-input").type(
|
||||
"Description long enough for validation rules here.",
|
||||
);
|
||||
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("@slowCreate");
|
||||
cy.get("@slowCreate.all").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("edit: prefills fields and updates then returns to list", () => {
|
||||
const cat = makeCompanyCategory({ name: "Before Name", id: "507f1f77bcf86cd799439055" }, 5);
|
||||
let categories = [cat];
|
||||
|
||||
stubCompanyCategoriesReads(() => categories);
|
||||
cy.intercept("PUT", `**/api/proxy/company-categories/${cat.id}`, (req) => {
|
||||
categories = [
|
||||
makeCompanyCategory(
|
||||
{
|
||||
...cat,
|
||||
name: "After Name",
|
||||
},
|
||||
5,
|
||||
),
|
||||
];
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: categoryMutationSuccessBody("Category updated"),
|
||||
});
|
||||
}).as("updateCategory");
|
||||
|
||||
cy.visit(companyCategoryEditUrl(cat.id));
|
||||
cy.contains("h2", "Edit Company Category").should("be.visible");
|
||||
cy.getByCy("company-category-form-name-input").should("have.value", "Before Name");
|
||||
cy.getByCy("company-category-form-name-input").clear().type("After Name");
|
||||
cy.getByCy("admin-form-submit-button").click();
|
||||
cy.wait("@updateCategory");
|
||||
cy.location("pathname").should("eq", "/company-categories");
|
||||
cy.contains(/success|updated|Category updated/i).should("be.visible");
|
||||
cy.contains("After Name").should("be.visible");
|
||||
});
|
||||
|
||||
it("edit: cancel uses back navigation", () => {
|
||||
const cat = makeCompanyCategory({}, 1);
|
||||
stubCompanyCategoriesReads(() => [cat]);
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy(`company-categories-row-edit-${cat.id}`).click();
|
||||
cy.location("pathname").should("eq", `/company-categories/edit/${cat.id}`);
|
||||
cy.getByCy("admin-form-cancel-button").click();
|
||||
cy.location("pathname").should("eq", "/company-categories");
|
||||
});
|
||||
|
||||
it("create: cancel navigates back when opened from list", () => {
|
||||
stubCompanyCategoriesReads(() => [makeCompanyCategory({}, 1)]);
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy("company-categories-create-button").click();
|
||||
cy.location("pathname").should("eq", "/company-categories/create");
|
||||
cy.getByCy("admin-form-cancel-button").click();
|
||||
cy.location("pathname").should("eq", "/company-categories");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/// <reference types="cypress" />
|
||||
import { makeCompanyCategory } from "../../support/company-category-fixtures";
|
||||
import { companyCategoriesListUrl } from "../../support/company-category-helpers";
|
||||
import { stubCompanyCategoriesReads } from "../../support/company-category-intercepts";
|
||||
|
||||
describe("company categories list", () => {
|
||||
beforeEach(() => {
|
||||
cy.setAuthCookies();
|
||||
});
|
||||
|
||||
it("toggles published optimistically, then keeps state after PATCH succeeds", () => {
|
||||
const cat = makeCompanyCategory({ published: false }, 1);
|
||||
let categories: ReturnType<typeof makeCompanyCategory>[] = [cat];
|
||||
|
||||
stubCompanyCategoriesReads(() => categories);
|
||||
cy.intercept("PATCH", "**/api/proxy/company-categories/**/publish", (req) => {
|
||||
const m = req.url.match(/company-categories\/([^/]+)\/publish/);
|
||||
const id = m?.[1];
|
||||
if (id) {
|
||||
categories = categories.map((c) =>
|
||||
c.id === id ? { ...c, published: !c.published } : c,
|
||||
);
|
||||
}
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: { success: true, message: "Publish status updated", data: {} },
|
||||
});
|
||||
}).as("togglePublished");
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
const toggle = () => cy.getByCy("company-categories-published-toggle-" + cat.id);
|
||||
toggle().should("not.be.checked");
|
||||
toggle().click({ force: true });
|
||||
// Optimistic update (useOptimistic) before the network call finishes.
|
||||
toggle().should("be.checked");
|
||||
cy.wait("@togglePublished");
|
||||
toggle().should("be.checked");
|
||||
});
|
||||
|
||||
it("reverts published toggle when PATCH fails", () => {
|
||||
const cat = makeCompanyCategory({ published: false }, 1);
|
||||
stubCompanyCategoriesReads(() => [cat]);
|
||||
cy.intercept("PATCH", "**/api/proxy/company-categories/**/publish", {
|
||||
statusCode: 500,
|
||||
body: { success: false, message: "Server error" },
|
||||
delay: 200,
|
||||
}).as("togglePublishedFail");
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
const toggle = () => cy.getByCy("company-categories-published-toggle-" + cat.id);
|
||||
toggle().should("not.be.checked");
|
||||
toggle().click({ force: true });
|
||||
toggle().should("be.checked");
|
||||
cy.wait("@togglePublishedFail");
|
||||
toggle().should("not.be.checked");
|
||||
});
|
||||
|
||||
it("opens filter modal, searches by q, and updates the list", () => {
|
||||
const alpha = makeCompanyCategory({ name: "Alpha Row", id: "507f1f77bcf86cd799439011" }, 1);
|
||||
const beta = makeCompanyCategory({ name: "Beta Row", id: "507f1f77bcf86cd799439012" }, 2);
|
||||
let categories = [alpha, beta];
|
||||
|
||||
stubCompanyCategoriesReads(() => categories);
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.contains("Alpha Row").should("be.visible");
|
||||
cy.getByCy("company-categories-filter-button").click();
|
||||
cy.getByCy("company-categories-filter-form").should("be.visible");
|
||||
cy.getByCy("company-categories-filter-search-input").clear().type("Beta");
|
||||
cy.then(() => { categories = [beta]; });
|
||||
cy.getByCy("company-categories-filter-submit-button").click();
|
||||
cy.location("search").should("include", "q=Beta");
|
||||
cy.contains("Beta Row").should("be.visible");
|
||||
cy.contains("Alpha Row").should("not.exist");
|
||||
});
|
||||
|
||||
it("filters by published select and clears published filter", () => {
|
||||
const pub = makeCompanyCategory({ name: "Pub Cat", published: true }, 1);
|
||||
const draft = makeCompanyCategory(
|
||||
{ name: "Draft Cat", published: false, id: "507f1f77bcf86cd799439013" },
|
||||
2,
|
||||
);
|
||||
let categories = [pub, draft];
|
||||
|
||||
stubCompanyCategoriesReads(() => categories);
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy("company-categories-filter-button").click();
|
||||
cy.getByCy("company-categories-filter-published-select").select("false");
|
||||
cy.then(() => { categories = [draft]; });
|
||||
cy.getByCy("company-categories-filter-submit-button").click();
|
||||
cy.location("search").should("include", "published=false");
|
||||
cy.contains("Draft Cat").should("be.visible");
|
||||
cy.contains("Pub Cat").should("not.exist");
|
||||
|
||||
cy.getByCy("company-categories-filter-button").click();
|
||||
cy.getByCy("company-categories-filter-published-select-clear").click();
|
||||
cy.then(() => { categories = [pub, draft]; });
|
||||
cy.getByCy("company-categories-filter-submit-button").click();
|
||||
cy.location("search").should("not.include", "published=false");
|
||||
cy.contains("Pub Cat").should("be.visible");
|
||||
});
|
||||
|
||||
it("closes filter modal without applying", () => {
|
||||
const cat = makeCompanyCategory({}, 1);
|
||||
stubCompanyCategoriesReads(() => [cat]);
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy("company-categories-filter-button").click();
|
||||
cy.getByCy("company-categories-filter-form").should("be.visible");
|
||||
cy.getByCy("company-categories-filter-close-button").click();
|
||||
cy.getByCy("company-categories-filter-form").should("not.exist");
|
||||
});
|
||||
|
||||
it("navigates to create from header button", () => {
|
||||
stubCompanyCategoriesReads(() => [makeCompanyCategory({}, 1)]);
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy("company-categories-create-button").click();
|
||||
cy.location("pathname").should("eq", "/company-categories/create");
|
||||
cy.getByCy("company-category-form").should("be.visible");
|
||||
cy.contains("h2", "Create Company Category").should("be.visible");
|
||||
});
|
||||
|
||||
it("opens edit route for a row", () => {
|
||||
const cat = makeCompanyCategory({}, 1);
|
||||
stubCompanyCategoriesReads(() => [cat]);
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy(`company-categories-row-edit-${cat.id}`).click();
|
||||
cy.location("pathname").should("eq", `/company-categories/edit/${cat.id}`);
|
||||
cy.contains("h2", "Edit Company Category").should("be.visible");
|
||||
});
|
||||
|
||||
it("deletes after confirm and shows empty state", () => {
|
||||
const cat = makeCompanyCategory({}, 1);
|
||||
let categories = [cat];
|
||||
|
||||
stubCompanyCategoriesReads(() => categories);
|
||||
cy.intercept("DELETE", `**/api/proxy/company-categories/${cat.id}`, {
|
||||
statusCode: 200,
|
||||
body: { success: true, message: "Deleted", data: {} },
|
||||
}).as("deleteCategory");
|
||||
|
||||
cy.visit(companyCategoriesListUrl());
|
||||
cy.getByCy(`company-categories-row-delete-${cat.id}`).click();
|
||||
cy.getByCy("confirmation-modal").should("be.visible");
|
||||
cy.getByCy("confirmation-modal-cancel").click();
|
||||
cy.getByCy("confirmation-modal").should("not.exist");
|
||||
|
||||
cy.getByCy(`company-categories-row-delete-${cat.id}`).click();
|
||||
cy.then(() => { categories = []; });
|
||||
cy.getByCy("confirmation-modal-confirm").click();
|
||||
cy.wait("@deleteCategory");
|
||||
cy.getByCy("empty-list-message").should("contain.text", "No categories were found");
|
||||
});
|
||||
});
|
||||
@@ -366,7 +366,11 @@ describe("customers management - list, actions, filters, feedback", () => {
|
||||
cy.visit("/customers");
|
||||
cy.wait("@getCustomers");
|
||||
cy.get('button[data-tooltip-id="feedback"]').first().click();
|
||||
cy.location("pathname").should("eq", `/customers/feedback/${customer.id}`);
|
||||
// Route transition can be delayed in headless runs.
|
||||
cy.location("pathname", { timeout: 15000 }).should(
|
||||
"eq",
|
||||
`/customers/feedback/${customer.id}`,
|
||||
);
|
||||
cy.wait("@getCustomerFeedback");
|
||||
cy.contains("feedback type").should("be.visible");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export type CompanyCategoryRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
published: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
published_at: number;
|
||||
thumbnail: string;
|
||||
};
|
||||
|
||||
export const makeCompanyCategory = (
|
||||
overrides: Partial<CompanyCategoryRow> = {},
|
||||
index = 1,
|
||||
): CompanyCategoryRow => ({
|
||||
id: `507f1f77bcf86cd7994390${(10 + index).toString().padStart(2, "0")}`,
|
||||
name: `E2E Category ${index}`,
|
||||
description: "This is a sufficiently long description for category rows and forms.",
|
||||
published: false,
|
||||
created_at: 1_700_000_000 + index,
|
||||
updated_at: 1_700_000_000 + index,
|
||||
published_at: 0,
|
||||
thumbnail: "",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const categoriesListResponse = (categories: CompanyCategoryRow[]) => ({
|
||||
success: true,
|
||||
message: "ok",
|
||||
data: { categories },
|
||||
});
|
||||
|
||||
export const categoryDetailsResponse = (data: {
|
||||
name: string;
|
||||
description: string;
|
||||
thumbnail?: string;
|
||||
}) => ({
|
||||
success: true,
|
||||
message: "ok",
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
thumbnail: data.thumbnail ?? "",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Matches `useCreateCompanyCategory` / `useUpdateCompanyCategories` onSuccess (axios `response.data` shape). */
|
||||
export const categoryMutationSuccessBody = (message: string) => ({
|
||||
success: true,
|
||||
data: { message },
|
||||
});
|
||||
|
||||
export const companyCategoriesListUrl = (path = "/company-categories") =>
|
||||
`${path}?_cy=${Date.now()}`;
|
||||
|
||||
export const companyCategoryCreateUrl = () =>
|
||||
`/company-categories/create?_cy=${Date.now()}`;
|
||||
|
||||
export const companyCategoryEditUrl = (id: string) =>
|
||||
`/company-categories/edit/${id}?_cy=${Date.now()}`;
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
categoriesListResponse,
|
||||
categoryDetailsResponse,
|
||||
type CompanyCategoryRow,
|
||||
} from "./company-category-fixtures";
|
||||
|
||||
/** Subset of Cypress `cy.intercept` request used by this stub (avoids non-exported / deep Cypress types). */
|
||||
type InterceptRequest = {
|
||||
url: string;
|
||||
reply(staticResponse: { statusCode: number; body: Record<string, unknown> }): void;
|
||||
continue(): void;
|
||||
};
|
||||
|
||||
const CATEGORY_ID_IN_URL = /\/company-categories\/([a-f0-9]{24})(?:\?|\/|$)/i;
|
||||
|
||||
/**
|
||||
* Stubs GET list and GET single category. Uses `req.url` string matching so all
|
||||
* request shapes (query strings, rewritten hosts) are handled consistently.
|
||||
*/
|
||||
export function stubCompanyCategoriesReads(
|
||||
getCategories: () => CompanyCategoryRow[],
|
||||
): void {
|
||||
const reply = (req: InterceptRequest) => {
|
||||
const raw = req.url;
|
||||
const detailMatch = raw.match(CATEGORY_ID_IN_URL);
|
||||
if (detailMatch) {
|
||||
const id = detailMatch[1];
|
||||
const cat = getCategories().find((c) => c.id === id);
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: categoryDetailsResponse({
|
||||
name: cat?.name ?? "Category",
|
||||
description: cat?.description ?? "Default description long enough.",
|
||||
thumbnail: cat?.thumbnail ?? "",
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw.includes("/company-categories")) {
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: categoriesListResponse(getCategories()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
req.continue();
|
||||
};
|
||||
|
||||
cy.intercept("GET", /\/api\/proxy\/company-categories/, reply);
|
||||
cy.intercept("GET", /\/admin\/v1\/company-categories/, reply);
|
||||
}
|
||||
Reference in New Issue
Block a user