PRACTICAL GUIDE / Playwright connectOverCDP noDefaults option

Attach to Chrome without rewriting its default context

Use noDefaults to preserve an attached Chrome profile, verify download and media behavior, and decide when a fresh Playwright context is still safer.

By The Testing AcademyUpdated August 7, 202620 min read
All field guides
In this guide6 sections
  1. See why a successful attachment can still change behavior
  2. Preserve an existing browser without guessing its state
  3. Test downloads and media without hiding the trade-off
  4. Tell an override problem from a shared-state problem
  5. Detect a replaced browser before blaming attach defaults
  6. Roll out preservation as an explicit mode
  7. Know when a fresh context is the better tool

What you will learn

  • See why a successful attachment can still change behavior
  • Preserve an existing browser without guessing its state
  • Test downloads and media without hiding the trade-off
  • Tell an override problem from a shared-state problem

You attach Playwright to a Chrome window that is already in use, and the page suddenly reports a different color preference. The tab still has the right account and URL, but focus-sensitive behavior changes and downloads stop following the user's normal policy. The connection worked. The attachment changed the environment you meant to observe.

Playwright 1.60 introduced noDefaults for this exact boundary. It lets a CDP client leave several defaults of the existing browser context alone. That makes the option valuable for inspection tools, assisted workflows, and tests that intentionally operate inside an already configured browser.

See why a successful attachment can still change behavior

chromium.connectOverCDP() connects to an existing Chromium-based browser. The attached browser's default context is available as the first item returned by browser.contexts(). Unlike a clean context created for a normal Playwright Test, that default context may already contain pages, authenticated state, extensions, permissions, and user preferences.

Historically, Playwright applied several of its own defaults when it attached to that context. Those defaults make automation more predictable, but they can interfere with a browser whose existing behavior is the subject of the work. Version 1.60 added the noDefaults boolean so the caller can opt out for the existing default context.

The documented effect is deliberately narrow. When noDefaults is true, Playwright does not apply these overrides to the attached default context:

  • The browser's current download acceptance behavior is left in place.
  • Focus emulation is not enabled.
  • Media emulation defaults are not applied, including color scheme, reduced motion, forced colors, and contrast.

The option defaults to false. It does not mean “do not touch anything.” Playwright still connects, instruments pages, evaluates JavaScript, creates protocol sessions, and performs actions you request. It does not freeze the profile or make observations read-only.

It also does not apply to new contexts created through browser.newContext(). That separation is useful. You can preserve a person's existing default context while creating an isolated context with explicit test settings in the same browser process.

Several similarly named options solve different problems:

  • ignoreDefaultArgs belongs to browser launch APIs. It changes command-line arguments when Playwright starts a browser. It is not the connection setting discussed here.
  • viewport: null opts out of Playwright's fixed viewport for a newly created context. It does not preserve all defaults of an attached context.
  • Omitting colorScheme from a new context accepts Playwright's documented context default. It does not ask the context to inherit every host preference.
  • noDefaults does not disable auto-waiting, locator strictness, actionability checks, or assertions.

That last point prevents a common debugging detour. If a click waits for an element to become visible or stable, the flag did not turn actionability off. If a selector resolves to two elements, it did not relax locator strictness. The only supported behavior changes are the attachment defaults named in the API documentation.

The distinction between observation and test control should drive the decision. An assistant connecting to a user's daily browser often wants the browser's real focus and media state. A regression suite usually wants every run to start with the same theme, motion preference, download policy, and storage. Those are different jobs, even if both use CDP.

Preserve an existing browser without guessing its state

Use a dedicated Chrome profile for automation even when the goal is to preserve browser defaults. Current Chrome restrictions make the normal daily profile an unsafe remote-debugging target, and shared personal state is a poor test fixture. “Daily-driver browser” should describe a browser managed outside Playwright, not permission to attach to an unprotected personal profile.

Start Chrome with a separate user data directory:

Shell
mkdir -p /tmp/tta-preserved-profile

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 \
  --user-data-dir=/tmp/tta-preserved-profile \
  about:blank

