← Back to Index

Lesson 12

Emulation & CSS

What You'll Learn

The Emulation domain lets you fake the device environment - screen size, pixel ratio, user agent, geolocation, timezone, and more. The CSS domain gives you programmatic access to stylesheets and computed styles. Together, they cover responsive design testing and CSS debugging.

  1. Setting viewport and device dimensions
  2. Emulating mobile devices (touch, user agent, pixel ratio)
  3. Overriding media features (dark mode, reduced motion, print)
  4. Geolocation, timezone, and locale overrides
  5. CPU throttling (simulating slow devices)
  6. Reading computed styles
  7. Inspecting and modifying stylesheets
  8. Tracking CSS coverage (unused rules)

The Emulation Domain

Setting viewport and device metrics

This is the CDP equivalent of the device toolbar in DevTools:

// Emulate an iPhone 14 Pro viewport
await send('Emulation.setDeviceMetricsOverride', {
  width: 393,
  height: 852,
  deviceScaleFactor: 3,    // Retina (3x pixel ratio)
  mobile: true,            // Enable mobile rendering mode
  screenWidth: 393,
  screenHeight: 852
});

// Reset to default (actual device dimensions)
await send('Emulation.clearDeviceMetricsOverride');
For the package: You could run the same journey at multiple viewport sizes to test responsive behaviour - loop through breakpoints (mobile, tablet, desktop) and capture screenshots at each. The journey code doesn't change; only the emulation settings do.

Touch emulation

// Enable touch event emulation (mobile testing)
await send('Emulation.setTouchEmulationEnabled', {
  enabled: true,
  maxTouchPoints: 5
});

User agent override

// Pretend to be a mobile Safari browser
await send('Emulation.setUserAgentOverride', {
  userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
  platform: 'iPhone'
});

Emulating media features (dark mode, reduced motion, print)

// Emulate dark mode - triggers @media (prefers-color-scheme: dark) in CSS
await send('Emulation.setEmulatedMedia', {
  features: [
    { name: 'prefers-color-scheme', value: 'dark' }
  ]
});

// Emulate reduced motion preference
await send('Emulation.setEmulatedMedia', {
  features: [
    { name: 'prefers-reduced-motion', value: 'reduce' }
  ]
});

// Emulate print media (for testing @media print styles)
await send('Emulation.setEmulatedMedia', {
  media: 'print'
});

// Reset to defaults
await send('Emulation.setEmulatedMedia', {
  media: '',
  features: []
});

Vision deficiency emulation

// Simulate how the page looks for users with colour blindness
await send('Emulation.setEmulatedVisionDeficiency', {
  type: 'deuteranopia'    // 'none', 'blurredVision', 'deuteranopia',
                          // 'protanopia', 'tritanopia', 'achromatopsia'
});

Geolocation override

// Fake the user's location (for location-based features)
await send('Emulation.setGeolocationOverride', {
  latitude: 51.5074,     // London
  longitude: -0.1278,
  accuracy: 1
});

// Clear the override
await send('Emulation.clearGeolocationOverride');

Timezone and locale

// Override timezone (affects Date objects and Intl formatting)
await send('Emulation.setTimezoneOverride', {
  timezoneId: 'America/New_York'
});

// Override locale (affects number/date formatting)
await send('Emulation.setLocaleOverride', {
  locale: 'de-DE'
});

CPU throttling

// Simulate a 4x slower CPU (like a low-end mobile device)
await send('Emulation.setCPUThrottlingRate', {
  rate: 4    // 1 = no throttle, 4 = 4x slower, 6 = 6x slower
});

// Reset
await send('Emulation.setCPUThrottlingRate', { rate: 1 });
Combined with network throttling (from Network.emulateNetworkConditions), CPU throttling lets you simulate a realistic low-end mobile experience - slow CPU + slow network. Useful for testing perceived performance under constrained conditions.

Other useful overrides

// Disable JavaScript (test progressive enhancement / no-JS fallbacks)
await send('Emulation.setScriptExecutionDisabled', {
  value: true
});

// Force page to think it's focused (useful in headless mode where
// the page might think it's in a background tab)
await send('Emulation.setFocusEmulationEnabled', {
  enabled: true
});

// Force dark mode rendering at the browser level
await send('Emulation.setAutoDarkModeOverride', {
  enabled: true
});

The CSS Domain

The CSS domain is experimental but gives you deep access to styles - the same data powering the Styles panel in DevTools.

Enable the domain

await send('CSS.enable');

Reading computed styles

Get the final computed style for any node (all resolved values after cascade, inheritance, and specificity):

// First, get the nodeId (from DOM domain)
const doc = await send('DOM.getDocument', { depth: 0 });
const h1 = await send('DOM.querySelector', {
  nodeId: doc.root.nodeId,
  selector: 'h1'
});

// Get computed styles for that node
const computed = await send('CSS.getComputedStyleForNode', {
  nodeId: h1.nodeId
});

// computed.computedStyle = [
//   { name: 'color', value: 'rgb(17, 17, 17)' },
//   { name: 'font-size', value: '28.8px' },
//   { name: 'font-family', value: '"Source Serif 4", Georgia, serif' },
//   { name: 'display', value: 'block' },
//   { name: 'margin-bottom', value: '4px' },
//   ... hundreds more properties
// ]

