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:
SJCoetz 2026-07-31 00:01:24 +02:00
parent 4cf9c782e6
commit f9a0d08dab
6 changed files with 64 additions and 33 deletions

View file

@ -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)");
});
});

View file

@ -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();

View file

@ -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 };

View file

@ -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;
}

View file

@ -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);

View file

@ -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");