| 1 | // Tests for the pricing picker's pure helpers. node home/test/fx.test.mjs |
| 2 | import assert from "node:assert/strict"; |
| 3 | import { BANDS, bandName, bandsOffered, tierForBand, regionCurrency, approxLocal, capNote } from "../web/static/fx.js"; |
| 4 | |
| 5 | let failures = 0; |
| 6 | async function test(name, fn) { |
| 7 | try { |
| 8 | await fn(); |
| 9 | console.log("PASS " + name); |
| 10 | } catch (e) { |
| 11 | console.log("FAIL " + name + " — " + (e && e.message)); |
| 12 | failures++; |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | const tiers = [ |
| 17 | { key: "family", name: "Family", label: "€5/year", eur: 5 }, |
| 18 | { key: "friends", name: "Friends", label: "€4/month", eur: 4, |
| 19 | bands: { reduced: { label: "€2/month", eur: 2 }, supporter: { label: "€5.50/month", eur: 5.5 } } }, |
| 20 | ]; |
| 21 | |
| 22 | await test("band vocabulary is the doc's four, in display order", () => { |
| 23 | assert.deepEqual(BANDS.map((b) => b.key), ["supporter", "base", "reduced", "solidarity"]); |
| 24 | assert.equal(bandName("base"), "Standard"); |
| 25 | // The messaging rules: aggregate cross-subsidy only, never "discount"/"eligible". |
| 26 | for (const b of BANDS) { |
| 27 | assert.ok(!/discount|eligible/i.test(b.line), b.key + " line breaks the messaging rules"); |
| 28 | } |
| 29 | }); |
| 30 | |
| 31 | await test("bandsOffered: base always; others once any tier defines them", () => { |
| 32 | assert.deepEqual(bandsOffered([tiers[0]]), ["base"]); |
| 33 | assert.deepEqual(bandsOffered(tiers), ["supporter", "base", "reduced"]); |
| 34 | assert.deepEqual(bandsOffered([]), ["base"]); |
| 35 | }); |
| 36 | |
| 37 | await test("tierForBand: band label when offered, base flagged when not", () => { |
| 38 | assert.deepEqual(tierForBand(tiers[1], "reduced"), { label: "€2/month", eur: 2, band: "reduced", offered: true }); |
| 39 | assert.deepEqual(tierForBand(tiers[1], "base"), { label: "€4/month", eur: 4, band: "base", offered: true }); |
| 40 | // family has no bands: a reduced selection falls back to base, UNOFFERED — |
| 41 | // the UI says "standard price" instead of implying a reduction. |
| 42 | assert.deepEqual(tierForBand(tiers[0], "reduced"), { label: "€5/year", eur: 5, band: "base", offered: false }); |
| 43 | }); |
| 44 | |
| 45 | await test("regionCurrency: locale region → ECB currency; euro area and unknown → none", () => { |
| 46 | assert.equal(regionCurrency("en-US"), "USD"); |
| 47 | assert.equal(regionCurrency("en-GB"), "GBP"); |
| 48 | assert.equal(regionCurrency("ja"), "JPY"); // maximize() infers the region |
| 49 | assert.equal(regionCurrency("de-DE"), ""); // euro area: billed in EUR already |
| 50 | assert.equal(regionCurrency("fr"), ""); |
| 51 | assert.equal(regionCurrency(""), ""); |
| 52 | assert.equal(regionCurrency("not a locale"), ""); |
| 53 | }); |
| 54 | |
| 55 | await test("approxLocal: converts and formats; silent when it can't be honest", () => { |
| 56 | const rates = { USD: 1.1, JPY: 170 }; |
| 57 | assert.equal(approxLocal(5, "USD", rates, "en"), "≈ $5.50"); |
| 58 | // Larger amounts drop the cents (an estimate pretending precision reads wrong). |
| 59 | assert.equal(approxLocal(999, "USD", rates, "en"), "≈ $1,099"); |
| 60 | // Unknown currency / missing rate / zero amount → no figure at all. |
| 61 | assert.equal(approxLocal(5, "CHF", rates, "en"), ""); |
| 62 | assert.equal(approxLocal(0, "USD", rates, "en"), ""); |
| 63 | assert.equal(approxLocal(5, "", rates, "en"), ""); |
| 64 | assert.equal(approxLocal(5, "USD", null, "en"), ""); |
| 65 | }); |
| 66 | |
| 67 | await test("capNote: the 15% promise in euros, not percent", () => { |
| 68 | assert.equal(capNote(5, "en"), "never more than €0.75 more at renewal without your say-so"); |
| 69 | assert.equal(capNote(0, "en"), ""); |
| 70 | }); |
| 71 | |
| 72 | console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); |
| 73 | process.exit(failures === 0 ? 0 : 1); |
| 74 | |