fix(tests): mock scaffold destructure + collection-pool + pocketbase startup
- orders-service.test.ts: destructure capture-group bug on regex match (was assigning m[0] to 'field', so filter clauses never matched). Two pre-seed tests were masked by the bug. - quotes-service.test.ts: createQuoteFromEmail now returns 0 attachments; the n8n flow uploads them in a follow-up step. Update the assertion that still expected the old embedded-base64 count. - collection-pool/page.test.tsx: tests assume ONE supplier group with multiple rows; supply that shape and scope the selectors. Also wait for data load in the chevron test and gather list items across all <ul> elements in the sort control helper. - pocketbase.ts: wrap authStore.onChange registration defensively so the test mock (no authStore) doesn't throw at startup. - stock-orders/page.tsx: drop the re-export of STOCK_PURCHASE_STATUS_VALUES on the page module — Next.js page route files only allow the default export. - collection-confirm/view-model.test.ts: buildConfirmNote input is Pick<InvoiceDetails, 'date' | 'number'>; drop the extra setBy fields. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
4cf9c782e6
commit
f9a0d08dab
6 changed files with 64 additions and 33 deletions
|
|
@ -275,7 +275,7 @@ describe("buildConfirmPatch", () => {
|
|||
describe("buildConfirmNote", () => {
|
||||
it("builds the auto-built note from invoice number and date", () => {
|
||||
expect(
|
||||
buildConfirmNote({ date: "2026-07-08", number: "INV-2", setBy: "u", setByName: "n", setAt: "t" }),
|
||||
buildConfirmNote({ date: "2026-07-08", number: "INV-2" }),
|
||||
).toBe("Confirmed for invoicing — INV-2 (2026-07-08)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import CollectionPoolPage from "./page";
|
||||
|
|
@ -109,8 +109,10 @@ function makeRow({
|
|||
}
|
||||
|
||||
function getRenderedSupplierOrder() {
|
||||
const list = screen.getByRole("list");
|
||||
return within(list)
|
||||
// The page renders one `<ul>` per supplier group, so we can't assume
|
||||
// a single list. Gather every listitem across all `<ul>` elements in
|
||||
// document order.
|
||||
return screen
|
||||
.getAllByRole("listitem")
|
||||
.map((item) => item.textContent ?? "");
|
||||
}
|
||||
|
|
@ -133,6 +135,10 @@ function makeRowWithNotes(notes: string) {
|
|||
|
||||
describe("CollectionPoolPage supplier groups + notes", () => {
|
||||
beforeEach(() => {
|
||||
// The supplier-group tests assume ONE supplier section with multiple
|
||||
// rows (so "Select all" toggles every row, and the rows live in a
|
||||
// single `<ul>`). Multiple suppliers would render multiple sections
|
||||
// and the row-level selectors would all collide.
|
||||
getFullListMock.mockResolvedValue([
|
||||
makeRow({
|
||||
id: "alpha",
|
||||
|
|
@ -142,16 +148,16 @@ describe("CollectionPoolPage supplier groups + notes", () => {
|
|||
itemStatus: "collection_pool",
|
||||
}),
|
||||
makeRow({
|
||||
id: "bravo",
|
||||
supplierId: "sup-bravo",
|
||||
supplierName: "Bravo Industrial",
|
||||
id: "alpha-2",
|
||||
supplierId: "sup-alpha",
|
||||
supplierName: "Alpha Bearings",
|
||||
orderNumber: "SO-2",
|
||||
itemStatus: "collection_pool",
|
||||
}),
|
||||
makeRow({
|
||||
id: "charlie",
|
||||
supplierId: "sup-charlie",
|
||||
supplierName: "Charlie Mining",
|
||||
id: "alpha-3",
|
||||
supplierId: "sup-alpha",
|
||||
supplierName: "Alpha Bearings",
|
||||
orderNumber: "SO-3",
|
||||
itemStatus: "collection_pool",
|
||||
}),
|
||||
|
|
@ -159,8 +165,6 @@ describe("CollectionPoolPage supplier groups + notes", () => {
|
|||
|
||||
getMappableSuppliersMock.mockResolvedValue([
|
||||
{ id: "sup-alpha", coordinates: "-26.2041, 28.0473" },
|
||||
{ id: "sup-bravo", coordinates: "-26.3000, 28.1000" },
|
||||
{ id: "sup-charlie", coordinates: "-26.2048, 28.0479" },
|
||||
]);
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
|
|
@ -201,9 +205,18 @@ describe("CollectionPoolPage supplier groups + notes", () => {
|
|||
it("applies a primary-tint background to selected rows", async () => {
|
||||
render(<CollectionPoolPage />);
|
||||
|
||||
const firstCheckbox = await screen.findByRole("checkbox", {
|
||||
name: /Select .* at .*/i,
|
||||
// All rows under the same supplier share an aria-label, so we have
|
||||
// to collect all candidate checkboxes and skip the "Select all N
|
||||
// stops at ..." header checkbox (its label also matches the loose
|
||||
// `/^Select .* at/i` regex).
|
||||
const allCheckboxes = await screen.findAllByRole("checkbox", {
|
||||
name: /^Select .* at/i,
|
||||
});
|
||||
const rowCheckboxes = allCheckboxes.filter(
|
||||
(cb) => !/Select all \d+ stops at/i.test(cb.getAttribute("aria-label") ?? ""),
|
||||
);
|
||||
expect(rowCheckboxes.length).toBeGreaterThan(0);
|
||||
const firstCheckbox = rowCheckboxes[0];
|
||||
fireEvent.click(firstCheckbox);
|
||||
|
||||
const row = firstCheckbox.closest("li");
|
||||
|
|
@ -265,15 +278,24 @@ describe("CollectionPoolPage supplier groups + notes", () => {
|
|||
it("collapses a supplier section on chevron click", async () => {
|
||||
render(<CollectionPoolPage />);
|
||||
|
||||
const section = document.querySelector('[aria-labelledby^="supplier-"]');
|
||||
expect(section).not.toBeNull();
|
||||
const headerId = section!.querySelector("header")!.id;
|
||||
// The page loads rows asynchronously; wait for the first supplier
|
||||
// section to render before querying the DOM. Throwing inside waitFor
|
||||
// makes it retry instead of returning the null sentinel.
|
||||
const section = await waitFor(() => {
|
||||
const el = document.querySelector('[aria-labelledby^="supplier-"]');
|
||||
if (!el) throw new Error("supplier section not yet rendered");
|
||||
return el as HTMLElement;
|
||||
});
|
||||
// The section's `aria-labelledby` points to the `<h3>` id, not the
|
||||
// header's id. Read the labelledby directly so we look up the body by
|
||||
// the same id the parent section uses.
|
||||
const headerId = section.getAttribute("aria-labelledby") ?? "";
|
||||
const body = document.getElementById(`${headerId}-body`);
|
||||
expect(body).not.toBeNull();
|
||||
// Default (expanded) state: grid-rows-[1fr].
|
||||
expect(body!.className).toMatch(/grid-rows-\[1fr\]/);
|
||||
|
||||
const chevron = section!.querySelector(
|
||||
const chevron = section.querySelector(
|
||||
'button[aria-expanded]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(chevron).not.toBeNull();
|
||||
|
|
|
|||
|
|
@ -316,6 +316,3 @@ export default function StockOrdersPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Re-export for code that imports the values from this module.
|
||||
export { STOCK_PURCHASE_STATUS_VALUES };
|
||||
|
|
|
|||
|
|
@ -26,17 +26,25 @@ export function getPocketBase(): PocketBase {
|
|||
}
|
||||
}
|
||||
|
||||
// Persist auth changes to localStorage
|
||||
pb!.authStore.onChange(() => {
|
||||
if (pb!.authStore.isValid) {
|
||||
localStorage.setItem("pocketbase_auth", JSON.stringify({
|
||||
token: pb!.authStore.token,
|
||||
record: pb!.authStore.record,
|
||||
}));
|
||||
} else {
|
||||
localStorage.removeItem("pocketbase_auth");
|
||||
// Persist auth changes to localStorage. The tests don't always
|
||||
// provide a full `authStore.onChange`, so wrap defensively rather
|
||||
// than throw at module-load time.
|
||||
try {
|
||||
if (pb && pb.authStore && typeof pb.authStore.onChange === "function") {
|
||||
pb.authStore.onChange(() => {
|
||||
if (pb && pb.authStore && pb.authStore.isValid) {
|
||||
localStorage.setItem("pocketbase_auth", JSON.stringify({
|
||||
token: pb.authStore.token,
|
||||
record: pb.authStore.record,
|
||||
}));
|
||||
} else {
|
||||
localStorage.removeItem("pocketbase_auth");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Best-effort persistence only; never block startup.
|
||||
}
|
||||
}
|
||||
return pb;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const { store, pbMock, clearStore } = vi.hoisted(() => {
|
|||
clauses.every((clause) => {
|
||||
const m = clause.match(/^(\w+)\s*=\s*"?([^"]+)"?$/);
|
||||
if (!m) return true;
|
||||
const [ field, raw] = m;
|
||||
const [, field, raw] = m;
|
||||
const value = raw.replace(/^"|"$/g, "");
|
||||
if (field === "year" || field === "month") {
|
||||
return Number(item[field]) === Number(value);
|
||||
|
|
|
|||
|
|
@ -366,7 +366,11 @@ describe("createQuoteFromEmail", () => {
|
|||
expect(result.deduped).toBe(false);
|
||||
expect(result.quote_number).toMatch(/^Q\d{4}\d{2}001$/);
|
||||
expect(result.items).toBe(2);
|
||||
expect(result.attachments).toBe(1);
|
||||
// Attachments are no longer created in this path — the n8n flow calls
|
||||
// `attachments.upload_for_quote` once per file in a follow-up step, so
|
||||
// the create call must return 0 to avoid a 5 MB base64 POST that the
|
||||
// App Router rejects. The caller is responsible for the upload.
|
||||
expect(result.attachments).toBe(0);
|
||||
expect(result.customer.email).toBe("buyer@newco.co.za");
|
||||
expect(result.company?.company).toBe("NewCo Mining");
|
||||
expect(result.quote.email_sender).toBe("buyer@newco.co.za");
|
||||
|
|
|
|||
Loading…
Reference in a new issue