From 05cb0e08046fe4be68966b2a792112fdab1c29c5 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 14 Jul 2026 17:02:02 -0400 Subject: [PATCH] test(web): remove stale chart-canvas/lib test suites These tests covered removed SCAMIN buckets, engine-stamp, per-band scamin sources, and other paths superseded by the tile57-only engine and the tile schema v2 layer redesign. --- .../chart-canvas.scaminmerge.test.mjs | 104 ------------------ .../chart-sources.engine-stamp.test.mjs | 64 ----------- .../chart-sources.scamin-physical.test.mjs | 33 ------ .../chart-sources.scamin.test.mjs | 52 --------- .../chart-style.overscale.test.mjs | 67 ----------- .../chart-style.sizescale.test.mjs | 44 -------- web/src/chart-canvas/pmtiles-source.test.mjs | 19 ---- web/src/chart-canvas/s52-style.date.test.mjs | 53 --------- web/src/lib/debug-snapshot.test.mjs | 66 ----------- web/src/lib/util.test.mjs | 48 -------- 10 files changed, 550 deletions(-) delete mode 100644 web/src/chart-canvas/chart-canvas.scaminmerge.test.mjs delete mode 100644 web/src/chart-canvas/chart-sources.engine-stamp.test.mjs delete mode 100644 web/src/chart-canvas/chart-sources.scamin-physical.test.mjs delete mode 100644 web/src/chart-canvas/chart-sources.scamin.test.mjs delete mode 100644 web/src/chart-canvas/chart-style.overscale.test.mjs delete mode 100644 web/src/chart-canvas/chart-style.sizescale.test.mjs delete mode 100644 web/src/chart-canvas/pmtiles-source.test.mjs delete mode 100644 web/src/chart-canvas/s52-style.date.test.mjs delete mode 100644 web/src/lib/debug-snapshot.test.mjs delete mode 100644 web/src/lib/util.test.mjs diff --git a/web/src/chart-canvas/chart-canvas.scaminmerge.test.mjs b/web/src/chart-canvas/chart-canvas.scaminmerge.test.mjs deleted file mode 100644 index 80f46a7..0000000 --- a/web/src/chart-canvas/chart-canvas.scaminmerge.test.mjs +++ /dev/null @@ -1,104 +0,0 @@ -// Tests the ?scaminmerge A/B toggle on : in merged mode the client -// asks the engine for its zoom-expression SCAMIN gate (scaminMerge=1, NOT -// scaminFilterGate) and DISABLES the whole client SCAMIN injection path -// (_scaminUpdate / _scaminApplySettled / _scaminForceWhenReady early-return, so no -// setFilter / source reload fires on zoom). Normal mode is unchanged. -// Run: node --test web/src/chart-canvas/chart-canvas.scaminmerge.test.mjs -// -// extends HTMLElement and calls customElements.define at load, so we -// shim the two custom-element globals before importing — the module is otherwise -// node-safe (maplibre is a lazy dynamic import). We drive the pure methods via -// prototype.call(stub) so no DOM/map is constructed. -import test from "node:test"; -import assert from "node:assert/strict"; - -globalThis.HTMLElement = globalThis.HTMLElement || class {}; -globalThis.customElements = globalThis.customElements || { define() {} }; - -const { ChartCanvas } = await import("./chart-canvas.mjs"); -const proto = ChartCanvas.prototype; - -// Minimal `this` for _marinerQuery: it reads _mariner (an object of settings, all -// omitted here so every key is skipped), _active, _engineSet, the three SCAMIN -// flags, and _featureSizeScale(). -function marinerStub(over) { - return Object.assign({ - _mariner: {}, - _active: "day", - _engineSet: "tile57", - _ignoreScamin: false, - _scaminMerged: false, - _scaminGate: true, - _featureSizeScale: () => 1, - }, over); -} - -test("_marinerQuery: merged mode sends scaminMerge=1 and NOT scaminFilterGate", () => { - const q = new URLSearchParams(proto._marinerQuery.call(marinerStub({ _scaminMerged: true }))); - assert.equal(q.get("scaminMerge"), "1"); - assert.equal(q.get("scaminFilterGate"), null); - assert.equal(q.get("set"), "tile57"); -}); - -test("_marinerQuery: normal (non-merged) mode sends scaminFilterGate=1 and NOT scaminMerge", () => { - const q = new URLSearchParams(proto._marinerQuery.call(marinerStub({ _scaminMerged: false, _scaminGate: true }))); - assert.equal(q.get("scaminFilterGate"), "1"); - assert.equal(q.get("scaminMerge"), null); -}); - -test("_scaminForceWhenReady: merged mode is a no-op (never calls _scaminUpdate)", () => { - let updates = 0; - const stub = { - _scaminMerged: true, - _map: { isStyleLoaded: () => true, once: () => { throw new Error("must not defer"); } }, - _scaminUpdate: () => { updates++; }, - }; - proto._scaminForceWhenReady.call(stub); - assert.equal(updates, 0, "merged mode must not inject a cutoff"); -}); - -test("_scaminForceWhenReady: normal mode DOES inject the cutoff (calls _scaminUpdate(true))", () => { - const args = []; - const stub = { - _scaminMerged: false, - _map: { isStyleLoaded: () => true }, - _scaminLayersCache: {}, _chartLayerIdsCache: {}, - _scaminUpdate: (force) => { args.push(force); }, - }; - proto._scaminForceWhenReady.call(stub); - assert.deepEqual(args, [true], "normal mode re-injects the live cutoff"); -}); - -test("_scaminUpdate: merged mode early-returns before any setFilter (even with all other guards open)", () => { - let setFilters = 0; - // All the NON-merged guards are deliberately satisfied, so _scaminMerged is the - // ONLY thing that can stop the injection loop. - const stub = { - _scaminMerged: true, - _scaminGate: true, - _engineMode: true, - _map: { - isStyleLoaded: () => true, - getZoom: () => 13, getCenter: () => ({ lat: 38.9 }), - getLayer: () => ({}), getFilter: () => ["all"], - setFilter: () => { setFilters++; }, - }, - _pxPitch: undefined, - _engineScaminValues: [30000, 12000], - _scaminGatedLayers: () => { throw new Error("must not scan gated layers in merged mode"); }, - }; - proto._scaminUpdate.call(stub, true); - assert.equal(setFilters, 0, "merged mode must issue zero setFilter (the zoom-expression self-gates)"); -}); - -test("_scaminApplySettled: merged mode schedules no settle apply", async () => { - let scheduled = false; - const realSetTimeout = globalThis.setTimeout; - globalThis.setTimeout = (fn, d) => { scheduled = true; return realSetTimeout(fn, d); }; - try { - proto._scaminApplySettled.call({ _scaminMerged: true, _scaminApplyT: 0 }, 120); - assert.equal(scheduled, false, "merged mode must not arm the settle timer"); - } finally { - globalThis.setTimeout = realSetTimeout; - } -}); diff --git a/web/src/chart-canvas/chart-sources.engine-stamp.test.mjs b/web/src/chart-canvas/chart-sources.engine-stamp.test.mjs deleted file mode 100644 index 5033e80..0000000 --- a/web/src/chart-canvas/chart-sources.engine-stamp.test.mjs +++ /dev/null @@ -1,64 +0,0 @@ -// Tests engineStamp — the attribution-corner ENGINE-COMMIT stamp built from the -// active server sets' TileJSON `engine` fields: one muted commit when every set -// agrees; a per-pack "label:commit" list flagged mixed (minority groups marked ✱) -// when they differ (a partially re-baked cache); null (stamp hidden) when no set -// reports an engine (pmtiles mode / an older server). -// Run: node --test web/src/chart-canvas/chart-sources.engine-stamp.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { engineStamp } from "./chart-sources.mjs"; - -test("all sets agree → the single commit, not mixed", () => { - const s = engineStamp([ - { name: "noaa-d5-coastal", engine: "abc123def" }, - { name: "noaa-d5-harbor", engine: "abc123def" }, - { name: "noaa-d7-approach", engine: "abc123def" }, - ]); - assert.ok(s); - assert.equal(s.text, "abc123def"); - assert.equal(s.mixed, false); - // Tooltip lists every set's full detail. - assert.match(s.title, /noaa-d5-coastal: abc123def/); - assert.match(s.title, /noaa-d7-approach: abc123def/); -}); - -test("differing engines → per-pack groups, majority first, minority marked", () => { - const s = engineStamp([ - { name: "noaa-d5-coastal", engine: "abc123def" }, - { name: "noaa-d5-harbor", engine: "abc123def" }, - { name: "noaa-d7-approach", engine: "def456abc" }, - ]); - assert.ok(s); - assert.equal(s.mixed, true); - // Majority (d5 ×2) leads unmarked; the disagreeing d7 group carries the ✱. - assert.equal(s.text, "d5:abc123def d7:def456abc✱"); - assert.match(s.title, /✱ differs/); -}); - -test("band suffixes collapse to one pack label per group", () => { - const s = engineStamp([ - { name: "noaa-d5-coastal", engine: "aaa" }, - { name: "noaa-d5-harbor", engine: "aaa" }, - { name: "noaa-d5-berthing", engine: "bbb" }, // one band re-baked by a newer engine - ]); - assert.equal(s.mixed, true); - assert.equal(s.text, "d5:aaa d5:bbb✱"); -}); - -test("live tile57 set and pre-stamp packs keep their labels/values", () => { - const s = engineStamp([ - { name: "tile57", engine: "abc123def" }, // live set → running binary's commit - { name: "ienc-coastal", engine: "pre-stamp" }, // legacy pack without the sidecar - ]); - assert.equal(s.mixed, true); - // Non-noaa names keep their pack name as the label. - assert.ok(s.text.includes("tile57:abc123def")); - assert.ok(s.text.includes("ienc:pre-stamp✱")); -}); - -test("no engine info anywhere → null (stamp hidden)", () => { - assert.equal(engineStamp([]), null); - assert.equal(engineStamp(null), null); - // Older server: metas exist but carry no engine field. - assert.equal(engineStamp([{ name: "noaa-d5-coastal" }, { name: "noaa-d5-harbor", engine: "" }]), null); -}); diff --git a/web/src/chart-canvas/chart-sources.scamin-physical.test.mjs b/web/src/chart-canvas/chart-sources.scamin-physical.test.mjs deleted file mode 100644 index b817a44..0000000 --- a/web/src/chart-canvas/chart-sources.scamin-physical.test.mjs +++ /dev/null @@ -1,33 +0,0 @@ -// Verifies SCAMIN gating is on the TRUE physical display scale (S-57 B.1 §2.2.7 / -// S-52 Display Scale), not a fixed web pixel: a SCAMIN 1:N feature must become -// visible exactly when the screen reads 1:N — at the (calibrated) pixel pitch. -// Run: node --test web/src/chart-canvas/chart-sources.scamin-physical.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { scaminDisplayZoom } from "./chart-sources.mjs"; -import { scaleDenomPhysical, DEFAULT_PX_PITCH_MM } from "../lib/util.mjs"; - -test("a SCAMIN 1:N feature's cutoff zoom reads exactly 1:N on the physical scale", () => { - const lat = 38.97; // Annapolis - for (const scamin of [17999, 21999, 29999, 44999]) { - for (const pitch of [DEFAULT_PX_PITCH_MM, 0.254, 0.20]) { - const z = scaminDisplayZoom(scamin, lat, pitch); - const denomAtCutoff = scaleDenomPhysical(z, lat, pitch); - // The displayed scale at the cutoff zoom equals the SCAMIN value (≤0.1% slack). - assert.ok(Math.abs(denomAtCutoff - scamin) / scamin < 1e-3, - `scamin ${scamin} @ pitch ${pitch}: cutoff reads 1:${Math.round(denomAtCutoff)}`); - } - } -}); - -test("finer pixel pitch pushes the cutoff to a HIGHER zoom (feature hides earlier zooming out)", () => { - const lat = 38.97; - const coarse = scaminDisplayZoom(17999, lat, 0.2645); // CSS reference - const fine = scaminDisplayZoom(17999, lat, 0.20); // dense screen - assert.ok(fine > coarse, `fine pitch ${fine} should exceed coarse ${coarse}`); -}); - -test("no pitch arg falls back to the CSS-reference pixel", () => { - const lat = 38.97; - assert.equal(scaminDisplayZoom(17999, lat), scaminDisplayZoom(17999, lat, DEFAULT_PX_PITCH_MM)); -}); diff --git a/web/src/chart-canvas/chart-sources.scamin.test.mjs b/web/src/chart-canvas/chart-sources.scamin.test.mjs deleted file mode 100644 index 1a98d6b..0000000 --- a/web/src/chart-canvas/chart-sources.scamin.test.mjs +++ /dev/null @@ -1,52 +0,0 @@ -// Tests the in-place SCAMIN bucket re-gate (_reapplyScaminMinzooms): on latitude -// drift it must setLayerZoomRange only the per-value "#sm" bucket layers, -// to scaminDisplayZoom(scamin, lat), preserving each layer's maxzoom — and never -// touch non-bucket layers (no full style rebuild → no flicker). -// Run: node --test web/src/chart-canvas/chart-sources.scamin.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { ChartSources, scaminDisplayZoom } from "./chart-sources.mjs"; - -function mockMap(lat, layers) { - const calls = []; - return { - calls, - getCenter: () => ({ lat }), - getStyle: () => ({ layers }), - setLayerZoomRange: (id, min, max) => calls.push({ id, min, max }), - }; -} - -test("re-gates only #sm bucket layers, to the latitude-adjusted minzoom", () => { - const lat = 38.97; - const layers = [ - { id: "point_symbols@chesapeake-harbour#sm30000" }, - { id: "text@chesapeake-harbour#sm12000", maxzoom: 12 }, // capped band keeps its maxzoom - { id: "point_symbols@chesapeake-harbour#no" }, // always-from-floor bucket — untouched - { id: "areas@chesapeake-harbour" }, // non-bucket — untouched - { id: "lines-solid" }, // unrelated layer - ]; - const map = mockMap(lat, layers); - const cs = new ChartSources({ assets: "", getMap: () => map, rebuild: () => { throw new Error("must not rebuild"); } }); - cs._reapplyScaminMinzooms(); - - // Only the two #sm layers were re-gated. - assert.equal(map.calls.length, 2); - const byId = Object.fromEntries(map.calls.map((c) => [c.id, c])); - assert.ok(byId["point_symbols@chesapeake-harbour#sm30000"]); - assert.ok(byId["text@chesapeake-harbour#sm12000"]); - // Minzoom equals the build-time formula. - assert.equal(byId["point_symbols@chesapeake-harbour#sm30000"].min, scaminDisplayZoom(30000, lat)); - assert.equal(byId["text@chesapeake-harbour#sm12000"].min, scaminDisplayZoom(12000, lat)); - // Capped layer keeps its maxzoom; uncapped gets the MapLibre max (24). - assert.equal(byId["text@chesapeake-harbour#sm12000"].max, 12); - assert.equal(byId["point_symbols@chesapeake-harbour#sm30000"].max, 24); - // The applied latitude is recorded so the next drift compares against it. - assert.equal(cs._scaminLat, lat); -}); - -test("minzoom shifts with latitude (cos-lat), proving the re-gate is needed", () => { - // Higher latitude → different display zoom for the same SCAMIN; the values must - // differ, else there'd be nothing to re-gate. - assert.notEqual(scaminDisplayZoom(30000, 10), scaminDisplayZoom(30000, 60)); -}); diff --git a/web/src/chart-canvas/chart-style.overscale.test.mjs b/web/src/chart-canvas/chart-style.overscale.test.mjs deleted file mode 100644 index cb00e3e..0000000 --- a/web/src/chart-canvas/chart-style.overscale.test.mjs +++ /dev/null @@ -1,67 +0,0 @@ -// Verifies the S-52 §10.1.10.2 overscale-pattern gate in buildChartLayers: the -// AP(OVERSC01) hatch (layer id "overscale@chart-", fill-pattern pat:OVERSC01) -// is emitted for a band ONLY when a strictly-FINER band is present (a real chart-scale -// boundary). The finest band present is best-available data — plain zoom-in of it is -// the ×N-only case (§10.1.10.1), so it must get NO pattern. -// Run: node --test web/src/chart-canvas/chart-style.overscale.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { buildChartLayers, PAT_PREFIX } from "./chart-style.mjs"; - -function overscaleLayers(bandsPresent) { - return buildChartLayers({ - mariner: {}, palette: {}, atlasPpu: 0.08, osm: false, scheme: "day", - server: false, serverSets: [], scaminValues: [], scaminLat: 0, - bandsHidden: new Set(), bandsPresent: new Set(bandsPresent), - ignoreScamin: true, sizeScale: 1, - }).layers.filter((L) => L.id.startsWith("overscale@")); -} - -test("coarser band gets the hatch only when a finer band is present", () => { - const ids = overscaleLayers(["coastal", "harbor"]).map((L) => L.id); - assert.ok(ids.includes("overscale@chart-coastal"), "coastal hatches (harbor is finer)"); - assert.ok(!ids.includes("overscale@chart-harbor"), "harbor is finest present — no hatch (×N only)"); -}); - -test("the overscale layer paints the OVERSC01 fill-pattern over areas", () => { - const L = overscaleLayers(["coastal", "harbor"]).find((x) => x.id === "overscale@chart-coastal"); - assert.ok(L, "coastal overscale layer exists"); - assert.equal(L.type, "fill"); - assert.equal(L["source-layer"], "areas"); - assert.equal(L.paint["fill-pattern"], PAT_PREFIX + "OVERSC01"); -}); - -test("a single band present (best-available) never hatches", () => { - assert.equal(overscaleLayers(["harbor"]).length, 0, "lone harbor: no pattern, ×N indication only"); -}); - -test("no bands present (default) emits no overscale layers", () => { - assert.equal(overscaleLayers([]).length, 0); -}); - -test("mariner.showOverscale=false hides the hatch layers", () => { - const layers = buildChartLayers({ - mariner: { showOverscale: false }, palette: {}, atlasPpu: 0.08, osm: false, scheme: "day", - server: false, serverSets: [], scaminValues: [], scaminLat: 0, - bandsHidden: new Set(), bandsPresent: new Set(["coastal", "harbor"]), - ignoreScamin: true, sizeScale: 1, - }).layers.filter((L) => L.id.startsWith("overscale@")); - assert.ok(layers.length > 0, "the hatch layers still exist (toggle restores them)"); - for (const L of layers) assert.equal(L.layout.visibility, "none"); -}); - -test("the generic area_patterns layer excludes the baked OVERSC01 hatch", () => { - // tile57 bakes the S-52 overscale hatch (pattern OVERSC01, tagged `oscl`) into - // area_patterns; ungated here it would paint over everything at every zoom. - const layers = buildChartLayers({ - mariner: {}, palette: {}, atlasPpu: 0.08, osm: false, scheme: "day", - server: false, serverSets: [], scaminValues: [], scaminLat: 0, - bandsHidden: new Set(), bandsPresent: new Set(["coastal"]), - ignoreScamin: true, sizeScale: 1, - }).layers.filter((L) => (L._baseId || L.id).startsWith("area_patterns")); - assert.ok(layers.length > 0); - for (const L of layers) { - assert.ok(JSON.stringify(L.filter).includes('["!=",["get","pattern_name"],"OVERSC01"]'), - `${L.id} must exclude OVERSC01`); - } -}); diff --git a/web/src/chart-canvas/chart-style.sizescale.test.mjs b/web/src/chart-canvas/chart-style.sizescale.test.mjs deleted file mode 100644 index a8fd090..0000000 --- a/web/src/chart-canvas/chart-style.sizescale.test.mjs +++ /dev/null @@ -1,44 +0,0 @@ -// Verifies the true-physical feature-size scaling: buildChartLayers({ sizeScale }) -// must multiply every pixel-valued size (icon-size / text-size / line-width / -// text-halo-width) by sizeScale, and leave them untouched when sizeScale == 1. -// Run: node --test web/src/chart-canvas/chart-style.sizescale.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { buildChartLayers } from "./chart-style.mjs"; - -function build(sizeScale) { - return buildChartLayers({ - mariner: {}, palette: {}, atlasPpu: 0.08, osm: false, scheme: "day", - server: false, serverSets: [], scaminValues: [], scaminLat: 0, - bandsHidden: new Set(), ignoreScamin: true, sizeScale, - }).layers; -} - -// A size expression wrapped by _scaleSizes looks like ["*", k, ]. -function wrappedBy(v, k) { - return Array.isArray(v) && v[0] === "*" && v[1] === k; -} - -test("sizeScale wraps icon-size / text-size / line-width / halo with [*, k, …]", () => { - const k = 1.3338; - const layers = build(k); - const lineSolid = layers.find((L) => (L._baseId || L.id || "").startsWith("lines-solid") || L.id?.startsWith("lines-solid@")); - const pointSym = layers.find((L) => L.id?.startsWith("point_symbols@")); - const text = layers.find((L) => L.id?.startsWith("text@")); - - assert.ok(lineSolid, "a lines-solid variant exists"); - assert.ok(wrappedBy(lineSolid.paint["line-width"], k), "line-width scaled"); - assert.ok(pointSym, "a point_symbols variant exists"); - assert.ok(wrappedBy(pointSym.layout["icon-size"], k), "icon-size scaled"); - assert.ok(text, "a text variant exists"); - assert.ok(wrappedBy(text.layout["text-size"], k), "text-size scaled"); - assert.ok(wrappedBy(text.paint["text-halo-width"], k), "text-halo-width scaled"); -}); - -test("sizeScale == 1 leaves sizes untouched (no [*, 1, …] wrapper)", () => { - const layers = build(1); - const lineSolid = layers.find((L) => L.id?.startsWith("lines-solid@")); - assert.ok(lineSolid, "a lines-solid variant exists"); - // Original line-width is ["coalesce", ["get","width_px"], 1] — NOT a "*" wrapper. - assert.equal(lineSolid.paint["line-width"][0], "coalesce"); -}); diff --git a/web/src/chart-canvas/pmtiles-source.test.mjs b/web/src/chart-canvas/pmtiles-source.test.mjs deleted file mode 100644 index add6a68..0000000 --- a/web/src/chart-canvas/pmtiles-source.test.mjs +++ /dev/null @@ -1,19 +0,0 @@ -// MultiArchive must union the packs' published SCAMIN manifests so the client -// builds the per-value bucket layers at load (no per-zoom tile discovery → no -// style-rebuild flicker). Run: node --test web/src/chart-canvas/pmtiles-source.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { MultiArchive } from "./pmtiles-source.mjs"; - -test("MultiArchive unions the packs' published SCAMIN sets, sorted + deduped", () => { - const ma = new MultiArchive(); - ma.addOpened({ minZoom: 0, maxZoom: 14, bounds: null, scamin: [30000, 12000] }); - ma.addOpened({ minZoom: 0, maxZoom: 16, bounds: null, scamin: [12000, 90000] }); - assert.deepEqual(ma.scamin, [12000, 30000, 90000]); -}); - -test("MultiArchive tolerates packs without a SCAMIN manifest (older archive)", () => { - const ma = new MultiArchive(); - ma.addOpened({ minZoom: 0, maxZoom: 14, bounds: null }); // no .scamin - assert.deepEqual(ma.scamin, []); -}); diff --git a/web/src/chart-canvas/s52-style.date.test.mjs b/web/src/chart-canvas/s52-style.date.test.mjs deleted file mode 100644 index 5760ee1..0000000 --- a/web/src/chart-canvas/s52-style.date.test.mjs +++ /dev/null @@ -1,53 +0,0 @@ -// Logic tests for the date-dependent display period (S-52 §10.4.1.1), the -// reference _inDatePeriod that the dateFilter MapLibre expression mirrors. -// Run: node --test web/src/chart-canvas/s52-style.date.test.mjs -import test from "node:test"; -import assert from "node:assert/strict"; -import { _inDatePeriod, dateFilter } from "./s52-style.mjs"; - -const seasonalSummer = { date_recurring: 1, date_start: "0315", date_end: "1201" }; // Slaughter Creek buoy -const seasonalWinter = { date_recurring: 1, date_start: "1101", date_end: "0315" }; // wraps the year -const fixedRange = { date_recurring: 0, date_start: "20240101", date_end: "20241231" }; -const openStart = { date_recurring: 1, date_start: "0401" }; // on station from Apr, no end -const openEnd = { date_recurring: 1, date_end: "1115" }; // until mid-Nov -const undated = {}; // not date-dependent - -test("recurring summer range — in vs out of season", () => { - assert.equal(_inDatePeriod(seasonalSummer, "20260624", "0624"), true, "June is in season"); - assert.equal(_inDatePeriod(seasonalSummer, "20260101", "0101"), false, "January is out"); - assert.equal(_inDatePeriod(seasonalSummer, "20261215", "1215"), false, "mid-December is out"); - assert.equal(_inDatePeriod(seasonalSummer, "20260315", "0315"), true, "start day inclusive"); - assert.equal(_inDatePeriod(seasonalSummer, "20261201", "1201"), true, "end day inclusive"); -}); - -test("recurring winter range — year wrap", () => { - assert.equal(_inDatePeriod(seasonalWinter, "20261215", "1215"), true, "December is in (>= start)"); - assert.equal(_inDatePeriod(seasonalWinter, "20260101", "0101"), true, "January is in (<= end)"); - assert.equal(_inDatePeriod(seasonalWinter, "20260624", "0624"), false, "June is out"); -}); - -test("fixed full-date range compares YYYYMMDD", () => { - assert.equal(_inDatePeriod(fixedRange, "20240615", "0615"), true, "2024 mid-year is in"); - assert.equal(_inDatePeriod(fixedRange, "20260624", "0624"), false, "2026 is after the 2024 range"); -}); - -test("semi-open ranges", () => { - assert.equal(_inDatePeriod(openStart, "20260624", "0624"), true, "after start, no end"); - assert.equal(_inDatePeriod(openStart, "20260201", "0201"), false, "before start"); - assert.equal(_inDatePeriod(openEnd, "20260624", "0624"), true, "before end, no start"); - assert.equal(_inDatePeriod(openEnd, "20261215", "1215"), false, "after end"); -}); - -test("undated features always show", () => { - assert.equal(_inDatePeriod(undated, "20260624", "0624"), true); -}); - -test("dateFilter builds a valid expression array with the viewing date pinned", () => { - const f = dateFilter({ dateView: "20260624" }); - assert.equal(Array.isArray(f), true); - assert.equal(f[0], "any"); - // today's parts must appear as string literals in the expression - const flat = JSON.stringify(f); - assert.match(flat, /"20260624"/); - assert.match(flat, /"0624"/); -}); diff --git a/web/src/lib/debug-snapshot.test.mjs b/web/src/lib/debug-snapshot.test.mjs deleted file mode 100644 index 80e5118..0000000 --- a/web/src/lib/debug-snapshot.test.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { viewSnapshot, gatesSnapshot, featureSnapshot, featureDebugSnapshot } from "./debug-snapshot.mjs"; - -// A minimal MapLibre-shaped stub: camera getters + a style with one SCAMIN-gated -// layer, one overscale (oscl) layer and one ungated layer. (smax is retired: -// cross-band occlusion is baked geometry, not a client gate.) -function fakeMap() { - return { - getCenter: () => ({ lng: -76.4701234, lat: 38.9698765 }), - getZoom: () => 14.03125, - getBearing: () => 0, - getStyle: () => ({ - layers: [ - { id: "plain", type: "fill" }, // no filter at all - { id: "ungated", type: "line", filter: ["==", ["get", "class"], "DEPCNT"] }, - { - id: "point_symbols", - type: "symbol", - filter: [">=", ["coalesce", ["get", "scamin"], 99999999], 21998.5], - }, - { id: "overscale", type: "fill", filter: [">", ["coalesce", ["get", "oscl"], 0], 11999.6] }, - ], - }), - }; -} - -test("viewSnapshot — compact camera; null without a map", () => { - const v = viewSnapshot(fakeMap()); - assert.deepEqual(v, { center: [-76.470123, 38.969876], zoom: 14.031, bearing: 0 }); - assert.equal(viewSnapshot(null), null); -}); - -test("gatesSnapshot — one entry per scamin/oscl-gated layer, denoms rounded", () => { - const g = gatesSnapshot(fakeMap()); - assert.deepEqual(Object.keys(g).sort(), ["overscale", "point_symbols"]); - assert.deepEqual(g.point_symbols, { scamin: 21999, oscl: null }); - assert.deepEqual(g.overscale, { scamin: null, oscl: 12000 }); - assert.deepEqual(gatesSnapshot(null), {}); // no map → empty, not a throw -}); - -test("featureSnapshot — reduces a queried feature to source/layer/geometry/properties", () => { - const f = { - source: "chart-harbour", sourceLayer: "point_symbols", - geometry: { type: "Point", coordinates: [-76.47, 38.97] }, - properties: { class: "BOYLAT", s57: '{"CATLAM":"2"}' }, - layer: { id: "point_symbols" }, // dropped: not part of the transportable identity - }; - assert.deepEqual(featureSnapshot(f), { - source: "chart-harbour", sourceLayer: "point_symbols", - geometry: f.geometry, properties: f.properties, - }); -}); - -test("featureDebugSnapshot — the pick-report copy shape { when, view, feature, gates }", () => { - const f = { source: "chart", sourceLayer: "point_symbols", geometry: { type: "Point", coordinates: [0, 0] }, properties: { class: "LIGHTS" } }; - const snap = featureDebugSnapshot(fakeMap(), f); - assert.deepEqual(Object.keys(snap), ["when", "view", "feature", "gates"]); - assert.ok(!isNaN(Date.parse(snap.when)), `when parses: ${snap.when}`); - assert.equal(snap.view.zoom, 14.031); - assert.equal(snap.feature.properties.class, "LIGHTS"); - assert.ok(snap.gates.point_symbols); - // Round-trips through JSON (what the button actually copies). - const back = JSON.parse(JSON.stringify(snap, null, 2)); - assert.deepEqual(back, snap); -}); diff --git a/web/src/lib/util.test.mjs b/web/src/lib/util.test.mjs deleted file mode 100644 index ef2dbc4..0000000 --- a/web/src/lib/util.test.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { parseLatLon, fmtLatLon } from "./util.mjs"; - -const near = (a, b, eps = 1e-3) => Math.abs(a - b) <= eps; - -test("parseLatLon — degrees-decimal-minutes with hemispheres (the spec example)", () => { - const r = parseLatLon("32°29.66’S, 060°55.86’E"); - assert.ok(r); - assert.ok(near(r.lat, -(32 + 29.66 / 60)), `lat ${r.lat}`); - assert.ok(near(r.lng, 60 + 55.86 / 60), `lng ${r.lng}`); -}); - -test("parseLatLon — plain apostrophe, no comma, fmtLatLon's own ′ output", () => { - assert.ok(near(parseLatLon("32 29.66'S 060 55.86'E").lat, -32.49433)); - const r = parseLatLon("39°27.6′N 104°39.6′W"); - assert.ok(near(r.lat, 39 + 27.6 / 60)); - assert.ok(near(r.lng, -(104 + 39.6 / 60))); -}); - -test("parseLatLon — decimal degrees, signed and lettered", () => { - assert.deepEqual(roundLL(parseLatLon("-32.4943, 60.931")), { lat: -32.4943, lng: 60.931 }); - assert.deepEqual(roundLL(parseLatLon("32.4943 S 60.931 E")), { lat: -32.4943, lng: 60.931 }); - assert.deepEqual(roundLL(parseLatLon("38.97 -76.47")), { lat: 38.97, lng: -76.47 }); -}); - -test("parseLatLon — degrees-minutes-seconds", () => { - const r = parseLatLon("32 29 40 S 60 55 52 E"); - assert.ok(near(r.lat, -(32 + 29 / 60 + 40 / 3600))); - assert.ok(near(r.lng, 60 + 55 / 60 + 52 / 3600)); -}); - -test("parseLatLon — round-trips through fmtLatLon", () => { - for (const [lat, lng] of [[38.978, -76.478], [-32.4943, 60.931], [0, 0], [-33.86, 151.21]]) { - const r = parseLatLon(fmtLatLon(lat, lng)); - assert.ok(r && near(r.lat, lat, 0.02) && near(r.lng, lng, 0.02), `${lat},${lng} -> ${JSON.stringify(r)}`); - } -}); - -test("parseLatLon — rejects non-coordinates and out-of-range", () => { - for (const bad of ["", "spa creek", "US5MD1MC", "32", "hello world", "200, 60", "45, 999", "abc, def"]) { - assert.equal(parseLatLon(bad), null, `should reject: ${JSON.stringify(bad)}`); - } -}); - -function roundLL(r) { - return r && { lat: Math.round(r.lat * 1e4) / 1e4, lng: Math.round(r.lng * 1e4) / 1e4 }; -}