WFDASH-WEBAPP/src/lib/file-fields.test.ts

87 lines
3.1 KiB
TypeScript
Raw Normal View History

fix(delivery-note): signed copy invisible, and a bigger QR in the corner TWO bugs, one of which was hiding the other. 1. "No signed copy on file" for a note that had just been signed, scanned and uploaded. The upload was never the problem — the READ path was. `delivery_notes.signed_copy` is a PB file field with `maxSelect: 1`: we WRITE it as an array (`signed_copy: [file]`), but PocketBase RETURNS A BARE STRING when exactly one file is attached. Verified on the live record after a real upload: typeof : string Array.isArray : false value : "image_ar52u2hjfq.jpg" value[0] : "i" <-- indexing yields a CHARACTER Every read site assumed `string[]`, and not one of them threw: - signed-copy GET: `Array.isArray(x) ? x[0] : null` returned null for notes that demonstrably had a copy — the reported error - delivery-note detail tooltip: `signed_copy[0]` rendered the first LETTER of the filename - delivery-notes-service: treated every signed copy as absent, so the quote went out with the UNSIGNED pdf attached - deliveries-service: reported an empty filename - deliveries page: `.join(", ")` on a string — this one THREW, on a page people open daily Adds `src/lib/file-fields.ts` (firstFileName / fileNames / hasFile) and routes all of them through it. The type was the root of the wrong assumptions, so it is now `string | string[]` with a comment pointing at the helper. 2. QR 28pt -> 52pt, moved to the top-right corner. Measured by rasterising the real PDF at 200 dpi and decoding: it now reads at 1.24 px/module, where the old size needed ~2.5 — roughly double the distance and angle tolerance. It could not simply go at top:40: the logo is an absolute 160x100pt block in the same corner, so the QR landed on top of it. That is not only ugly, it breaks the QR's quiet zone — logo ink inside the light margin makes the code refuse to decode. The logo now steps left (right: 112 -> 106, width 160 -> 132) to give the QR the true corner, and the company name is width-bounded ONLY when a logo is present. The Westscott letterhead has no matching .svg in public/letterhead/, so in practice the name still gets the full width and stays on one line. Nine tests pin the file-field shapes, including one that asserts the old `.join(", ")` call really does throw on a string, so the string case cannot be quietly dropped again.
2026-10-04 15:00:30 +00:00
import { describe, it, expect } from "vitest";
import { firstFileName, fileNames, hasFile } from "./file-fields";
/**
* Regression guard for the "No signed copy on file" bug.
*
* `delivery_notes.signed_copy` is a PB file field with `maxSelect: 1`.
* We WRITE it as an array (`signed_copy: [file]`), but PocketBase
* RETURNS a bare STRING when exactly one file is attached. Verified
* against the live record on DN26100403 after a real upload:
*
* typeof : string
* Array.isArray : false
* value : "image_ar52u2hjfq.jpg"
* value[0] : "i"
*
* Every read site that assumed `string[]` therefore failed, and none
* of them threw: `Array.isArray(x) ? x[0] : null` returns null, so the
* app reported "No signed copy on file" for a note that had just been
* signed, scanned and uploaded. `x[0]` in a tooltip returned the first
* LETTER of the filename. One site called `.join(", ")` on the string,
* which does throw.
*
* These tests pin the real shapes PB returns, so the string case can
* never be quietly dropped again.
*/
describe("firstFileName", () => {
it("reads a bare string — the shape PB actually returns for one file", () => {
expect(firstFileName("image_ar52u2hjfq.jpg")).toBe("image_ar52u2hjfq.jpg");
});
it("reads an array, for the shape we WRITE or for several files", () => {
expect(firstFileName(["a.jpg"])).toBe("a.jpg");
expect(firstFileName(["a.jpg", "b.jpg"])).toBe("a.jpg");
});
it("skips empty entries rather than returning ''", () => {
expect(firstFileName(["", "b.jpg"])).toBe("b.jpg");
expect(firstFileName("")).toBeNull();
});
it("is null for nothing attached", () => {
expect(firstFileName(null)).toBeNull();
expect(firstFileName(undefined)).toBeNull();
expect(firstFileName([])).toBeNull();
});
it("does NOT index into the string — the bug this replaces", () => {
// `"image_x.jpg"[0]` is "i". Any read site doing that was showing
// the first character of the filename as though it were the file.
const value: string = "image_ar52u2hjfq.jpg";
expect(value[0]).toBe("i");
expect(firstFileName(value)).toBe("image_ar52u2hjfq.jpg");
});
});
describe("fileNames", () => {
it("normalises both shapes to an array", () => {
expect(fileNames("a.jpg")).toEqual(["a.jpg"]);
expect(fileNames(["a.jpg", "b.jpg"])).toEqual(["a.jpg", "b.jpg"]);
expect(fileNames(null)).toEqual([]);
expect(fileNames([])).toEqual([]);
});
it("would have thrown on the string form before — .join on a string", () => {
// This is the deliveries page crash: `dn.signed_copy.join(", ")`.
const value = "image_ar52u2hjfq.jpg" as unknown as string[];
expect(() => value.join(", ")).toThrow();
expect(fileNames("image_ar52u2hjfq.jpg").join(", ")).toBe("image_ar52u2hjfq.jpg");
});
});
describe("hasFile", () => {
it("is truthy for either shape", () => {
expect(hasFile("a.jpg")).toBe(true);
expect(hasFile(["a.jpg"])).toBe(true);
});
it("is falsy when empty", () => {
expect(hasFile(null)).toBe(false);
expect(hasFile("")).toBe(false);
expect(hasFile([])).toBe(false);
expect(hasFile(undefined)).toBe(false);
});
});