/** * 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); })();