Loading...

Handling Light and Dark Themes in Playwright Screenshots

Need to capture theme-aware screenshots? Learn how to master light and dark theme screenshots using Playwright for web testing and documentation.

Back

Modern web applications commonly support both light and dark themes to enhance user experience. When automating screenshot captures for testing or documentation, handling both themes effectively becomes crucial. Let's explore how to master theme-aware screenshots using Playwright.

The Challenge with Theme Screenshots

Capturing theme-specific screenshots presents several challenges:

  1. Theme Detection: Websites implement themes differently - some use CSS classes, others use data attributes or media queries
  2. Theme Switching: Ensuring the theme has fully applied before capturing
  3. Visual Consistency: Maintaining consistent results across different environments
  4. Dynamic Content: Handling theme-dependent assets and transitions

Basic Theme Screenshot Implementation

Let's start with a basic implementation that handles both themes:

import { chromium, Browser, Page } from 'playwright';
 
interface ThemeScreenshotOptions {
  url: string;
  selector?: string;
  fullPage?: boolean;
  waitForTimeout?: number;
}
 
async function captureThemeScreenshots(options: ThemeScreenshotOptions) {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
 
  try {
    // Navigate to the page
    await page.goto(options.url, {
      waitUntil: 'networkidle'
    });
 
    // Capture light theme first
    const lightScreenshot = await captureScreenshot(page, {
      ...options,
      theme: 'light'
    });
 
    // Switch to dark theme and capture
    const darkScreenshot = await captureScreenshot(page, {
      ...options,
      theme: 'dark'
    });
 
    return {
      light: lightScreenshot,
      dark: darkScreenshot
    };
  } finally {
    await browser.close();
  }
}

Enforcing Theme Styles

To ensure consistent theme rendering, we'll inject custom styles:

const themeScripts = {
  dark: `
    (function() {
      document.documentElement.classList.add('dark');
      document.body.classList.add('dark');
      
      const style = document.createElement('style');
      style.textContent = \`
        html.dark, body.dark {
          background-color: #1a1a1a !important;
          color: #ffffff !important;
        }
        html.dark *, body.dark * {
          color-scheme: dark !important;
        }
        html.dark img, body.dark img {
          filter: brightness(.8) contrast(1.2);
        }
      \`;
      document.head.appendChild(style);
    })();
  `,
  light: `
    (function() {
      document.documentElement.classList.remove('dark');
      document.body.classList.remove('dark');
      
      const style = document.createElement('style');
      style.textContent = \`
        html, body {
          background-color: #ffffff !important;
          color: #000000 !important;
        }
        * {
          color-scheme: light !important;
        }
      \`;
      document.head.appendChild(style);
    })();
  `
};

Advanced Theme Handling

Here's a more robust implementation that handles various theme scenarios:

async function captureScreenshot(page: Page, options: {
  theme: 'light' | 'dark',
  selector?: string,
  fullPage?: boolean,
  waitForTimeout?: number
}) {
  // Set color scheme at browser level
  await page.emulateMedia({ colorScheme: options.theme });
  
  // Inject theme-specific scripts
  await page.addInitScript(themeScripts[options.theme]);
  
  // Handle common theme storage mechanisms
  await page.evaluate((theme) => {
    // Local storage
    localStorage.setItem('theme', theme);
    
    // Data attributes
    document.documentElement.setAttribute('data-theme', theme);
    
    // Media query override
    window.matchMedia = (query) => ({
      matches: query.includes('dark') ? theme === 'dark' : theme === 'light',
      addEventListener: () => {},
      removeEventListener: () => {}
    });
  }, options.theme);
 
  // Wait for theme to apply
  await page.waitForTimeout(options.waitForTimeout || 1000);
 
  // Wait for any theme transitions
  await page.waitForFunction(() => {
    const elements = document.querySelectorAll('[class*="transition"]');
    return Array.from(elements).every(
      el => getComputedStyle(el).transitionDuration === '0s'
    );
  });
 
  // Take the screenshot
  if (options.selector) {
    const element = await page.$(options.selector);
    return element?.screenshot({
      type: 'png',
      fullPage: options.fullPage
    });
  }
 
  return page.screenshot({
    type: 'png',
    fullPage: options.fullPage
  });
}

Handling Common Theme Patterns

Different websites implement themes in various ways. Here's how to handle common patterns:

async function handleThemePatterns(page: Page, theme: 'light' | 'dark') {
  // Handle CSS class-based themes
  await page.evaluate((theme) => {
    const classList = document.documentElement.classList;
    classList.remove('light', 'dark');
    classList.add(theme);
  }, theme);
 
  // Handle data attribute-based themes
  await page.evaluate((theme) => {
    document.documentElement.setAttribute('data-theme', theme);
    document.documentElement.setAttribute('data-color-scheme', theme);
  }, theme);
 
  // Handle CSS variables
  await page.evaluate((theme) => {
    const root = document.documentElement;
    if (theme === 'dark') {
      root.style.setProperty('--background', '#1a1a1a');
      root.style.setProperty('--text', '#ffffff');
    } else {
      root.style.setProperty('--background', '#ffffff');
      root.style.setProperty('--text', '#000000');
    }
  }, theme);
}

Best Practices

  1. Wait for Theme Application

    async function waitForThemeApplication(page: Page) {
      // Wait for any CSS transitions to complete
      await page.waitForFunction(() => {
        return !document.querySelector('[class*="transition"][style*="transition"]');
      });
      
      // Additional delay for safety
      await page.waitForTimeout(500);
    }
  2. Handle Dynamic Content

    async function waitForThemeContent(page: Page) {
      // Wait for theme-specific images
      await page.waitForFunction(() => {
        const images = document.querySelectorAll('img');
        return Array.from(images).every(img => img.complete);
      });
    }
  3. Verify Theme Application

    async function verifyTheme(page: Page, theme: 'light' | 'dark') {
      const backgroundColor = await page.evaluate(() => {
        return getComputedStyle(document.body).backgroundColor;
      });
      
      const isDark = theme === 'dark';
      const isCorrectTheme = isDark ? 
        backgroundColor.includes('1a1a1a') : 
        backgroundColor.includes('255, 255, 255');
        
      if (!isCorrectTheme) {
        throw new Error(`Theme verification failed: Expected ${theme} theme`);
      }
    }

