mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-01 01:21:44 +00:00
Fixes #1614 ## Problem `window.getTileUrl()` in `public/roles.js` returned the active provider's `url` property as-is. After #1533 added carto/osm/stamen providers with lazy-resolved URLs (`url: function () { ... }`), the helper returned the function itself instead of a URL template string. Callers handed that function to `L.tileLayer()`, which stringified the source as the template — every tile 404'd, the map went blank, and Leaflet logged no error. User-visible impact: node-detail inset map and analytics minimap rendered zero tiles whenever a function-`url` provider was the active dark-theme pick. ## Root cause `public/roles.js:365-381` — `return p.url || p.baseUrl;` with no `typeof === 'function'` invocation. The provider registry in `public/map-tile-providers.js:45-53` declares almost every provider with `url: function() { ... }` for lazy config resolution (cartocdn domain, OSM provider/token, Stamen API key). ## Fix One-line change in the consumer (`getTileUrl()`). Invoke `url` / `baseUrl` if it's a function; otherwise return it verbatim. `map-tile-providers.js` is not touched — it remains the source of truth for the lazy-resolver pattern. ```js var u = p.url || p.baseUrl; return (typeof u === 'function') ? u() : u; ``` ## Callers reviewed | Caller | Disposition | | --- | --- | | `public/nodes.js:94` (`_applyTilesToNodeMap`) | Routes through `window.getTileUrl()` → fixed transitively | | `public/analytics.js:2055` (`L.tileLayer(getTileUrl(), …)`) | Routes through `getTileUrl()` → fixed transitively | | No other `getTileUrl()` callers | `grep -n "getTileUrl\b" public/*.js` confirms only the two above | ## Commits (red → green) - `a2b23392` — `test(#1614): red — getTileUrl() must return string, not function` — adds `test-issue-1614-tile-url-function.js`. Verified to fail on assertion (not build error) before the fix landed; passes after. - `26fcacd1` — `fix(#1614): invoke provider url() when it's a function` — minimal one-line fix in `roles.js` plus wiring the new test into `deploy.yml` and `test-all.sh`. ## Tests Unit test asserts the public contract from three angles so any regression of either branch fails CI: 1. Dark + `url: function()` → returns a string template containing `{z}/{x}/{y}`. 2. Dark + `url: 'https://…'` → returns the string verbatim (no double-invoke). 3. Dark + `baseUrl: function()` fallback → also invoked, also returns a string. Wired into CI via `.github/workflows/deploy.yml` and `test-all.sh`. ## E2E coverage Skipped intentionally. The existing Playwright harness (`test-e2e-playwright.js`) runs against a deployed BASE_URL and is not invoked from the Go CI workflow (`deploy.yml`). Adding a new E2E flow there would require standing up a leaflet/tile-loading harness for a single one-line regression. The unit test covers the exact `getTileUrl()` contract that this bug violates and would have caught it; if reviewers want a Playwright assertion later we can add it as a follow-up. Manual verification was performed against staging (`http://analyzer-stg.00id.net/#/nodes/...`). ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean (all gates pass, PII clean, red commit verified). --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
@@ -128,6 +128,7 @@ jobs:
|
||||
node test-issue-1418-deeplink-hops-channels.js
|
||||
node test-issue-1418-polish-review.js
|
||||
node test-issue-1420-tile-providers.js
|
||||
node test-issue-1614-tile-url-function.js
|
||||
node test-issue-1438-marker-css-vars.js
|
||||
node test-issue-1562-observers-summary.js
|
||||
node test-issue-1509-nav-active-bg.js
|
||||
|
||||
+6
-1
@@ -374,7 +374,12 @@
|
||||
var id = window.MC_getDarkTileProvider();
|
||||
var p = window.MC_TILE_PROVIDERS[id];
|
||||
if (p && (p.url || p.baseUrl)) {
|
||||
return p.url || p.baseUrl;
|
||||
// #1614: providers added in #1533 (carto/osm/stamen) declare
|
||||
// `url` as a function for lazy config resolution. Invoke it so
|
||||
// we always return a string URL template; L.tileLayer otherwise
|
||||
// stringifies the function source and every tile request 404s.
|
||||
var u = p.url || p.baseUrl;
|
||||
return (typeof u === 'function') ? u() : u;
|
||||
}
|
||||
}
|
||||
} catch (_e) {}
|
||||
|
||||
@@ -37,6 +37,7 @@ node test-issue-1418-spider-fan.js
|
||||
node test-issue-1418-deeplink-hops-channels.js
|
||||
node test-issue-1418-polish-review.js
|
||||
node test-issue-1420-tile-providers.js
|
||||
node test-issue-1614-tile-url-function.js
|
||||
node test-issue-1438-marker-css-vars.js
|
||||
node test-issue-1438-customizer-mcrole.js
|
||||
node test-issue-1446-cb-preset-cascade.js
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/* test-issue-1614-tile-url-function.js — regression test for #1614.
|
||||
*
|
||||
* Bug: window.getTileUrl() in public/roles.js returns the provider's
|
||||
* `url` *property as-is*. When the active dark-mode provider declares
|
||||
* `url: function()` (carto/osm/stamen lazy resolvers added in #1533),
|
||||
* the helper returns the function itself. Callers pass that function to
|
||||
* L.tileLayer(), which stringifies the function source as the URL
|
||||
* template — every tile request 404s, the map is blank, no console error.
|
||||
*
|
||||
* Contract under test:
|
||||
* - getTileUrl() MUST return a string URL template, regardless of
|
||||
* whether the active provider declares `url` as a string or a function.
|
||||
* - It must contain the leaflet template placeholders {z}/{x}/{y}.
|
||||
* - For string-`url` providers it must return the string verbatim
|
||||
* (so we don't double-invoke or otherwise regress).
|
||||
*
|
||||
* Loads only roles.js — provider registry is mocked directly via
|
||||
* window.MC_TILE_PROVIDERS + window.MC_getDarkTileProvider so the test
|
||||
* stays focused on the consumer (the source of the bug) and would still
|
||||
* fail if map-tile-providers.js drifted.
|
||||
*/
|
||||
'use strict';
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); passed++; console.log(' ✅ ' + name); }
|
||||
catch (e) { failed++; console.log(' ❌ ' + name + ': ' + e.message); }
|
||||
}
|
||||
|
||||
function makeSandbox(theme) {
|
||||
const ctx = {
|
||||
console, setTimeout, clearTimeout,
|
||||
JSON, Date, Math, Object, Array, String, Number, Boolean, Set, Map,
|
||||
fetch: () => Promise.resolve({ ok: false, json: () => Promise.resolve({}) }),
|
||||
CustomEvent: function (type, init) { this.type = type; this.detail = (init && init.detail) || null; },
|
||||
document: {
|
||||
documentElement: { getAttribute: () => theme, style: { getPropertyValue: () => '' } },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
getElementById: () => null,
|
||||
createElement: () => ({ style: {}, appendChild: () => {}, setAttribute: () => {}, addEventListener: () => {} }),
|
||||
addEventListener: () => {},
|
||||
body: { appendChild: () => {}, style: {} },
|
||||
head: { appendChild: () => {} },
|
||||
readyState: 'complete',
|
||||
},
|
||||
window: {
|
||||
addEventListener: () => {},
|
||||
matchMedia: () => ({ matches: theme === 'dark', addEventListener: () => {} }),
|
||||
},
|
||||
};
|
||||
ctx.window.document = ctx.document;
|
||||
ctx.globalThis = ctx;
|
||||
vm.createContext(ctx);
|
||||
const src = fs.readFileSync(path.join(__dirname, 'public', 'roles.js'), 'utf8');
|
||||
vm.runInContext(src, ctx, { filename: 'public/roles.js' });
|
||||
// Mirror window.* back so bare-name refs inside roles.js (TILE_DARK etc.) resolve.
|
||||
for (const k of Object.keys(ctx.window)) if (!(k in ctx)) ctx[k] = ctx.window[k];
|
||||
return ctx;
|
||||
}
|
||||
|
||||
console.log('── #1614 getTileUrl() must return a string (not a function) ──');
|
||||
|
||||
test('dark + provider with url:function → getTileUrl returns string URL template', () => {
|
||||
const ctx = makeSandbox('dark');
|
||||
ctx.window.MC_TILE_PROVIDERS = {
|
||||
'fn-provider': {
|
||||
provider: 'carto',
|
||||
label: 'Fn Provider',
|
||||
url: function () { return 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'; },
|
||||
attribution: '© OSM © CartoDB',
|
||||
},
|
||||
};
|
||||
ctx.window.MC_getDarkTileProvider = function () { return 'fn-provider'; };
|
||||
|
||||
const out = ctx.window.getTileUrl();
|
||||
assert.strictEqual(typeof out, 'string',
|
||||
'getTileUrl() must return a string, got typeof ' + typeof out +
|
||||
' (value: ' + String(out).slice(0, 80) + '…)');
|
||||
assert.ok(/\{z\}/.test(out) && /\{x\}/.test(out) && /\{y\}/.test(out),
|
||||
'returned string must be a leaflet URL template with {z}/{x}/{y}; got: ' + out);
|
||||
});
|
||||
|
||||
test('dark + provider with url:string → getTileUrl returns it verbatim', () => {
|
||||
const ctx = makeSandbox('dark');
|
||||
const STR = 'https://tiles.example.com/dark/{z}/{x}/{y}.png';
|
||||
ctx.window.MC_TILE_PROVIDERS = {
|
||||
'str-provider': { provider: 'carto', label: 'Str', url: STR, attribution: 'x' },
|
||||
};
|
||||
ctx.window.MC_getDarkTileProvider = function () { return 'str-provider'; };
|
||||
|
||||
const out = ctx.window.getTileUrl();
|
||||
assert.strictEqual(out, STR, 'string-url provider must round-trip verbatim');
|
||||
});
|
||||
|
||||
test('dark + provider with only baseUrl as function → getTileUrl returns string', () => {
|
||||
// Defense-in-depth: roles.js falls back to p.baseUrl when p.url is missing.
|
||||
// Same function/string treatment must apply.
|
||||
const ctx = makeSandbox('dark');
|
||||
ctx.window.MC_TILE_PROVIDERS = {
|
||||
'base-fn': {
|
||||
provider: 'carto', label: 'Base',
|
||||
baseUrl: function () { return 'https://tiles.example.com/{z}/{x}/{y}.png'; },
|
||||
attribution: 'x',
|
||||
},
|
||||
};
|
||||
ctx.window.MC_getDarkTileProvider = function () { return 'base-fn'; };
|
||||
|
||||
const out = ctx.window.getTileUrl();
|
||||
assert.strictEqual(typeof out, 'string',
|
||||
'baseUrl function must also be invoked; got typeof ' + typeof out);
|
||||
assert.ok(/\{z\}/.test(out), 'returned string must contain {z}; got: ' + out);
|
||||
});
|
||||
|
||||
console.log('\n#1614 getTileUrl() string contract: ' + passed + ' passed, ' + failed + ' failed');
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user