Set the profile's theme and download behavior manually if those values are part of the case. Record them in the test ticket. A preserved unknown state is still unknown.

This Playwright Test connects with noDefaults, finds or creates one page in the existing context, and attaches a diagnostic snapshot to the report. The code uses only public APIs from Playwright 1.60 or later.

TypeScript
import { chromium, expect, test } from '@playwright/test';

test('observes the attached context without attach-time defaults', async ({}, testInfo) => {
  const browser = await chromium.connectOverCDP(
    'http://127.0.0.1:9222',
    {
      noDefaults: true,
      isLocal: true,
      timeout: 10_000,
    },
  );

  try {
    const [context] = browser.contexts();
    expect(context, 'Expected Chromium default context').toBeTruthy();

    const page = context.pages()[0] ?? await context.newPage();
    await page.goto('https://example.com/');

    const observed = await page.evaluate(() => ({
      colorSchemeDark: matchMedia('(prefers-color-scheme: dark)').matches,
      reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
      forcedColors: matchMedia('(forced-colors: active)').matches,
      prefersMoreContrast: matchMedia('(prefers-contrast: more)').matches,
      hasFocus: document.hasFocus(),
      visibilityState: document.visibilityState,
    }));

    await testInfo.attach('attached-context-state', {
      body: Buffer.from(JSON.stringify(observed, null, 2)),
      contentType: 'application/json',
    });

    await expect(page).toHaveTitle('Example Domain');
  } finally {
    await browser.close();
  }
});

An attachment from a foreground Chrome window on a dark desktop might contain:

JSON
{
  "colorSchemeDark": true,
  "reducedMotion": false,
  "forcedColors": false,
  "prefersMoreContrast": false,
  "hasFocus": true,
  "visibilityState": "visible"
}

That output is evidence, not a universal expected result. Move another application in front of Chrome and hasFocus may become false. Change the operating system theme and colorSchemeDark may change. A remote desktop session can affect focus independently of Playwright. The purpose of the probe is to capture what the page observed during that attempt.

The example includes isLocal: true because Chrome and the test process were started on the same host. Do not copy that property into a remote setup without verifying topology. noDefaults and isLocal are independent. One controls attach-time context overrides; the other describes whether file-system optimizations are safe.

Run the probe alone:

Shell
npx playwright test tests/preserved-context.spec.ts --workers=1

One worker prevents two clients from competing for the same page while you establish a baseline. Parallel clients can move focus, navigate a shared tab, or trigger simultaneous downloads. Those failures can look exactly like a changed default, but they come from shared ownership.

Test downloads and media without hiding the trade-off

Download behavior is where an opt-out can surprise a conventional test suite. With ordinary Playwright-managed contexts, downloads are accepted by default and remain associated with the context that created them. With noDefaults: true on the attached default context, Playwright leaves the browser's existing download acceptance setting alone.

That is correct for an observational tool and risky for a deterministic export test. A user's browser might prompt for a destination, block multiple downloads, or enforce enterprise policy. The connection option should not silently override those choices.

Use a small probe to find out what your managed profile does:

TypeScript
import path from 'node:path';
import { chromium, expect, test } from '@playwright/test';

test('checks the preserved profile download policy', async ({}, testInfo) => {
  const browser = await chromium.connectOverCDP(
    'http://127.0.0.1:9222',
    { noDefaults: true, isLocal: true },
  );

  try {
    const [context] = browser.contexts();
    if (!context) throw new Error('Default context is missing');

    const page = context.pages()[0] ?? await context.newPage();
    await page.setContent(`
      <a id="download"
         download="health-check.txt"
         href="data:text/plain,download%20works">
        Download health check
      </a>
    `);

    const downloadPromise = page.waitForEvent('download');
    await page.getByRole('link', { name: 'Download health check' }).click();
    const download = await downloadPromise;

    expect(await download.failure()).toBeNull();
    expect(download.suggestedFilename()).toBe('health-check.txt');

    await download.saveAs(
      path.join(testInfo.outputDir, download.suggestedFilename()),
    );
  } finally {
    await browser.close();
  }
});