Usage Example

Here's how to use these implementations:

async function example() {
  const screenshots = await captureThemeScreenshots({
    url: 'https://example.com',
    fullPage: true,
    waitForTimeout: 2000
  });
 
  console.log('Screenshots captured:', {
    light: screenshots.light ? 'Success' : 'Failed',
    dark: screenshots.dark ? 'Success' : 'Failed'
  });
}

Practical Use Cases

  1. Visual Regression Testing

    • Automatically detect unwanted theme-related changes during development
    • Compare theme implementations across different branches
    • Ensure consistent styling across theme switches
    async function visualRegressionTest() {
      const baselineScreenshots = await loadBaselineScreenshots();
      const currentScreenshots = await captureThemeScreenshots({
        url: 'https://your-app.com',
        fullPage: true
      });
      
      const differences = await compareScreenshots(baselineScreenshots, currentScreenshots);
      if (differences.length > 0) {
        console.error('Visual differences detected:', differences);
      }
    }
  2. Documentation Generation

    • Automatically generate theme-aware documentation
    • Create visual guides showing both theme variants
    • Document component states across themes
    async function generateComponentDocs() {
      const components = ['button', 'card', 'modal'];
      const states = ['default', 'hover', 'active', 'disabled'];
      
      for (const component of components) {
        for (const state of states) {
          await captureThemeScreenshots({
            url: `https://your-docs.com/components/${component}`,
            selector: `#${component}-${state}`,
            waitForTimeout: 1000
          });
        }
      }
    }
  3. Cross-browser Theme Testing

    • Verify theme consistency across different browsers
    • Test theme implementations in various viewport sizes
    async function crossBrowserThemeTest() {
      const browsers = ['chromium', 'firefox', 'webkit'];
      const viewports = [
        { width: 375, height: 667 },  // Mobile
        { width: 1024, height: 768 }, // Tablet
        { width: 1920, height: 1080 } // Desktop
      ];
      
      for (const browser of browsers) {
        for (const viewport of viewports) {
          await captureThemeScreenshots({
            url: 'https://your-app.com',
            browserType: browser,
            viewport
          });
        }
      }
    }
  4. Continuous Integration

    • Integrate theme testing into your CI/CD pipeline
    • Block deployments on theme-related visual regressions
    async function ciThemeCheck() {
      try {
        await captureAndCompareThemes();
        console.log('Theme verification passed');
        process.exit(0);
      } catch (error) {
        console.error('Theme verification failed:', error);
        process.exit(1);
      }
    }

External Resources and References

Official Documentation

  • Playwright Official Documentation: Comprehensive guides and tutorials to get started with Playwright. View Documentation
  • Playwright API Reference: Detailed API documentation for Playwright's classes and methods. View API Reference
  • Playwright Test Documentation: Information on using Playwright's testing framework for end-to-end testing. View Test Documentation

Theme Implementation Guides

  • MDN: Dark Mode: Learn about the color-scheme CSS property for implementing dark mode. Read Article
  • Web.dev: Dark Mode: An article discussing the prefers-color-scheme media query for dark mode support. Read Article
  • CSS Tricks: Dark Mode: Insights into implementing dark mode using CSS. Read Article

Visual Testing Tools

  • Percy: Integrate visual testing with Playwright using Percy's platform. Learn More
  • Applitools Eyes: Automated visual testing for Playwright applications. Learn More
  • Chromatic: Visual testing and review for UI components, integrating with Playwright. Learn More
  • Playwright Visual Regression Example: An example project demonstrating visual regression testing with Playwright. View Repository
  • Playwright Test Examples: A collection of test examples using Playwright. View Examples
  • Awesome Playwright: A curated list of awesome tools, utils, and projects using Playwright. View List

Community Resources

  • Playwright Discord Community: Join the Playwright community on Discord for discussions and support. Join Discord
  • Playwright GitHub Discussions: Engage in discussions and find answers to questions about Playwright. View Discussions
  • Stack Overflow: Playwright Tags: Browse questions tagged with 'playwright' on Stack Overflow for community support. View Questions

Further Reading

  • Advanced Playwright Testing Patterns: Explore advanced testing patterns with Playwright. Read More
  • Visual Regression Testing Best Practices: Learn about techniques and importance of visual regression testing. Read More
  • Implementing Dark Mode in Web Applications: A guide on implementing dark mode in web applications. Read More
  • Automated Visual Testing at Scale: Insights into scaling automated visual testing for mobile and web applications. Read More

Remember that theme testing is just one part of a comprehensive testing strategy. Combine these techniques with other testing approaches like unit testing, integration testing, and end-to-end testing for the most robust results.

Remember to:

  • Always wait for theme transitions to complete
  • Handle multiple theme implementation patterns
  • Verify theme application before capturing
  • Consider dynamic content loading
  • Implement proper error handling and validation

This approach provides a robust foundation for capturing theme-specific screenshots in your testing or documentation workflows.

Note: If you're looking for a managed solution, screenshotsapi.dev offers professional screenshot services with features like theme handling, responsive captures, and automated testing - all through a simple API without managing your own infrastructure.

Want to learn more about Playwright? Check out our other guides:

Written by

Durgaprasad Budhwani

At

Tue Jan 02 2024