This is useful for assertions: "is this element actually red?" "is it display: none?"

// Helper to get a specific computed property
function getComputedValue(computedStyle, property) {
  const entry = computedStyle.find(style => style.name === property);
  return entry ? entry.value : null;
}

const display = getComputedValue(computed.computedStyle, 'display');
const visibility = getComputedValue(computed.computedStyle, 'visibility');
const color = getComputedValue(computed.computedStyle, 'color');

console.log(`h1: display=${display}, visibility=${visibility}, color=${color}`);

Getting matched styles (which rules apply to an element)

// Get all CSS rules that match this element (like the Styles panel)
const matched = await send('CSS.getMatchedStylesForNode', {
  nodeId: h1.nodeId
});

// matched.matchedCSSRules = array of rules with selectors, properties, and source locations
// matched.inherited = styles inherited from parent elements
// matched.inlineStyle = inline style attribute

Modifying styles

// Get inline styles for the element
const inline = await send('CSS.getInlineStylesForNode', {
  nodeId: h1.nodeId
});

// Set style properties
await send('CSS.setStyleTexts', {
  edits: [{
    styleSheetId: inline.inlineStyle.styleSheetId,
    range: inline.inlineStyle.range,
    text: 'color: red; font-size: 48px;'
  }]
});

CSS coverage (finding unused rules)

Track which CSS rules are actually used - valuable for identifying dead CSS:

// Start tracking CSS rule usage
await send('CSS.startRuleUsageTracking');

// ... navigate and interact with the page ...
await send('Page.navigate', { url: 'http://localhost:4200' });
await waitForSelector('[data-testid="app-ready"]');

// Stop and get the coverage report
const coverage = await send('CSS.stopRuleUsageTracking');
// coverage.ruleUsage = [
//   { styleSheetId: '1', startOffset: 0, endOffset: 150, used: true },
//   { styleSheetId: '1', startOffset: 151, endOffset: 300, used: false },  ← unused rule
//   ...
// ]

const totalRules = coverage.ruleUsage.length;
const usedRules = coverage.ruleUsage.filter(rule => rule.used).length;
const unusedRules = totalRules - usedRules;

console.log(`CSS Coverage: ${usedRules}/${totalRules} rules used (${unusedRules} unused)`);

Forcing pseudo-states

Force an element into :hover, :focus, :active, or :visited state - without actually hovering or focusing:

// Force the button to be in :hover state (to inspect hover styles)
await send('CSS.forcePseudoState', {
  nodeId: buttonNodeId,
  forcedPseudoClasses: ['hover']
});

// Force multiple states simultaneously
await send('CSS.forcePseudoState', {
  nodeId: inputNodeId,
  forcedPseudoClasses: ['focus', 'valid']
});

Practical Patterns

Responsive testing loop

const viewports = [
  { name: 'Mobile', width: 375, height: 812, deviceScaleFactor: 3, mobile: true },
  { name: 'Tablet', width: 768, height: 1024, deviceScaleFactor: 2, mobile: true },
  { name: 'Desktop', width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false },
];

for (const viewport of viewports) {
  await send('Emulation.setDeviceMetricsOverride', viewport);
  await send('Page.navigate', { url: 'http://localhost:4200' });
  await waitForSelector('[data-testid="app-ready"]');

  const screenshot = await send('Page.captureScreenshot', { format: 'png' });
  writeFileSync(`screenshot-${viewport.name}.png`, Buffer.from(screenshot.data, 'base64'));

  console.log(`${viewport.name}: screenshot captured`);
}

Checking visibility for assertions

async function isVisible(selector) {
  const doc = await send('DOM.getDocument', { depth: 0 });
  const node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });

  if (node.nodeId === 0) return false;  // Element doesn't exist

  const computed = await send('CSS.getComputedStyleForNode', { nodeId: node.nodeId });

  const display = getComputedValue(computed.computedStyle, 'display');
  const visibility = getComputedValue(computed.computedStyle, 'visibility');
  const opacity = getComputedValue(computed.computedStyle, 'opacity');

  return display !== 'none' && visibility !== 'hidden' && opacity !== '0';
}

Key Commands Reference

Command Purpose
Emulation.setDeviceMetricsOverride Set viewport size, pixel ratio, mobile mode
Emulation.setEmulatedMedia Emulate media type/features (dark mode, print, reduced motion)
Emulation.setUserAgentOverride Fake the browser's user agent string
Emulation.setCPUThrottlingRate Simulate slow CPU (rate: 4 = 4x slower)
Emulation.setGeolocationOverride Fake geolocation coordinates
Emulation.setTimezoneOverride Override system timezone
CSS.getComputedStyleForNode Get resolved CSS values for an element
CSS.getMatchedStylesForNode Get all CSS rules matching an element
CSS.startRuleUsageTracking Begin tracking CSS coverage
CSS.forcePseudoState Force :hover, :focus, :active states

For the full list, see the Emulation and CSS sections of the complete reference.

Primary Source

Read the Emulation domain documentation and the CSS domain documentation. The Emulation domain is stable; CSS is experimental but widely used.