Files
meshcore-analyzer/tests/e2e/test-customize-theme-e2e.js
T
Alex B 893773338e chore(tests): move root test-*.js into tests/unit and tests/e2e (#2036)
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.

Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.

Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.

Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.

Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
2026-09-17 19:06:07 +02:00

149 lines
6.9 KiB
JavaScript

/**
* E2E (#1297 B4): Customizer V2 — Theme tokens + presets + dark/light mode
*
* Exercises the theme-tab subsystem of public/customize-v2.js:
* - Open customizer panel via #customizeToggle
* - Click a preset button → assert CSS variable on documentElement updates
* AND localStorage 'cs-theme-overrides' is written
* - Change a basic color picker → assert THEME_CSS_MAP[key] CSS var updates
* (verifies "all colors via CSS variables" invariant — NOT inline styles)
* - Reload page → assert override persists and CSS var still applied
*
* Usage: BASE_URL=http://localhost:13581 node test-customize-theme-e2e.js
*/
'use strict';
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:3000';
let passed = 0, failed = 0;
async function step(name, fn) {
try { await fn(); passed++; console.log(' \u2713 ' + name); }
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
}
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
(async () => {
const browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
});
const ctx = await browser.newContext();
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
console.log(`\n=== #1297 B4 customize-theme E2E against ${BASE} ===`);
await step('navigate home and wait for customizer API', async () => {
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => localStorage.removeItem('cs-theme-overrides'));
await page.reload({ waitUntil: 'load' });
await page.waitForFunction(() => window._customizerV2 && window._customizerV2.initDone, null, { timeout: 8000 });
});
await step('customizer toggle button is wired', async () => {
const btn = await page.$('#customizeToggle');
assert(btn, '#customizeToggle button missing in nav');
});
await step('click toggle opens .cust-overlay panel', async () => {
await page.click('#customizeToggle');
await page.waitForSelector('.cust-overlay:not(.hidden)', { timeout: 4000 });
const visible = await page.isVisible('.cust-overlay');
assert(visible, 'customizer overlay should be visible');
});
await step('theme tab renders preset buttons', async () => {
// Switch to theme tab if needed
const tabBtn = await page.$('.cust-tab[data-tab="theme"]');
if (tabBtn) await tabBtn.click();
const presets = await page.$$('.cust-preset-btn[data-preset]');
assert(presets.length >= 2, 'expected multiple presets, got ' + presets.length);
});
await step('clicking preset writes localStorage AND updates CSS vars (not inline styles)', async () => {
// Find a non-active preset
const presetIds = await page.$$eval('.cust-preset-btn[data-preset]', els =>
els.filter(e => !e.classList.contains('active')).map(e => e.getAttribute('data-preset'))
);
assert(presetIds.length > 0, 'need at least one non-active preset');
const presetId = presetIds[0];
await page.click('.cust-preset-btn[data-preset="' + presetId + '"]');
await page.waitForTimeout(400); // debounced write
// Assert localStorage
const raw = await page.evaluate(() => localStorage.getItem('cs-theme-overrides'));
assert(raw, 'cs-theme-overrides not written after preset click');
const parsed = JSON.parse(raw);
assert(parsed.theme || parsed.themeDark, 'preset write should include theme section, got ' + raw);
// Assert CSS var on :root reflects new accent (vs hardcoded inline style)
const accentVar = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
);
assert(accentVar.length > 0, '--accent CSS variable should be set on documentElement');
assert(accentVar.startsWith('#') || accentVar.startsWith('rgb'),
'--accent should be a color value, got: ' + accentVar);
});
await step('color picker change updates CSS variable (THEME_CSS_MAP invariant)', async () => {
// Find the accent color picker — determine which section is active (theme vs themeDark)
const pickerInfo = await page.evaluate(() => {
const el = document.querySelector('input[data-cv2-field="theme.accent"], input[data-cv2-field="themeDark.accent"]');
if (!el) return null;
return { section: el.dataset.cv2Field.split('.')[0] };
});
assert(pickerInfo, 'accent color picker not found');
const themeKey = pickerInfo.section; // 'theme' or 'themeDark'
// Set a known color via the input event
await page.evaluate((sel) => {
const el = document.querySelector(sel);
el.value = '#ff00aa';
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}, 'input[data-cv2-field="' + themeKey + '.accent"]');
await page.waitForTimeout(400);
// CSS variable should be updated
const accentVal = await page.evaluate(() =>
document.documentElement.style.getPropertyValue('--accent').trim()
);
assert(accentVal === '#ff00aa', 'expected inline --accent=#ff00aa on documentElement, got ' + accentVal);
// localStorage should reflect the override in the *active* section
const raw = await page.evaluate(() => localStorage.getItem('cs-theme-overrides'));
const parsed = JSON.parse(raw);
assert(parsed[themeKey] && parsed[themeKey].accent === '#ff00aa',
'localStorage[' + themeKey + '].accent should be #ff00aa, got ' + raw);
// Save the key for the persist-across-reload assertion
page._themeKey = themeKey;
});
await step('override persists across reload', async () => {
await page.reload({ waitUntil: 'load' });
await page.waitForFunction(() => window._customizerV2 && window._customizerV2.initDone, null, { timeout: 8000 });
const accentVal = await page.evaluate(() =>
document.documentElement.style.getPropertyValue('--accent').trim()
);
assert(accentVal === '#ff00aa', 'expected --accent to persist as #ff00aa after reload, got: ' + accentVal);
const raw = await page.evaluate(() => localStorage.getItem('cs-theme-overrides'));
const parsed = JSON.parse(raw);
// After reload, the override may be in theme or themeDark depending on active mode
const themeKey = (parsed.themeDark && parsed.themeDark.accent === '#ff00aa') ? 'themeDark' :
(parsed.theme && parsed.theme.accent === '#ff00aa') ? 'theme' : null;
assert(themeKey, 'persisted override missing — neither theme.accent nor themeDark.accent is #ff00aa, got: ' + raw);
});
await step('cleanup: clear overrides', async () => {
await page.evaluate(() => localStorage.removeItem('cs-theme-overrides'));
});
await browser.close();
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
process.exit(failed > 0 ? 1 : 0);
})();