If policy blocks the download, that failure is the result of the probe. Do not make it pass by adding an arbitrary wait. Inspect Chrome's download settings and enterprise policy, then decide whether preservation or deterministic downloading is the real requirement.

Media preferences need the same discipline. A product may intentionally adapt animation and contrast to the user's environment. In that case, noDefaults lets the attached page report the profile's current behavior. A regression test that must verify a specific branch should set that branch explicitly rather than depending on the machine running CI.

One reliable pattern is to leave the existing context untouched, then create a new context for the controlled assertion:

TypeScript
import { chromium, expect, test } from '@playwright/test';

test('preserves the user context and tests dark reduced-motion UI separately', async () => {
  const browser = await chromium.connectOverCDP(
    'http://127.0.0.1:9222',
    { noDefaults: true, isLocal: true },
  );

  const testContext = await browser.newContext({
    acceptDownloads: true,
    colorScheme: 'dark',
    reducedMotion: 'reduce',
  });

  try {
    const page = await testContext.newPage();
    await page.setContent(`
      <style>
        #theme::after { content: 'light motion'; }
        @media (prefers-color-scheme: dark) {
          #theme::after { content: 'dark motion'; }
        }
        @media (prefers-color-scheme: dark) and (prefers-reduced-motion: reduce) {
          #theme::after { content: 'dark reduced'; }
        }
      </style>
      <p id="theme" aria-label="theme state"></p>
    `);

    await expect(page.locator('#theme')).toHaveCSS(
      'content',
      '"dark reduced"',
      { pseudo: 'after' },
    );
  } finally {
    await testContext.close();
    await browser.close();
  }
});

The pseudo assertion option is available in Playwright 1.60. If your project is pinned earlier, assert the media queries directly with page.evaluate() or use a visible DOM state produced by the application. Do not cast new syntax past old type definitions.

This design pays a concrete cost. The new context does not automatically inherit the default profile's cookies and open tabs. If the test needs authentication, provide an approved storageState or log in through a setup project. Copying a person's full profile into test storage trades determinism for sensitive-data risk and should not be the shortcut.

Tell an override problem from a shared-state problem

A page reporting the wrong theme after attachment points toward media defaults, but the same symptom can come from application storage. Many products save a theme choice in localStorage, a cookie, or a server-side user preference. That application value may override prefers-color-scheme entirely.

Capture both layers in the failing attempt:

TypeScript
const evidence = await page.evaluate(() => ({
  mediaDark: matchMedia('(prefers-color-scheme: dark)').matches,
  storedTheme: localStorage.getItem('theme'),
  rootTheme: document.documentElement.getAttribute('data-theme'),
  focused: document.hasFocus(),
  visibility: document.visibilityState,
}));

console.log(JSON.stringify(evidence, null, 2));

If mediaDark is true but rootTheme is light, investigate product precedence and stored state. Changing noDefaults would target the wrong layer. If mediaDark flips only when the connection uses the default behavior, the attach-time media override is a plausible cause.

Focus failures have an equally convincing near-miss. A test might assert document.hasFocus() after clicking a tab, then fail because CI moved the window behind a permission dialog or remote-desktop overlay. With focus emulation disabled, that is expected host behavior. It does not mean the page is hidden. Record both document.hasFocus() and document.visibilityState, and inspect whether another page or application took the foreground.

An assertion failure may look like this:

Example
Expected: true
Received: false

  expect(await page.evaluate(() => document.hasFocus())).toBe(true)

That output names an observation, not a cause. Check the trace around the last action, list the context's pages, and record the OS or window-manager state if focus is the product behavior under test. Trace Viewer can show Playwright actions and page snapshots, but it cannot show which unrelated desktop window covered Chrome.

Capture environment state on both sides of the user action when the product reacts live to preference changes. A single sample after failure cannot tell whether the test attached with the wrong value or the host changed while the test ran:

