{ "_config": { "baseUrl": "http://localhost:3000", "screenshotDir": "docs/plans/2026-02-09-dark-mode/screenshots", "startup": { "command": "npm run dev", "readyWhen": "http://localhost:3000 responds", "note": "App must be running before any test. chrome-browser agent auto-logs in as test user." }, "selectors": { "sidebar": "aside[data-automation-id*='-sidebar']", "header": "header", "submenuTooltip": "[class*='submenu'], [class*='Submenu']", "mainContent": "main.flex-1", "bodyContent": "main.flex-1 > div.flex-1", "card": "[class*='card'], [class*='Card'], .bg-white.rounded", "input": "input[type='text'], input[type='email'], input[type='password'], textarea", "select": "select, [role='combobox'], [role='listbox']", "checkbox": "input[type='checkbox'], [role='checkbox']", "switch": "[role='switch'], .switch", "buttonPrimary": "button.bg-primary, button[class*='primary'], button.bg-\\[rgb\\(var\\(--color-primary", "buttonGhost": "button[class*='ghost'], button[class*='outline'], button.border", "table": "table, .rt-TableRoot, [role='grid']", "tableRow": "tr, .rt-TableRow, [role='row']", "dialogOverlay": "[data-radix-dialog-overlay]", "dialogContent": "[data-radix-dialog-content], [data-automation-id$='-dialog']", "dropdown": "[data-radix-popper-content-wrapper], [role='menu']", "tooltip": "[role='tooltip']", "badge": "[class*='badge'], [class*='Badge']", "toast": "[class*='toast'], [class*='Toaster'], [role='status']", "tabs": "[role='tablist']", "tabActive": "[role='tab'][aria-selected='true'], [role='tab'][data-state='active']", "themeToggle": "[data-automation-id='theme-toggle'], #theme-toggle, [aria-label*='theme'], [aria-label*='Theme']", "clientPortalNav": "nav", "clientPortalMain": "main.flex-1", "tiptapEditor": ".tiptap, .ProseMirror, [class*='editor']", "dayPicker": ".rdp, [class*='day-picker'], [class*='DayPicker']", "bigCalendar": ".rbc-calendar, [class*='big-calendar']", "sidebarToggle": "#sidebar-toggle-button", "userMenu": "#user-menu-trigger", "quickCreate": "#global-quick-create-trigger" }, "themes": { "forceDark": "() => { document.documentElement.classList.remove('light'); document.documentElement.classList.add('dark'); document.body.classList.remove('light'); document.body.classList.add('dark'); document.documentElement.setAttribute('data-theme', 'dark'); return 'dark mode forced'; }", "forceLight": "() => { document.documentElement.classList.remove('dark'); document.documentElement.classList.add('light'); document.body.classList.remove('dark'); document.body.classList.add('light'); document.documentElement.setAttribute('data-theme', 'light'); return 'light mode forced'; }", "note": "Use forceDark/forceLight BEFORE the ThemeToggle (F008) is implemented. After F008, click the toggle instead." }, "helpers": { "isDark": "(selector) => { const el = document.querySelector(selector); if (!el) return { pass: false, reason: 'element not found: ' + selector }; const bg = getComputedStyle(el).backgroundColor; const m = bg.match(/\\d+/g); if (!m) return { pass: false, reason: 'no rgb in: ' + bg }; const [r,g,b] = m.map(Number); const lum = (0.299*r + 0.587*g + 0.114*b) / 255; return { pass: lum < 0.3, luminance: lum, value: bg }; }", "isLight": "(selector, prop) => { prop = prop || 'color'; const el = document.querySelector(selector); if (!el) return { pass: false, reason: 'element not found: ' + selector }; const val = getComputedStyle(el)[prop]; const m = val.match(/\\d+/g); if (!m) return { pass: false, reason: 'no rgb in: ' + val }; const [r,g,b] = m.map(Number); const lum = (0.299*r + 0.587*g + 0.114*b) / 255; return { pass: lum > 0.6, luminance: lum, value: val }; }", "isNotWhite": "(selector) => { const el = document.querySelector(selector); if (!el) return { pass: false, reason: 'element not found: ' + selector }; const bg = getComputedStyle(el).backgroundColor; return { pass: bg !== 'rgb(255, 255, 255)' && bg !== 'rgba(0, 0, 0, 0)', value: bg }; }", "isVisible": "(selector) => { const el = document.querySelector(selector); if (!el) return { pass: false, reason: 'element not found: ' + selector }; const r = el.getBoundingClientRect(); return { pass: r.width > 0 && r.height > 0, width: r.width, height: r.height }; }", "contrastRatio": "(fgSelector, bgSelector) => { function luminance(r,g,b) { const a = [r,g,b].map(v => { v /= 255; return v <= 0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4); }); return 0.2126*a[0] + 0.7152*a[1] + 0.0722*a[2]; } const fg = getComputedStyle(document.querySelector(fgSelector)).color; const bg = getComputedStyle(document.querySelector(bgSelector)).backgroundColor; const [fr,fg2,fb] = fg.match(/\\d+/g).map(Number); const [br,bg2,bb] = bg.match(/\\d+/g).map(Number); const l1 = luminance(fr,fg2,fb); const l2 = luminance(br,bg2,bb); const ratio = (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05); return { pass: ratio >= 4.5, ratio: Math.round(ratio*100)/100 }; }", "getCssVar": "(varName) => { return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); }", "hasClass": "(selector, className) => { const el = document.querySelector(selector); if (!el) return { pass: false, reason: 'element not found' }; return { pass: el.classList.contains(className), classes: Array.from(el.classList) }; }" } }, "tests": [ { "id": "T001", "description": "next-themes package is installed and importable in server workspace", "implemented": false, "featureIds": ["F001"], "type": "code-check", "steps": [ { "tool": "bash", "command": "cd server && node -e \"require.resolve('next-themes')\"", "expectSuccess": true } ] }, { "id": "T001b", "description": "Tailwind config includes darkMode: 'class'", "implemented": false, "featureIds": ["F001b"], "type": "code-check", "steps": [ { "tool": "grep", "file": "server/tailwind.config.ts", "pattern": "darkMode.*class", "expectMatch": true } ] }, { "id": "T002", "description": "Tailwind background color uses CSS variable, not hardcoded white", "implemented": false, "featureIds": ["F002"], "type": "code-check", "steps": [ { "tool": "grep", "file": "server/tailwind.config.ts", "pattern": "background:\\s*['\"]white['\"]", "expectMatch": false, "failMessage": "background should not be hardcoded 'white'" }, { "tool": "grep", "file": "server/tailwind.config.ts", "pattern": "background.*var\\(--color", "expectMatch": true } ] }, { "id": "T003", "description": "Tailwind card color uses CSS variable, not hardcoded white", "implemented": false, "featureIds": ["F002"], "type": "code-check", "steps": [ { "tool": "grep", "file": "server/tailwind.config.ts", "pattern": "card:\\s*['\"]white['\"]", "expectMatch": false }, { "tool": "grep", "file": "server/tailwind.config.ts", "pattern": "card.*var\\(--color", "expectMatch": true } ] }, { "id": "T004", "description": "Status colors (success, warning, error) have different values in .light vs .dark blocks", "implemented": false, "featureIds": ["F003"], "type": "code-check", "steps": [ { "tool": "grep", "file": "server/src/app/globals.css", "pattern": "--color-status-success", "expectMinMatches": 2, "note": "Should appear in both .light and .dark blocks" }, { "tool": "grep", "file": "server/src/app/globals.css", "pattern": "--color-status-warning", "expectMinMatches": 2 }, { "tool": "grep", "file": "server/src/app/globals.css", "pattern": "--color-status-error", "expectMinMatches": 2 } ] }, { "id": "T005", "description": "next-themes setTheme('dark') applies dark class to ", "implemented": false, "featureIds": ["F004"], "type": "runtime", "page": "/msp", "steps": [ { "tool": "navigate_page", "url": "/msp" }, { "tool": "wait_for", "text": "Dashboard" }, { "tool": "evaluate_script", "function": "() => { window.__NEXT_THEME_SET && window.__NEXT_THEME_SET('dark'); return document.documentElement.classList.contains('dark'); }", "expect": true, "note": "If next-themes is wired, test via its API. Alternatively check: document.documentElement.className includes 'dark'" } ] }, { "id": "T006", "description": "next-themes setTheme('light') applies light class to ", "implemented": false, "featureIds": ["F004"], "type": "runtime", "page": "/msp", "steps": [ { "tool": "navigate_page", "url": "/msp" }, { "tool": "wait_for", "text": "Dashboard" }, { "tool": "evaluate_script", "function": "() => { window.__NEXT_THEME_SET && window.__NEXT_THEME_SET('light'); return document.documentElement.classList.contains('light'); }", "expect": true } ] }, { "id": "T007", "description": "next-themes setTheme('system') reads prefers-color-scheme and applies matching class", "implemented": false, "featureIds": ["F004"], "type": "runtime", "page": "/msp", "steps": [ { "tool": "navigate_page", "url": "/msp" }, { "tool": "emulate", "colorScheme": "dark" }, { "tool": "evaluate_script", "function": "() => { window.__NEXT_THEME_SET && window.__NEXT_THEME_SET('system'); return new Promise(r => setTimeout(() => r(document.documentElement.classList.contains('dark')), 200)); }", "expect": true }, { "tool": "emulate", "colorScheme": "auto" } ] }, { "id": "T007b", "description": "useAppTheme() syncs theme change to user_preferences DB table", "implemented": false, "featureIds": ["F004b"], "type": "runtime", "page": "/msp", "steps": [ { "tool": "navigate_page", "url": "/msp" }, { "tool": "wait_for", "text": "Dashboard" }, { "tool": "evaluate_script", "function": "() => { window.__NEXT_THEME_SET && window.__NEXT_THEME_SET('dark'); return 'theme set to dark'; }" }, { "tool": "evaluate_script", "function": "async () => { await new Promise(r => setTimeout(r, 1500)); return 'waited for DB sync'; }", "note": "Wait for async DB write" }, { "tool": "bash", "command": "Query user_preferences table for setting_name='theme' to verify the row exists with value 'dark'", "note": "Use mcp__my-private-server__query or psql" } ] }, { "id": "T008", "description": "Root layout
does not have hardcoded className='light'", "implemented": false, "featureIds": ["F005"], "type": "code-check", "steps": [ { "tool": "grep", "file": "server/src/app/layout.tsx", "pattern": "className.*['\"`]light['\"`]", "expectMatch": false, "failMessage": "body should not have hardcoded 'light' class" } ] }, { "id": "T009", "description": "next-themes blocking script prevents flash of wrong theme on reload", "implemented": false, "featureIds": ["F005", "F010"], "type": "runtime", "page": "/msp", "steps": [ { "tool": "navigate_page", "url": "/msp" }, { "tool": "wait_for", "text": "Dashboard" }, { "tool": "evaluate_script", "function": "() => { localStorage.setItem('theme', 'dark'); return 'localStorage set'; }" }, { "tool": "navigate_page", "type": "reload" }, { "tool": "evaluate_script", "function": "() => { const cl = document.documentElement.className; return { hasDark: cl.includes('dark'), className: cl }; }", "expect": { "hasDark": true }, "note": "After reload with localStorage='dark', the page should load with dark class immediately (no flash)" } ] }, { "id": "T010", "description": "Radix UI