mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-11 12: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>
125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/* #2236 E2E: ?theme=dark|light in the hash query sets the theme for one page
|
|
* load without touching the saved choice.
|
|
*
|
|
* Asserts:
|
|
* 1. #/map?embed=1&theme=dark with a saved "light" loads dark, in embed mode,
|
|
* and the saved choice is still "light" once app.js has started.
|
|
* 2. A reload without the param loads the saved "light" again.
|
|
* 3. theme=light with a saved "dark" loads light.
|
|
* 4. A theme change from another tab (storage event) does not end the override.
|
|
* 5. The dark-mode toggle is an explicit choice: it ends the override and
|
|
* saves (toggled twice, so the saved value differs from the start).
|
|
* 6. An unknown value (theme=blue) leaves the saved choice in place.
|
|
*/
|
|
'use strict';
|
|
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
|
|
|
function fail(msg) {
|
|
console.error(`test-theme-url-param-2236-e2e.js: FAIL — ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
async function main() {
|
|
let browser;
|
|
try {
|
|
browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: process.env.CHROMIUM_PATH || undefined,
|
|
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
});
|
|
} catch (err) {
|
|
if (process.env.CHROMIUM_REQUIRE === '1') fail(`Chromium required but unavailable: ${err.message}`);
|
|
console.log(`test-theme-url-param-2236-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
|
|
process.exit(0);
|
|
}
|
|
|
|
let passed = 0;
|
|
const total = 6;
|
|
try {
|
|
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(10000);
|
|
|
|
const state = () => page.evaluate(() => ({
|
|
theme: document.documentElement.getAttribute('data-theme'),
|
|
saved: localStorage.getItem('meshcore-theme'),
|
|
embed: document.body.classList.contains('embed'),
|
|
checked: !!(document.getElementById('darkModeCheckbox') || {}).checked,
|
|
}));
|
|
// Load a URL with a given saved choice; wait until app.js has run its
|
|
// theme setup (the toggle is in sync with data-theme).
|
|
async function open(hash, saved) {
|
|
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate((v) => localStorage.setItem('meshcore-theme', v), saved);
|
|
await page.goto(BASE + '/' + hash, { waitUntil: 'domcontentloaded' });
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => {
|
|
const cb = document.getElementById('darkModeCheckbox');
|
|
return cb && cb.checked === (document.documentElement.getAttribute('data-theme') === 'dark') &&
|
|
document.querySelector('#app') && document.querySelector('#app').children.length > 0;
|
|
});
|
|
}
|
|
|
|
await open('#/map?embed=1&theme=dark', 'light');
|
|
let s = await state();
|
|
if (s.theme !== 'dark' || !s.embed || s.saved !== 'light' || !s.checked) {
|
|
fail(`1: expected dark + embed + saved light, got ${JSON.stringify(s)}`);
|
|
}
|
|
passed++;
|
|
|
|
await page.goto(BASE + '/#/map', { waitUntil: 'domcontentloaded' });
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => document.getElementById('darkModeCheckbox'));
|
|
s = await state();
|
|
if (s.theme !== 'light' || s.saved !== 'light') fail(`2: expected saved light after reload, got ${JSON.stringify(s)}`);
|
|
passed++;
|
|
|
|
await open('#/channels?theme=light', 'dark');
|
|
s = await state();
|
|
if (s.theme !== 'light' || s.saved !== 'dark') fail(`3: expected light with saved dark, got ${JSON.stringify(s)}`);
|
|
passed++;
|
|
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new StorageEvent('storage', { key: 'meshcore-theme', newValue: 'dark' }));
|
|
});
|
|
s = await state();
|
|
if (s.theme !== 'light') fail(`4: a storage event ended the override, got ${JSON.stringify(s)}`);
|
|
passed++;
|
|
|
|
const toggle = () => page.evaluate(() => {
|
|
const cb = document.getElementById('darkModeCheckbox');
|
|
cb.checked = !cb.checked;
|
|
cb.dispatchEvent(new Event('change'));
|
|
});
|
|
await toggle();
|
|
await toggle();
|
|
s = await state();
|
|
if (s.theme !== 'light' || s.saved !== 'light') fail(`5: toggling twice should leave light saved, got ${JSON.stringify(s)}`);
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new StorageEvent('storage', { key: 'meshcore-theme', newValue: 'dark' }));
|
|
});
|
|
s = await state();
|
|
if (s.theme !== 'dark') fail(`5: after the toggle, other tabs apply again, got ${JSON.stringify(s)}`);
|
|
passed++;
|
|
|
|
await open('#/map?theme=blue', 'light');
|
|
s = await state();
|
|
if (s.theme !== 'light' || s.saved !== 'light') fail(`6: unknown value should keep saved light, got ${JSON.stringify(s)}`);
|
|
passed++;
|
|
|
|
await context.close();
|
|
} catch (err) {
|
|
fail(err.stack || err.message);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
console.log(`test-theme-url-param-2236-e2e.js: ${passed}/${total} passed`);
|
|
process.exit(passed === total ? 0 : 1);
|
|
}
|
|
|
|
main();
|