TypeScript
const readEnvironment = () => page.evaluate(() => ({
  dark: matchMedia('(prefers-color-scheme: dark)').matches,
  reduce: matchMedia('(prefers-reduced-motion: reduce)').matches,
  focused: document.hasFocus(),
  visibility: document.visibilityState,
  rootTheme: document.documentElement.getAttribute('data-theme'),
}));

const before = await readEnvironment();
await page.getByRole('button', { name: 'Open preview' }).click();
const after = await readEnvironment();

console.log(JSON.stringify({ before, after }, null, 2));

If dark changes and the root theme follows, the product may be responding correctly to a real host update. If the media value stays constant while rootTheme changes, inspect application storage or JavaScript. If only focus changes, look for a popup, permission prompt, or competing operator. This comparison turns one visual mismatch into a bounded branch.

The cost is that a preserved environment can change mid-test. Normal emulated contexts hold the chosen media values steady unless the test changes them. An assisted-browser workflow accepts that variance because it wants the current user environment; a blocking regression suite usually should not.

Downloads also have a near-miss. A click can fail to produce a download because the application returned a JSON error, opened a popup, or navigated to an HTML login page. Before blaming browser policy, observe the event and the triggering network response. If no download event arrives, inspect the click's resulting request, popup, or navigation. If a Download object exists and download.failure() returns a reason, the browser-side download path is further along.

Use API logs for a minimal reproduction:

Shell
DEBUG=pw:api npx playwright test tests/preserved-context.spec.ts --workers=1

Also print the installed client version:

Shell
npx playwright --version

When TypeScript says that noDefaults is not a known property, the client is older than 1.60 or package versions are inconsistent. When the connection fails before a browser object is returned, test endpoint reachability and headers. When the connection works but the context array is empty, confirm that the URL identifies the browser endpoint and that Chromium was started in a supported way.

The diagnostic order should follow the first broken boundary: client supports option, endpoint connects, default context exists, expected page exists, page reports environment, application applies its own preference, product assertion passes. Skipping directly to a longer timeout hides which boundary failed.

Detect a replaced browser before blaming attach defaults

A restarted browser can produce almost the same report as an attach-time override. The CDP endpoint accepts the connection, a default context exists, and the test observes light mode, no familiar tab, or a different download policy. With noDefaults: true, those values may be preserved exactly as found. They are simply the values of a new browser process or a different profile, not the state of the process that setup prepared.

This distinction matters most with supervised Chrome instances. A health check can pass against one process, the supervisor can replace it, and the test can connect to a new process at the same host and port. A pooled browser service can also route two connections to different workers while keeping one stable public endpoint. Neither event requires a Playwright connection error. The option cannot preserve state that disappeared before attachment.

Add a browser-generation record outside the page assertion. Use an identifier or launch record supplied by the infrastructure that starts Chrome, together with the managed profile location and process start time. If the deployment exposes the browser-level WebSocket URL through its normal endpoint metadata, its browser identifier can be retained in sanitized form for the duration of the run. Do not publish the complete endpoint when it contains credentials. The exact source is deployment-specific; the requirement is one value that changes when the owned browser process is replaced.

Read that generation value beside the context probe. A healthy preservation attempt has the same generation at browser preparation and Playwright attachment, the expected managed profile, and a plausible pre-connection page count. Its media and focus values may vary, because that is the state being preserved, but the identity of the browser does not. A replaced-process attempt has a different generation or launch time, often accompanied by a reset page set or a missing workflow marker. A wrong-target attempt may have a stable generation that never matched the one setup recorded.

browser.version() is a misleading continuity check. A replacement process normally runs the same Chrome build and returns the same version string. A successful title assertion is also misleading because both old and new processes can navigate to the same public page. Even the same profile path in configuration is insufficient if the new process started before the previous profile writes were durable or if two hosts interpret that path independently. Process generation, profile ownership, and observed context state must agree.

