mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-12 08:37:33 +00:00
Fixes #2236. A display that can only be given a URL (digital signage, an iframe without cookies or login) had no way to choose dark or light. This adds `theme=dark` or `theme=light` to the hash query, next to `embed=1`: ``` https://<instance>/#/map?embed=1&theme=dark ``` ## What changes **`public/index.html`, the pre-paint script** - Reads `theme` from the hash query with `URLSearchParams`, as `shouldEmbedRoute` does for `embed`. Only `dark` and `light` count; anything else is ignored. - A valid value wins over the saved choice and the OS preference, and is set before first paint, so there is no light flash. - The value is handed to `app.js` as `window.__themeOverride`. - When `localStorage` throws (a sandboxed iframe), the URL value is still applied. **`public/app.js`, the dark-mode setup** - On load, a URL value is applied through `applyTheme(theme, false)`, which does not write `meshcore-theme`. The saved choice, and the account settings that `settings-sync.js` syncs from that key, stay as they were. - Theme changes from other tabs or from settings sync (the `storage` listener) are ignored while the URL value holds. Otherwise a logged-in display would flip within a minute. - A click on the dark-mode toggle ends the URL value and is saved as before. It works on every page, not only on the two `embed=1` pages. The value is read once per page load, so later hash changes (navigation, map viewport) keep it. ## Tests | File | Assertions | Covers | |---|---|---| | `tests/unit/test-theme-url-param-2236.js` | 8 | Runs the real pre-paint script from `index.html`: `dark` and `light` win over a saved choice and the OS, nothing is written to `localStorage`, any page and position in the query, an unknown value, no param, `localStorage` throwing | | `tests/e2e/test-theme-url-param-2236-e2e.js` | 6 steps | `#/map?embed=1&theme=dark` over a saved "light" (dark, embed, saved still "light"), a reload without the param, `theme=light` over a saved "dark", a storage event while the override holds, the toggle ending it, `theme=blue` | How I checked them: - With `index.html` and `app.js` from master, 6 of the 8 unit assertions fail, and the E2E fails at step 1 ("expected dark + embed + saved light"). - With only the `app.js` change reverted, the E2E fails at step 1. With only the `if (themeOverride) return;` line in the `storage` listener removed, it fails at step 4. - `sh test-all.sh` passes, except `test-issue-1956-release-routing.js`, which I skipped: it needs the workflow's `GITHUB_REF_NAME` and does not run in local git-bash. `npx eslint public/app.js` reports 0 errors (18 warnings, the same count as master). - E2E on a fixture seeded as in CI: | Suite | Result | |---|---| | `test-e2e-playwright.js` | 148 passed, 3 skipped | | `test-customize-theme-e2e.js` | 8/8 | | the new #2236 suite | 6/6 | - `test-logo-theme-e2e.js` fails locally with "wordmark `<text>` overflows SVG viewBox", with the same numbers on master. It measures font metrics, which differ on this Windows machine. The new E2E is registered in `scripts/non-unit-tests.json` and `deploy.yml` (shard 2, next to the logo theme suites); the unit test is in `test-all.sh`. ## Not done - No screenshot: the E2E checks `data-theme` in headless Chromium, not how the page looks. - `live.js` Matrix mode forces dark and, when switched off, saves the theme it restores. With a URL value active, turning Matrix off saves that value as the stored choice. I left this as is, because a display that only shows a URL does not toggle Matrix mode. - Only `theme=`; the `?darkMode=1` form from the issue text is not supported, so there is one form to document. - I placed the E2E in shard 2 without measuring suite times. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.4 KiB
JavaScript
85 lines
3.4 KiB
JavaScript
/* Unit tests for issue #2236: ?theme=dark|light in the hash query.
|
|
*
|
|
* Runs the real pre-paint script from public/index.html in a vm sandbox and
|
|
* checks the theme it sets before first paint, and the override it hands to
|
|
* app.js (window.__themeOverride).
|
|
*
|
|
* Usage: node tests/unit/test-theme-url-param-2236.js
|
|
*/
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
const assert = require('assert');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'index.html'), 'utf8');
|
|
const m = /<script>\s*(\(function \(\) \{[\s\S]*?meshcore-theme[\s\S]*?\}\)\(\);)\s*<\/script>/.exec(html);
|
|
if (!m) { console.log(' ❌ pre-paint theme script not found in index.html'); process.exit(1); }
|
|
const SRC = m[1];
|
|
|
|
let passed = 0, failed = 0;
|
|
function test(name, fn) {
|
|
try { fn(); passed++; console.log(' ✅ ' + name); }
|
|
catch (e) { failed++; console.log(' ❌ ' + name + ': ' + e.message); }
|
|
}
|
|
|
|
function run(hash, opts) {
|
|
opts = opts || {};
|
|
const attrs = {};
|
|
const store = Object.assign({}, opts.stored);
|
|
const ctx = {
|
|
URLSearchParams,
|
|
location: { hash: hash },
|
|
document: { documentElement: { setAttribute: (k, v) => { attrs[k] = v; } } },
|
|
localStorage: opts.blocked
|
|
? { getItem: () => { throw new Error('SecurityError'); } }
|
|
: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
|
|
matchMedia: () => ({ matches: !!opts.osDark }),
|
|
};
|
|
ctx.window = ctx;
|
|
vm.createContext(ctx);
|
|
vm.runInContext(SRC, ctx);
|
|
return { theme: attrs['data-theme'], override: ctx.__themeOverride, store };
|
|
}
|
|
|
|
console.log('issue #2236 — pre-paint theme from the URL');
|
|
test('theme=dark wins over a saved light choice', () => {
|
|
const r = run('#/map?embed=1&theme=dark', { stored: { 'meshcore-theme': 'light' } });
|
|
assert.strictEqual(r.theme, 'dark');
|
|
assert.strictEqual(r.override, 'dark');
|
|
});
|
|
test('theme=light wins over a saved dark choice and a dark OS', () => {
|
|
const r = run('#/channels?theme=light', { stored: { 'meshcore-theme': 'dark' }, osDark: true });
|
|
assert.strictEqual(r.theme, 'light');
|
|
assert.strictEqual(r.override, 'light');
|
|
});
|
|
test('the override is not written to localStorage', () => {
|
|
const r = run('#/map?theme=dark', { stored: { 'meshcore-theme': 'light' } });
|
|
assert.strictEqual(r.store['meshcore-theme'], 'light');
|
|
});
|
|
test('works on any page, with the param anywhere in the query', () => {
|
|
assert.strictEqual(run('#/packets?region=BE&theme=dark&x=1').theme, 'dark');
|
|
assert.strictEqual(run('#/?theme=dark').theme, 'dark');
|
|
});
|
|
test('an unknown value is ignored', () => {
|
|
const r = run('#/map?theme=blue', { stored: { 'meshcore-theme': 'light' } });
|
|
assert.strictEqual(r.theme, 'light');
|
|
assert.strictEqual(r.override, null);
|
|
});
|
|
test('without the param the saved choice applies', () => {
|
|
const r = run('#/map?embed=1', { stored: { 'meshcore-theme': 'dark' } });
|
|
assert.strictEqual(r.theme, 'dark');
|
|
assert.strictEqual(r.override, null);
|
|
});
|
|
test('without the param or a saved choice the OS preference applies', () => {
|
|
assert.strictEqual(run('#/map', { osDark: true }).theme, 'dark');
|
|
assert.strictEqual(run('', {}).theme, 'light');
|
|
});
|
|
test('the override still applies when localStorage throws (sandboxed iframe)', () => {
|
|
const r = run('#/map?embed=1&theme=dark', { blocked: true });
|
|
assert.strictEqual(r.theme, 'dark');
|
|
});
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed > 0 ? 1 : 0);
|