Add dependency-free browser test suite and README

49 tests run in headless Chrome via tests/run.sh: unit tests for the pure
modules plus integration tests that drive the real page in an iframe
(filtering, URL state, bag, limits, cross-tab sync, checkout, dialogs,
accessibility and overflow checks).
This commit is contained in:
2026-11-07 21:18:38 -07:00
parent 7d34ab22f8
commit eb101c3e05
10 changed files with 652 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
import { PRODUCTS } from "../dist/js/catalog.js";
import {
MAX_QTY,
STORAGE_KEY,
cartCount,
cartLines,
cartSubtotal,
changeQty,
loadCart,
sanitizeCart,
saveCart,
} from "../dist/js/cart.js";
import { assert, equal, test } from "./harness.js";
const ids = PRODUCTS.map((p) => p.id);
function fakeStorage(initial = {}) {
const data = { ...initial };
return {
data,
getItem: (k) => (k in data ? data[k] : null),
setItem: (k, v) => void (data[k] = String(v)),
removeItem: (k) => void delete data[k],
};
}
const broken = {
getItem() { throw new Error("blocked"); },
setItem() { throw new Error("quota"); },
removeItem() { throw new Error("blocked"); },
};
test("cart: the storage key is unchanged, so existing bags carry over", () => {
equal(STORAGE_KEY, "fieldwork-bag-v1");
});
test("cart: sanitizeCart keeps valid lines", () => {
equal(sanitizeCart({ pack: 2, mug: 1 }, ids), { pack: 2, mug: 1 });
});
test("cart: sanitizeCart drops invalid quantities and unknown products", () => {
const dirty = { pack: -3, mug: 0, cap: 1.5, bottle: "2", lantern: NaN, blanket: null, ghost: 4 };
equal(sanitizeCart(dirty, ids), {});
equal(sanitizeCart({ pack: Infinity }, ids), {});
});
test("cart: sanitizeCart clamps oversized quantities", () => {
equal(sanitizeCart({ pack: 500 }, ids), { pack: MAX_QTY });
});
test("cart: sanitizeCart rejects non-objects and cannot be prototype-polluted", () => {
for (const bad of [null, undefined, [], "x", 5, true]) equal(sanitizeCart(bad, ids), {});
const polluted = JSON.parse('{"__proto__":{"pack":3},"constructor":{"mug":2}}');
equal(sanitizeCart(polluted, ids), {});
assert(({}).pack === undefined, "Object.prototype untouched");
});
test("cart: changeQty adds, removes at zero, and clamps to the limit", () => {
let cart = changeQty({}, "pack", 1);
equal(cart, { pack: 1 });
cart = changeQty(cart, "pack", 1);
equal(cart, { pack: 2 });
cart = changeQty(cart, "pack", -2);
equal(cart, {}, "line removed at zero");
equal(changeQty({}, "pack", -1), {}, "cannot go negative");
equal(changeQty({ pack: MAX_QTY }, "pack", 1), { pack: MAX_QTY }, "limit holds");
});
test("cart: changeQty is immutable", () => {
const original = Object.freeze({ pack: 1 });
const next = changeQty(original, "mug", 1);
equal(original, { pack: 1 });
assert(next !== original);
});
test("cart: counts, lines and subtotal (exact, in cents, in catalogue order)", () => {
const cart = { mug: 2, pack: 1 }; // inserted out of catalogue order
equal(cartCount(cart), 3);
equal(cartLines(cart, PRODUCTS).map((l) => [l.product.id, l.qty, l.total]), [
["pack", 1, 8600],
["mug", 2, 4800],
]);
equal(cartSubtotal(cart, PRODUCTS), 13400);
equal(cartSubtotal({}, PRODUCTS), 0);
});
test("cart: loadCart reads, sanitises, and survives bad storage", () => {
equal(loadCart(fakeStorage({ [STORAGE_KEY]: '{"pack":2}' }), ids), { pack: 2 });
equal(loadCart(fakeStorage({ [STORAGE_KEY]: "not json{" }), ids), {}, "corrupt JSON");
equal(loadCart(fakeStorage(), ids), {}, "nothing saved");
equal(loadCart(broken, ids), {}, "storage throws");
equal(loadCart(null, ids), {}, "storage unavailable");
});
test("cart: saveCart writes, clears when empty, and never throws", () => {
const storage = fakeStorage();
saveCart(storage, { pack: 1 });
equal(JSON.parse(storage.data[STORAGE_KEY]), { pack: 1 });
saveCart(storage, {});
assert(!(STORAGE_KEY in storage.data), "empty cart removes the key");
saveCart(broken, { pack: 1 });
saveCart(null, { pack: 1 });
});