Land continuity evidence before changing the connection mode in an existing suite. First, have the launcher publish a safe generation value and have the connection helper attach it to the test result without changing noDefaults. Next, prove the existing job sees one generation from preparation through disconnect. Then enable preserve mode for a canary and compare environment probes. If the canary changes state while generation stays fixed, attach defaults or another actor in the same browser remain plausible. If generation changes, fix browser lifecycle before comparing media or download behavior.

The ordering prevents a common rollout trap. Without continuity evidence, a canary failure invites a mode rollback even when Chrome was replaced. The rollback may appear to work on the next attempt simply because that attempt keeps one process alive. That is correlation, not proof. Preserve the first attempt's generation and do not merge it with retry evidence.

Browser infrastructure owns unexpected replacement, endpoint routing, and the managed profile. Test automation owns the selected connection mode, page selection, and before-and-after environment probe. The application team owns a mismatch between stable media inputs and the rendered product state. A handoff should contain the test and retry identity, sanitized endpoint class, browser generation at preparation and connection, launch time, expected profile label, context page count before actions, safe page origins, observed media and focus values, and the first product assertion that diverged. Avoid cookies, full URLs with tokens, storage dumps, and personal tab titles.

Continuity recording adds operational coupling. The launcher must expose one safe identity, the test fixture must carry it, and pooled services must define what “same browser” means. Capturing page inventories also creates privacy and retention work, so log only origins or workflow-owned markers. The cost is justified for a preserved-session workflow, but it is needless machinery for isolated contexts that are designed to be disposable.

This check does not prove that the preserved settings are correct. A stable browser can still hold an unwanted enterprise policy, stale application preference, or extension-modified page. Generation evidence answers “is this the same browser?” It does not answer “is this the intended user state?” The media, storage, policy, and product assertions still have separate owners.

Roll out preservation as an explicit mode

An existing CDP suite probably assumes Playwright's current defaults even if nobody wrote those assumptions down. Switching noDefaults on globally can change theme, motion, focus, contrast, and downloads in one run. That is too broad for a safe rollout.

Centralize the connection first:

TypeScript
import { chromium } from '@playwright/test';

export async function connectToManagedChrome() {
  const endpoint = process.env.CDP_ENDPOINT;
  if (!endpoint) throw new Error('CDP_ENDPOINT is required');

  const mode = process.env.CDP_CONTEXT_MODE;
  if (mode !== 'preserve' && mode !== 'test-defaults') {
    throw new Error('CDP_CONTEXT_MODE must be preserve or test-defaults');
  }

  return chromium.connectOverCDP(endpoint, {
    noDefaults: mode === 'preserve',
    timeout: 15_000,
  });
}

The mode name communicates intent better than exposing a raw boolean to every job. “Preserve” means the browser's current settings matter. “Test defaults” means the suite accepts Playwright's attach-time defaults. Keep locality in a separate setting because it describes a different fact.

Inventory tests that assert any of these signals:

  • CSS driven by color scheme, reduced motion, forced colors, or contrast
  • focus, blur, visibility, clipboard, or keyboard routing
  • file downloads and export flows
  • behavior in pre-existing tabs or authenticated sessions

Run those tests in a small matrix with both modes. Attach the environment probe to each result. A visual difference with matching product state may require snapshot baselines; a focus-only difference may reveal a test that was asserting harness behavior rather than customer behavior.

Do not update every snapshot immediately. First determine whether the old image represented Playwright's emulated default or the actual browser configuration you now intend to preserve. Snapshot churn is evidence of changed inputs, not approval of the new output.

For CI, pin the Playwright version and browser version during the comparison. Otherwise a browser upgrade can change default media behavior or download policy at the same time as the connection flag. The release should have one controlled variable.

Keep first-attempt evidence. A retry may bring Chrome to the foreground and turn a focus failure green. That does not prove stability. The first report should retain the probe, trace, list of pages, and sanitized context mode.

