\$\{qty\}<\/div>/);
});
it("gives the quantity button an accessible label", () => {
// An unlabelled button reads as just "7" to a screen reader, which is
// no better than the div it replaced.
expect(POS_APP).toMatch(/aria-label="Set quantity for /);
});
it("keeps the steppers as +/- only", () => {
expect(POS_APP).toMatch(/changeQty\('\$\{p\.id\}',-1\)/);
expect(POS_APP).toMatch(/changeQty\('\$\{p\.id\}',1\)/);
});
it("exposes openEditLine on the global the button calls", () => {
// The inline handler guard proves the name is reachable; this proves the
// specific member the button reaches for exists.
expect(POS_APP).toMatch(/window\.__pos\s*=\s*\{[^}]*openEditLine/);
});
it("still opens the sheet on long-press", () => {
expect(POS_APP).toMatch(/startLongPress\(line\)/);
expect(POS_APP).toMatch(/openEditLine\(id\)/);
});
});
describe("POS cart long-press does not hijack the quantity controls", () => {
/** The pointerdown listener body, up to its options argument. */
const pointerdownHandler = () => {
const m = POS_APP.match(
/addEventListener\('pointerdown'[\s\S]*?\{ passive: true \}\);/,
);
expect(m, "pointerdown handler not found").not.toBeNull();
return m![0];
};
it("bails out of the pointerdown handler for .qty and .rm", () => {
// Holding "+" to ring up bulk used to pop the edit sheet open at 600ms,
// mid-tap. The steppers and the new quantity button each handle their
// own tap, so they must not start the row long-press.
expect(pointerdownHandler()).toMatch(/closest\('\.qty, \.rm'\)/);
});
it("bails BEFORE starting the timer, not after", () => {
// Order matters: an early `return` leaves no pending timer to fire.
const handler = pointerdownHandler();
const guardAt = handler.indexOf("closest('.qty, .rm')");
const startAt = handler.indexOf("startLongPress(line)");
expect(guardAt, "guard not found in handler").toBeGreaterThan(-1);
expect(startAt, "startLongPress not found in handler").toBeGreaterThan(-1);
expect(guardAt, "guard must come before startLongPress").toBeLessThan(startAt);
});
it("still falls through to the row long-press for a tap on the row body", () => {
// The guard must not disable the long-press gesture entirely.
expect(pointerdownHandler()).toMatch(/startLongPress\(line\)/);
});
});
describe("POS discount trigger costs no footer height", () => {
it("sits inside the Subtotal row rather than its own row", () => {
// The standalone .discount block was ~54px of always-on footer
// (12px padding + a 42px min-height button). The trigger now rides
// inside the existing Subtotal row, so it adds nothing vertically.
expect(POS_HTML).not.toMatch(/class="discount"/);
expect(POS_HTML).toMatch(
/
\s*
Subtotal[^<]*\s* {
// The tiers replaced the old R-off / %-off pills + number field.
// A free-typed discount is what we're deliberately removing, so its
// absence is the thing worth pinning — an input creeping back in
// would let a cashier invent an arbitrary discount again.
expect(POS_HTML).toMatch(/id="discTier20"[^>]*onclick="setDiscountTier\(20\)"/);
expect(POS_HTML).toMatch(/id="discTier40"[^>]*onclick="setDiscountTier\(40\)"/);
expect(POS_HTML).not.toMatch(/id="discInput"/);
expect(POS_HTML).not.toMatch(/setDiscountKind|applyDiscount/);
});
it("keeps every id the discount JS depends on", () => {
// paintDiscount() and toggleDiscount() look these up by id; renaming any
// of them throws on drawer open, which would break the till.
for (const id of ["discountTrigger", "discountTriggerLabel", "discountBody"]) {
expect(POS_HTML, `missing #${id}`).toMatch(new RegExp(`id="${id}"`));
}
});
it("drops the old full-width button styling", () => {
expect(POS_HTML).not.toMatch(/\.discount-trigger\{[^}]*width:100%/);
expect(POS_HTML).toMatch(/\.discount-trigger\{[^}]*border:0/);
});
it("keeps the applied-discount totals line as the always-on indicator", () => {
// The trigger is a small link now; the red totals row is what proves a
// discount is live when nobody is looking at the footer.
expect(POS_HTML).toMatch(/id="discountLine"/);
expect(POS_HTML).toMatch(/class="totals discount-line"/);
});
});
describe("POS Edit line sheet is actually visible when opened", () => {
// This is the bug that hid manual quantity entry entirely: #editSheet is
// a SIBLING of #editBackdrop, but the CSS revealed it with
// `.edit-backdrop.show .edit-sheet` — a descendant selector that cannot
// match a sibling. The sheet stayed at opacity:0 while the backdrop went
// opaque, so long-pressing a cart row showed a grey screen with nothing
// on it. Nothing threw, the build was green, and the feature looked
// simply absent.
it("reveals the sheet with its own .show rule, not a descendant of the backdrop", () => {
expect(POS_HTML).toMatch(/^\.edit-sheet\.show\{/m);
expect(POS_HTML).not.toMatch(/^\.edit-backdrop\.show \.edit-sheet/m);
});
it("stacks the sheet above the backdrop", () => {
const backdrop = POS_HTML.match(/\.edit-backdrop\{[^}]*z-index:(\d+)/)?.[1];
const sheet = POS_HTML.match(/\.edit-sheet\{[^}]*z-index:(\d+)/)?.[1];
expect(backdrop, "backdrop z-index not found").toBeDefined();
expect(sheet, "sheet z-index not found — it would render under the backdrop").toBeDefined();
expect(Number(sheet)).toBeGreaterThan(Number(backdrop));
});
it("keeps the sheet anchored to the viewport, not the document", () => {
// position:absolute resolved against an unpredictable ancestor.
expect(POS_HTML).toMatch(/\.edit-sheet\{position:fixed/);
});
it("still opens and closes both elements from JS", () => {
// openEditLine/closeEditLine add .show to the backdrop AND the sheet —
// that part was always right, which is why the bug looked like a
// missing feature rather than a broken one.
expect(POS_APP).toMatch(/editBackdrop'\)\.classList\.add\('show'\)/);
expect(POS_APP).toMatch(/editSheet'\)\.classList\.add\('show'\)/);
expect(POS_APP).toMatch(/editBackdrop'\)\.classList\.remove\('show'\)/);
expect(POS_APP).toMatch(/editSheet'\)\.classList\.remove\('show'\)/);
});
it("keeps them siblings so a tap on the sheet does not close it", () => {
// #editBackdrop carries onclick="closeEditLine()". Nesting the sheet
// inside it — the "obvious" fix — would make every tap on Save bubble
// to the backdrop and dismiss the dialog first. The backdrop must
// therefore stay an empty, immediately-closed element.
const backdropTag = POS_HTML.match(/]*>/)?.[0];
expect(backdropTag, "edit-backdrop not found").toBeDefined();
expect(backdropTag).toMatch(/id="editBackdrop"/);
const after = POS_HTML.slice(
POS_HTML.indexOf(backdropTag!) + backdropTag!.length,
);
expect(after.startsWith("
"), "backdrop must close immediately").toBe(true);
});
});
describe("POS Edit line popup is quantity-only", () => {
// Requested: no price editing from the till. The override it used to set
// was also never persisted (persistCart saves only {id, qty, p}), so it
// was a session-only thing that produced a sale record the server
// ignored. Removing it is a deletion, not a feature loss.
it("has no price field in the sheet", () => {
expect(POS_HTML).not.toMatch(/id="editPrice"/);
expect(POS_HTML).not.toMatch(/Unit price/);
expect(POS_APP).not.toMatch(/getElementById\('editPrice'\)/);
});
it("keeps the quantity field", () => {
expect(POS_HTML).toMatch(/id="editQty"/);
expect(POS_APP).toMatch(/getElementById\('editQty'\)\.value = String\(line\.qty\)/);
});
it("removes the override mechanism entirely", () => {
// Nothing else could set it — not a cart reload, not addToCart — so
// leaving the field in place would be dead code implying the till can
// still change a price.
expect(POS_APP).not.toMatch(/unitPriceOverride/);
expect(POS_HTML).not.toMatch(/price edited/);
});
it("still sends a price, and it is the shelf price", () => {
// /checkout requires unit_price; dropping the override must not drop
// the price itself.
expect(POS_APP).toMatch(/unit_price: lineUnitPrice\(\{p,qty\}\)/);
expect(POS_APP).toMatch(/selling_price: lineUnitPrice\(\{p,qty\}\)/);
expect(POS_APP).toMatch(
/function lineUnitPrice\(line\)\{\s*return Number\(line\.p\.selling_price/,
);
});
it("no longer sends the unused `edited` flag", () => {
// pos-service.ts never read it — only `unit_price` matters.
expect(POS_APP).not.toMatch(/\bedited:/);
});
});
describe("POS service worker cache tracks shell edits", () => {
it("is past v30, since pos-app.js and pos.html are served cache-first", () => {
const v = POS_HTML; // placeholder to keep the import honest
expect(v).toBeDefined();
const sw = readFileSync(join(process.cwd(), "public", "pos", "sw.js"), "utf8");
const m = sw.match(/const CACHE = "pos-v(\d+)"/);
expect(m, "sw.js CACHE constant not found").not.toBeNull();
expect(Number(m![1])).toBeGreaterThanOrEqual(33);
});
});
describe("POS [hidden] attribute actually hides", () => {
/**
* A `hidden` attribute is implemented by the UA stylesheet as
* `[hidden]{display:none}` — but ANY author rule that sets `display`
* beats it, because author styles outrank UA styles regardless of
* specificity. `.totals` and `.discount-body` both set
* `display:flex`, so `el.hidden = true` did nothing: the discount
* panel could not be collapsed, and the "Discount" row could not be
* dismissed — it stayed on screen showing the last figure even
* after the discount was cleared, because the JS only ever set
* `hidden` and never cleared the text.
*
* Both symptoms looked like two unrelated bugs. One line of CSS
* fixes both, and this guards the whole class of them.
*/
it("has a global [hidden]{display:none} override", () => {
expect(POS_HTML).toMatch(/\[hidden\]\s*\{[^}]*display:\s*none\s*!important/);
});
it("covers every element that carries a hidden attribute", () => {
// Any element with `hidden` whose class rule sets `display` would
// otherwise be permanently visible. Assert the override exists
// rather than trusting each element to have remembered one.
const risky = [
...POS_HTML.matchAll(/]*\shidden[^>]*>/g),
].filter((tag) => /\bclass="/.test(tag[0]));
expect(risky.length, "expected some hidden elements to audit").toBeGreaterThan(0);
expect(POS_HTML).toMatch(/\[hidden\]\s*\{[^}]*display:\s*none\s*!important/);
});
it("blanks the discount figure when the row is cleared", () => {
// Hiding alone left the old amount painted on screen, which the
// operator had no way to distinguish from a live one. Matched
// loosely because a comment sits between the two statements.
expect(POS_APP).toMatch(
/dl\.hidden\s*=\s*true;[\s\S]{0,400}discountLineAmount'\)\.textContent\s*=\s*''/,
);
});
});