/** * Dark mode capture — screenshot pages in both light and dark color schemes. * * Uses Playwright's page.emulateMedia({ colorScheme: 'dark' }) to force dark mode, * then captures a screenshot. Compares with light mode to determine if dark mode * is supported (significant visual difference). * * Also detects prefers-color-scheme media query support in CSS. */ import type { Page } from 'playwright'; export interface DarkModeCapture { url: string; lightScreenshot?: Buffer; darkScreenshot?: Buffer; hasDarkMode: boolean; hasCssDarkMode: boolean; // @media (prefers-color-scheme: dark) detected in stylesheets backgroundColorLight?: string; backgroundColorDark?: string; capturedAt: string; } async function getBackgroundColor(page: Page): Promise { try { return await page.evaluate(() => { const body = document.body; return window.getComputedStyle(body).backgroundColor; }); } catch { return 'unknown'; } } function colorsAreDifferent(a: string, b: string): boolean { if (a === b) return false; if (a === 'unknown' || b === 'unknown') return false; // Parse rgb(r, g, b) and compare luminance const parse = (c: string) => { const m = c.match(/\d+/g); return m ? m.map(Number) : [0, 0, 0]; }; const [ar, ag, ab] = parse(a); const [br, bg, bb] = parse(b); const lumA = 0.299 * ar + 0.587 * ag + 0.114 * ab; const lumB = 0.299 * br + 0.587 * bg + 0.114 * bb; return Math.abs(lumA - lumB) > 30; // significant luminance difference } async function hasCssDarkModeSupport(page: Page): Promise { try { return await page.evaluate(() => { const sheets = Array.from(document.styleSheets); for (const sheet of sheets) { try { const rules = Array.from(sheet.cssRules ?? []); for (const rule of rules) { if (rule instanceof CSSMediaRule && rule.conditionText?.includes('prefers-color-scheme')) { return true; } } } catch { /* cross-origin stylesheet — skip */ } } return false; }); } catch { return false; } } export async function captureDarkMode( page: Page, url: string, opts: { takeScreenshots?: boolean } = {}, ): Promise { const capturedAt = new Date().toISOString(); const { takeScreenshots = false } = opts; // Light mode await page.emulateMedia({ colorScheme: 'light' }).catch(() => {}); const backgroundColorLight = await getBackgroundColor(page); const hasCssDark = await hasCssDarkModeSupport(page); const lightScreenshot = takeScreenshots ? await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined) : undefined; // Dark mode await page.emulateMedia({ colorScheme: 'dark' }).catch(() => {}); await page.waitForTimeout(300); // allow CSS transitions const backgroundColorDark = await getBackgroundColor(page); const darkScreenshot = takeScreenshots ? await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined) : undefined; // Restore light mode await page.emulateMedia({ colorScheme: 'light' }).catch(() => {}); const hasDarkMode = colorsAreDifferent(backgroundColorLight, backgroundColorDark) || hasCssDark; return { url, lightScreenshot, darkScreenshot, hasDarkMode, hasCssDarkMode: hasCssDark, backgroundColorLight, backgroundColorDark, capturedAt, }; }