Set a measurable acceptance rule before expanding the mode. For an assisted-browser workflow, that rule might require the reported media queries to match a manually verified profile, no unexpected download-policy change, and no new tab created during connection. For a CI suite, it might require identical product assertions across both modes while allowing the environment attachment to differ. A vague goal such as “the browser feels untouched” cannot block a regression or explain one.

Watch resource ownership during the comparison. The attached default context belongs to the browser and cannot be treated like a disposable per-test fixture. A test that closes every page after it finishes may erase the state the next assisted step needed. A test that leaves pages behind may make the next run select the wrong tab. Identify pages by a verified URL or a marker controlled by the workflow, and close only pages the test created. Record the page count before and after each canary run.

If several jobs need the same long-lived browser, serialize them at the infrastructure layer. Playwright workers know how to isolate ordinary test contexts, but they cannot stop an external operator or a second pipeline from driving the same default context. A lock adds queue time, which is the real price of shared-browser fidelity. Separate browser instances cost more memory but remove that queue and give failures a single owner.

Review retention as well. A state attachment can contain URLs, feature flags, or account-specific values. Keep the diagnostic fields narrow, avoid cookies and storage dumps, and apply the same access controls used for traces. Preserving a profile should not lead to publishing that profile's sensitive state in CI artifacts.

The main cost of preservation is environmental variance. Two runners can report different operating-system preferences. The main cost of Playwright defaults is intervention: the attached browser no longer behaves exactly as its owner configured it. Choose the cost that matches the job, and label it in configuration.

Know when a fresh context is the better tool

Leave noDefaults off for ordinary end-to-end CI when the suite expects controlled inputs. Predictable downloads and media settings are features of a test harness, not contamination, when every run should exercise the same branch.

Do not use preservation to reuse a personal login. A dedicated test account and controlled storage state are easier to revoke, audit, and reproduce. CDP access can expose sensitive browser state, so endpoint protection and profile ownership matter more than convenience.

Avoid the option when the failure involves auto-waiting, selector resolution, navigation, or response status. Those mechanisms are outside its documented scope. Changing an unrelated connection flag adds noise to the investigation.

Do not assume it makes the default context isolated. Existing pages can still navigate, extensions can still run, and another operator can still move focus or alter storage. If parallel reliability matters, create separate contexts or separate browser processes.

Skip it when you cannot state the expected host behavior. Preserving an uncontrolled theme, unknown enterprise policy, and arbitrary focus gives you realism without a test oracle. That is useful for exploration, not for a blocking regression check.

Prefer a new context when the product assertion needs explicit colorScheme, reducedMotion, forcedColors, contrast, locale, permissions, or downloads. The new context costs setup time and does not inherit the default profile's live state, but it buys reproducibility and clean teardown.

Finally, remember that a CDP connection has lower fidelity than Playwright's own protocol connection. If you control browser startup and do not need an existing Chrome session, standard Playwright fixtures are usually simpler. noDefaults is a precise escape hatch for preserving an attached context, not a new default architecture for every automation suite.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 7, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

What does noDefaults change when Playwright attaches over CDP?

With `noDefaults: true`, Playwright leaves the existing default context's download setting, focus behavior, and media preferences at the browser's current values. The option does not change contexts created later with `browser.newContext()`.

Can I use noDefaults with a Playwright version older than 1.60?

No. Support began in Playwright 1.60, so older TypeScript definitions and clients do not recognize the property. Upgrade the Playwright packages together before adding it.

Will noDefaults preserve cookies and open tabs in my Chrome profile?

Existing profile state comes from attaching to the browser's default context, not from this flag. `noDefaults` limits specific Playwright overrides, but it neither clears nor guarantees cookies, tabs, local storage, extensions, or permissions.

Why did a dark-mode assertion change after enabling noDefaults?

Your page is now more likely to observe the browser or operating system preference instead of Playwright's attach-time media defaults. Set an explicit media value in a new test context when the assertion needs a deterministic theme.

Is noDefaults a good choice for normal CI tests?

Usually, a fresh context with explicit options is easier to reproduce in CI. Reserve `noDefaults` for cases where preserving the attached default context is part of the test or tooling requirement.