SurfSense/.cursor/skills/playwright-testing/error-index.md
2026-05-04 13:54:13 +05:30

58 KiB
Executable file

Playwright Error Index

Quick-reference for specific Playwright error messages. Find your error, understand the cause, apply the fix.

How to use: Search this file for the exact error text you see in your terminal or test report. Each entry gives you the cause and a working fix.


Locator & Element Errors


"locator.click: Target closed"

Cause: The page or frame navigated away (or was closed) before Playwright finished executing the action on the element.

Common triggers:

  • Clicking a link that triggers navigation while another action is still pending
  • A form submit causes a page reload before a subsequent click() or fill() resolves
  • Calling page.close() or context.close() in a finally block that races with an in-flight action
  • An unhandled exception in the application triggers an error page redirect

Fix: Wait for navigation to complete before performing the next action, or use Promise.all to coordinate the click and navigation together.

// TypeScript — wait for navigation caused by clicking a link
await Promise.all([
  page.waitForURL('**/dashboard'),
  page.getByRole('link', { name: 'Dashboard' }).click(),
]);
// JavaScript — same pattern
await Promise.all([
  page.waitForURL('**/dashboard'),
  page.getByRole('link', { name: 'Dashboard' }).click(),
]);

If the page is closing intentionally (e.g., a popup), capture a reference before the action:

// TypeScript — handle popup that closes itself
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Open popup' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
// interact with popup before it closes
await popup.getByRole('button', { name: 'Confirm' }).click();
// JavaScript — handle popup that closes itself
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Open popup' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
await popup.getByRole('button', { name: 'Confirm' }).click();

Related: core/assertions-and-waiting.md, core/multi-context-and-popups.md


"waiting for locator('...') to be visible"

Cause: Playwright's auto-waiting timed out because the element never appeared in the DOM or remained hidden (e.g., display: none, visibility: hidden, zero size, or behind aria-hidden).

Common triggers:

  • The element renders conditionally and the condition was not met (missing data, unauthenticated state)
  • Content loads asynchronously and the locator was evaluated before the API response arrived
  • The selector is wrong — typo in role, name, or test ID
  • The element exists but is off-screen or inside a collapsed container
  • CSS animation or transition keeps the element at opacity: 0 without making it visible to Playwright

Fix: First confirm the locator matches what you expect using the Playwright Inspector. Then ensure the precondition for the element to appear is met.

// TypeScript — debug: check what the locator resolves to
console.log(await page.getByRole('button', { name: 'Submit' }).count());
// If 0, the element is not in the DOM — check your selector or page state

// Correct approach: wait for the data to load first
await page.waitForResponse(resp =>
  resp.url().includes('/api/data') && resp.status() === 200
);
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
// JavaScript — same approach
console.log(await page.getByRole('button', { name: 'Submit' }).count());

await page.waitForResponse(resp =>
  resp.url().includes('/api/data') && resp.status() === 200
);
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();

Related: core/locators.md, core/assertions-and-waiting.md


"locator.click: Error: strict mode violation"

Cause: The locator matched more than one element and Playwright's strict mode (enabled by default) refuses to act on ambiguous matches.

Common triggers:

  • Using getByRole('button') when multiple buttons exist on the page
  • Using getByText('Save') when both "Save" and "Save as draft" are present
  • Partial text matching when exact: true is needed
  • The same component is rendered multiple times (e.g., in a list)

Fix: Make the locator more specific so it resolves to exactly one element.

// TypeScript

// BAD — matches multiple buttons
await page.getByRole('button', { name: 'Save' }).click();

// GOOD — use exact matching
await page.getByRole('button', { name: 'Save', exact: true }).click();

// GOOD — scope to a specific container
await page.getByRole('dialog')
  .getByRole('button', { name: 'Save' }).click();

// GOOD — use .first(), .last(), .nth() when order is meaningful
await page.getByRole('listitem').first().click();

// GOOD — chain with filter for list items
await page.getByRole('listitem')
  .filter({ hasText: 'Project Alpha' })
  .getByRole('button', { name: 'Delete' }).click();
// JavaScript

// BAD — matches multiple buttons
await page.getByRole('button', { name: 'Save' }).click();

// GOOD — use exact matching
await page.getByRole('button', { name: 'Save', exact: true }).click();

// GOOD — scope to a specific container
await page.getByRole('dialog')
  .getByRole('button', { name: 'Save' }).click();

// GOOD — use .first(), .last(), .nth() when order is meaningful
await page.getByRole('listitem').first().click();

// GOOD — chain with filter for list items
await page.getByRole('listitem')
  .filter({ hasText: 'Project Alpha' })
  .getByRole('button', { name: 'Delete' }).click();

Related: core/locators.md, core/locator-strategy.md


"Error: expect(locator).toBeVisible() — locator resolved to X elements"

Cause: Same root cause as strict mode violation above — the assertion's locator matched multiple elements, so Playwright cannot determine which one to assert on.

Common triggers:

  • Same as strict mode violation triggers above
  • Writing expect(page.locator('.item')).toBeVisible() when there are many .item elements

Fix: Narrow the locator to a single element (see strict mode violation fix above). Alternatively, if you intentionally want to check all matches:

// TypeScript — assert count instead of visibility
await expect(page.getByRole('listitem')).toHaveCount(5);

// Or assert on a specific one
await expect(page.getByRole('listitem').first()).toBeVisible();

// Or assert all are visible with a loop
for (const item of await page.getByRole('listitem').all()) {
  await expect(item).toBeVisible();
}
// JavaScript — assert count instead of visibility
await expect(page.getByRole('listitem')).toHaveCount(5);

// Or assert on a specific one
await expect(page.getByRole('listitem').first()).toBeVisible();

// Or assert all are visible with a loop
for (const item of await page.getByRole('listitem').all()) {
  await expect(item).toBeVisible();
}

Related: core/locators.md, core/assertions-and-waiting.md


"locator.fill: Error: Element is not an , or [contenteditable] element"</h3> <p dir="auto"><strong>Cause</strong>: <code>fill()</code> only works on <code><input></code>, <code><textarea></code>, or elements with <code>contenteditable</code>. The locator resolved to a different element type (e.g., a <code><div></code>, <code><label></code>, or the <code><form></code> itself).</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Locating by label text but the label and input are not properly associated via <code>for</code>/<code>id</code></li> <li>Using <code>getByText()</code> instead of <code>getByLabel()</code> or <code>getByRole('textbox')</code></li> <li>Custom components that wrap the actual <code><input></code> in a styled <code><div></code></li> <li>Rich text editors that use <code>contenteditable</code> on an inner element, not the outer wrapper</li> </ul> <p dir="auto"><strong>Fix</strong>: Target the actual input element.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — targets the label, not the input </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="c1">// GOOD — getByLabel finds the associated input </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="c1">// GOOD — target by role </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'textbox'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Email'</span> <span class="p">}).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="c1">// For contenteditable rich text editors </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'[contenteditable="true"]'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'Hello world'</span><span class="p">);</span> <span class="c1">// If fill() still doesn't work (e.g., custom widget), use keyboard input </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'textbox'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Email'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">keyboard</span><span class="p">.</span><span class="kr">type</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// BAD — targets the label, not the input </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="c1">// GOOD — getByLabel finds the associated input </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="c1">// GOOD — target by role </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'textbox'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Email'</span> <span class="p">}).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="c1">// For contenteditable rich text editors </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'[contenteditable="true"]'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'Hello world'</span><span class="p">);</span> <span class="c1">// If fill() still doesn't work (e.g., custom widget), use keyboard input </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'textbox'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Email'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">keyboard</span><span class="p">.</span><span class="nx">type</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/locators.md">core/locators.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/forms-and-validation.md">core/forms-and-validation.md</a></p> <hr> <h3 id="user-content-error-elementhandle-click-node-is-not-an-element" dir="auto">"Error: elementHandle.click: Node is not an Element"</h3> <p dir="auto"><strong>Cause</strong>: You are using the legacy ElementHandle API and the handle has gone stale — the underlying DOM node was removed or replaced by a re-render.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Storing an <code>elementHandle</code> in a variable and using it after a navigation or React/Vue re-render</li> <li>Using <code>page.$()</code> or <code>page.$$()</code> instead of Locator API</li> <li>Framework hydration replaces the server-rendered node with a client-rendered one</li> </ul> <p dir="auto"><strong>Fix</strong>: Switch from ElementHandle to Locator. Locators re-query the DOM on every action and never go stale.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — stale handle </span><span class="c1"></span><span class="kr">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">$</span><span class="p">(</span><span class="s1">'button.submit'</span><span class="p">);</span> <span class="c1">// ... page re-renders ... </span><span class="c1"></span><span class="k">await</span> <span class="nx">button</span><span class="o">!</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Node is not an Element </span><span class="c1"></span> <span class="c1">// GOOD — locator always re-queries </span><span class="c1"></span><span class="kr">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">});</span> <span class="c1">// ... page re-renders ... </span><span class="c1"></span><span class="k">await</span> <span class="nx">button</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// works fine </span></code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// BAD — stale handle </span><span class="c1"></span><span class="kr">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">$</span><span class="p">(</span><span class="s1">'button.submit'</span><span class="p">);</span> <span class="c1">// ... page re-renders ... </span><span class="c1"></span><span class="kr">await</span> <span class="nx">button</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Node is not an Element </span><span class="c1"></span> <span class="c1">// GOOD — locator always re-queries </span><span class="c1"></span><span class="kr">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">});</span> <span class="c1">// ... page re-renders ... </span><span class="c1"></span><span class="kr">await</span> <span class="nx">button</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// works fine </span></code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/locators.md">core/locators.md</a></p> <hr> <h3 id="user-content-error-locator-click-timeout-30000ms-exceeded" dir="auto">"Error: locator.click: Timeout 30000ms exceeded"</h3> <p dir="auto"><strong>Cause</strong>: The element was found in the DOM but was not actionable within the timeout. "Actionable" means visible, stable (not animating), enabled, and not obscured by another element.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>An overlay, modal, or toast is covering the element</li> <li>The element is disabled (<code>disabled</code> attribute or <code>aria-disabled="true"</code>)</li> <li>CSS animation has not completed (element is still moving)</li> <li>A loading spinner overlays the button</li> <li>The element is outside the viewport and Playwright's auto-scroll failed</li> </ul> <p dir="auto"><strong>Fix</strong>: Identify what is blocking the action using traces, then address it.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// Step 1: Enable tracing to diagnose </span><span class="c1">// In playwright.config.ts: </span><span class="c1">// use: { trace: 'on-first-retry' } </span><span class="c1">// Then run: npx playwright show-trace trace.zip </span><span class="c1"></span> <span class="c1">// Common fix: wait for the overlay to disappear </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.loading-overlay'</span><span class="p">)).</span><span class="nx">toBeHidden</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Common fix: force click when you know the overlay is benign </span><span class="c1">// (use sparingly — this skips actionability checks) </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">({</span> <span class="nx">force</span>: <span class="kt">true</span> <span class="p">});</span> <span class="c1">// Common fix: wait for the element to be enabled </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">})).</span><span class="nx">toBeEnabled</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// Common fix: wait for the overlay to disappear </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.loading-overlay'</span><span class="p">)).</span><span class="nx">toBeHidden</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Common fix: force click (use sparingly) </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">({</span> <span class="nx">force</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span> <span class="c1">// Common fix: wait for the element to be enabled </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">})).</span><span class="nx">toBeEnabled</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Submit'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/debugging.md">core/debugging.md</a></p> <hr> <h2 id="user-content-navigation-page-errors" dir="auto">Navigation & Page Errors</h2> <hr> <h3 id="user-content-page-goto-net-err_connection_refused" dir="auto">"page.goto: net::ERR_CONNECTION_REFUSED"</h3> <p dir="auto"><strong>Cause</strong>: Playwright could not connect to the target URL. The server is not running or is not listening on the expected host/port.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Forgot to start the dev server before running tests</li> <li>Dev server is on a different port than <code>baseURL</code> in config</li> <li>Server is listening on <code>127.0.0.1</code> but test uses <code>localhost</code> (or vice versa) and the OS resolves them differently</li> <li>In CI, the application build/start step failed silently</li> <li>Docker container networking: the app runs inside a container but the test tries to reach <code>localhost</code></li> </ul> <p dir="auto"><strong>Fix</strong>: Use the <code>webServer</code> config option to let Playwright start and manage the dev server automatically.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — playwright.config.ts </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">webServer</span><span class="o">:</span> <span class="p">{</span> <span class="nx">command</span><span class="o">:</span> <span class="s1">'npm run dev'</span><span class="p">,</span> <span class="nx">url</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="nx">reuseExistingServer</span><span class="o">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span> <span class="nx">timeout</span>: <span class="kt">120_000</span><span class="p">,</span> <span class="c1">// 2 minutes for server start </span><span class="c1"></span> <span class="p">},</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">baseURL</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — playwright.config.js </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">webServer</span><span class="o">:</span> <span class="p">{</span> <span class="nx">command</span><span class="o">:</span> <span class="s1">'npm run dev'</span><span class="p">,</span> <span class="nx">url</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="nx">reuseExistingServer</span><span class="o">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span> <span class="nx">timeout</span><span class="o">:</span> <span class="mi">120_000</span><span class="p">,</span> <span class="p">},</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">baseURL</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/configuration.md">core/configuration.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/ci/ci-github-actions.md">ci/ci-github-actions.md</a></p> <hr> <h3 id="user-content-page-goto-timeout-30000ms-exceeded" dir="auto">"page.goto: Timeout 30000ms exceeded"</h3> <p dir="auto"><strong>Cause</strong>: The page did not reach the <code>load</code> state (by default) within the navigation timeout. The server responded but the page took too long to fully load.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Large assets (images, fonts, scripts) slow the page load</li> <li>Third-party scripts (analytics, ads) blocking the load event</li> <li>The page makes many API calls before becoming interactive</li> <li>The server responded with a redirect chain that takes too long</li> </ul> <p dir="auto"><strong>Fix</strong>: Use a more appropriate <code>waitUntil</code> option or increase the navigation timeout.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// Use 'domcontentloaded' instead of 'load' if you don't need all assets </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">waitUntil</span><span class="o">:</span> <span class="s1">'domcontentloaded'</span> <span class="p">});</span> <span class="c1">// Or increase the navigation timeout for known slow pages </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">timeout</span>: <span class="kt">60_000</span> <span class="p">});</span> <span class="c1">// Or set it globally in config </span><span class="c1">// playwright.config.ts </span><span class="c1"></span><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">navigationTimeout</span>: <span class="kt">60_000</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">waitUntil</span><span class="o">:</span> <span class="s1">'domcontentloaded'</span> <span class="p">});</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">timeout</span><span class="o">:</span> <span class="mi">60_000</span> <span class="p">});</span> <span class="c1">// playwright.config.js </span><span class="c1"></span><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">navigationTimeout</span><span class="o">:</span> <span class="mi">60_000</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/configuration.md">core/configuration.md</a></p> <hr> <h3 id="user-content-error-page-goto-navigation-failed-because-page-was-closed" dir="auto">"Error: page.goto: Navigation failed because page was closed!"</h3> <p dir="auto"><strong>Cause</strong>: The page or browser context was closed while <code>goto()</code> was still in progress. This typically happens in teardown/cleanup race conditions.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li><code>afterEach</code> or a fixture closes the page/context before navigation completes</li> <li>Another test or hook called <code>browser.close()</code> prematurely</li> <li>A test failed and the cleanup ran while an async navigation was still pending</li> <li>Using <code>test.fixme()</code> or <code>test.skip()</code> after an async operation has started</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure cleanup only runs after all page operations complete. Use fixtures for lifecycle management instead of manual <code>afterEach</code>.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — race condition between test body and cleanup </span><span class="c1"></span><span class="nx">test</span><span class="p">.</span><span class="nx">afterEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="c1">// may race with in-flight navigation </span><span class="c1"></span><span class="p">});</span> <span class="c1">// GOOD — let Playwright manage page lifecycle via fixtures </span><span class="c1">// Playwright automatically creates and destroys the page per test </span><span class="c1">// No manual cleanup needed </span><span class="c1"></span> <span class="c1">// If you need custom cleanup, make sure to await all navigation first </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'navigates to dashboard'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">responsePromise</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForResponse</span><span class="p">(</span><span class="s1">'**/api/user'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">responsePromise</span><span class="p">;</span> <span class="c1">// ensure all async ops are done </span><span class="c1"></span> <span class="c1">// test assertions here </span><span class="c1"></span><span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — same pattern </span><span class="c1"></span> <span class="nx">test</span><span class="p">(</span><span class="s1">'navigates to dashboard'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">responsePromise</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForResponse</span><span class="p">(</span><span class="s1">'**/api/user'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">responsePromise</span><span class="p">;</span> <span class="c1">// test assertions here </span><span class="c1"></span><span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/fixtures-and-hooks.md">core/fixtures-and-hooks.md</a></p> <hr> <h3 id="user-content-page-waitfornavigation-timeout-30000ms-exceeded" dir="auto">"page.waitForNavigation: Timeout 30000ms exceeded"</h3> <p dir="auto"><strong>Cause</strong>: <code>waitForNavigation()</code> waited for a full page navigation that never happened. In SPAs, client-side routing does not trigger a navigation event.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Using <code>waitForNavigation()</code> with a SPA that uses React Router, Vue Router, or Next.js client-side navigation</li> <li>The navigation already happened before <code>waitForNavigation()</code> was called (race condition)</li> <li>Using the deprecated pattern when <code>waitForURL()</code> is the correct replacement</li> </ul> <p dir="auto"><strong>Fix</strong>: Replace <code>waitForNavigation()</code> with <code>waitForURL()</code> which works with both SPAs and traditional page loads.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — deprecated, race-prone, doesn't work with SPA routing </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForNavigation</span><span class="p">();</span> <span class="c1">// GOOD — works with SPAs and full navigations </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'link'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Dashboard'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForURL</span><span class="p">(</span><span class="s1">'**/dashboard'</span><span class="p">);</span> <span class="c1">// GOOD — combine with glob patterns </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForURL</span><span class="p">(</span><span class="sr">/\/dashboard\/\d+/</span><span class="p">);</span> <span class="c1">// GOOD — for SPA route changes, assert on visible content </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'link'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Settings'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'heading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Settings'</span> <span class="p">})).</span><span class="nx">toBeVisible</span><span class="p">();</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// BAD — deprecated, race-prone </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForNavigation</span><span class="p">();</span> <span class="c1">// GOOD — works with SPAs and full navigations </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'link'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Dashboard'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForURL</span><span class="p">(</span><span class="s1">'**/dashboard'</span><span class="p">);</span> <span class="c1">// GOOD — combine with glob patterns </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForURL</span><span class="p">(</span><span class="sr">/\/dashboard\/\d+/</span><span class="p">);</span> <span class="c1">// GOOD — for SPA route changes, assert on visible content </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'link'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Settings'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'heading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Settings'</span> <span class="p">})).</span><span class="nx">toBeVisible</span><span class="p">();</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a></p> <hr> <h3 id="user-content-error-frame-goto-frame-was-detached" dir="auto">"Error: frame.goto: Frame was detached"</h3> <p dir="auto"><strong>Cause</strong>: The iframe was removed from the DOM (detached) while Playwright was navigating or interacting with it.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>The parent page re-rendered and recreated the iframe element</li> <li>JavaScript on the parent page removed and re-added the iframe</li> <li>A SPA route change unmounted the component containing the iframe</li> <li>An ad iframe reloaded itself</li> </ul> <p dir="auto"><strong>Fix</strong>: Re-acquire the frame reference after the parent page updates.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — frame reference becomes stale </span><span class="c1"></span><span class="kr">const</span> <span class="nx">frame</span> <span class="o">=</span> <span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">);</span> <span class="c1">// ... parent page re-renders ... </span><span class="c1"></span><span class="k">await</span> <span class="nx">frame</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Frame was detached </span><span class="c1"></span> <span class="c1">// GOOD — use frameLocator (re-evaluates each time) </span><span class="c1">// frameLocator itself is lazy, but the frame it points to must exist </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">).</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">).</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// GOOD — wait for the iframe to be present after a re-render </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">)).</span><span class="nx">toBeAttached</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">).</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — frameLocator re-evaluates lazily </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">).</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">).</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// GOOD — wait for the iframe to be present after a re-render </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">)).</span><span class="nx">toBeAttached</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">frameLocator</span><span class="p">(</span><span class="s1">'#my-iframe'</span><span class="p">).</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/iframes-and-shadow-dom.md">core/iframes-and-shadow-dom.md</a></p> <hr> <h2 id="user-content-test-framework-errors" dir="auto">Test Framework Errors</h2> <hr> <h3 id="user-content-error-test-timeout-of-30000ms-exceeded" dir="auto">"Error: Test timeout of 30000ms exceeded"</h3> <p dir="auto"><strong>Cause</strong>: The entire test (including all hooks and assertions) did not complete within the configured test timeout.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Waiting for an element that never appears (wrong locator, missing data)</li> <li>Unresolved <code>page.waitForEvent()</code> — the expected event never fires</li> <li>Slow test environment (CI with limited resources)</li> <li>Too many sequential actions in a single test</li> <li>A <code>beforeEach</code> hook performs expensive setup (login, data seeding)</li> </ul> <p dir="auto"><strong>Fix</strong>: Identify the slow step using <code>test.step()</code> and traces, then either fix the root cause or adjust the timeout.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// Increase timeout for a specific slow test </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'generates large report'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">test</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="mi">120</span><span class="nx">_000</span><span class="p">);</span> <span class="c1">// 2 minutes for this test only </span><span class="c1"></span> <span class="c1">// ... </span><span class="c1"></span><span class="p">});</span> <span class="c1">// Increase timeout globally in config </span><span class="c1">// playwright.config.ts </span><span class="c1"></span><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">timeout</span>: <span class="kt">60_000</span><span class="p">,</span> <span class="c1">// 60 seconds per test </span><span class="c1"></span><span class="p">});</span> <span class="c1">// Better: diagnose with test.step() to find what's slow </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'checkout flow'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">test</span><span class="p">.</span><span class="nx">step</span><span class="p">(</span><span class="s1">'add item to cart'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/products'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Add to Cart'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="p">});</span> <span class="k">await</span> <span class="nx">test</span><span class="p">.</span><span class="nx">step</span><span class="p">(</span><span class="s1">'complete checkout'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'link'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Cart'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Checkout'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="p">});</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="nx">test</span><span class="p">(</span><span class="s1">'generates large report'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="nx">test</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="mi">120_000</span><span class="p">);</span> <span class="c1">// ... </span><span class="c1"></span><span class="p">});</span> <span class="c1">// playwright.config.js </span><span class="c1"></span><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">timeout</span><span class="o">:</span> <span class="mi">60_000</span><span class="p">,</span> <span class="p">});</span> <span class="nx">test</span><span class="p">(</span><span class="s1">'checkout flow'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">test</span><span class="p">.</span><span class="nx">step</span><span class="p">(</span><span class="s1">'add item to cart'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/products'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Add to Cart'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="p">});</span> <span class="kr">await</span> <span class="nx">test</span><span class="p">.</span><span class="nx">step</span><span class="p">(</span><span class="s1">'complete checkout'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'link'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Cart'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Checkout'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="p">});</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/configuration.md">core/configuration.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/debugging.md">core/debugging.md</a></p> <hr> <h3 id="user-content-error-expect-received-tomatchsnapshot" dir="auto">"Error: expect(received).toMatchSnapshot()"</h3> <p dir="auto"><strong>Cause</strong>: The current screenshot or text does not match the stored baseline snapshot.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Legitimate UI changes that require updating the snapshot</li> <li>Different rendering between local and CI (fonts, anti-aliasing, OS-level rendering)</li> <li>Dynamic content (timestamps, random IDs, ads) that changes between runs</li> <li>Different viewport sizes or device emulation settings between environments</li> </ul> <p dir="auto"><strong>Fix</strong>: Update the snapshot if the change is intentional, or mask/hide dynamic areas.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// Update snapshots after intentional changes </span><span class="c1">// Run: npx playwright test --update-snapshots </span><span class="c1"></span> <span class="c1">// Mask dynamic content before taking a screenshot </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveScreenshot</span><span class="p">(</span><span class="s1">'dashboard.png'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">mask</span><span class="o">:</span> <span class="p">[</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.timestamp'</span><span class="p">),</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.random-ad'</span><span class="p">),</span> <span class="p">],</span> <span class="nx">maxDiffPixelRatio</span>: <span class="kt">0.01</span><span class="p">,</span> <span class="c1">// allow 1% pixel difference </span><span class="c1"></span><span class="p">});</span> <span class="c1">// Use text snapshot with a sanitizer for dynamic values </span><span class="c1"></span><span class="kr">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'main'</span><span class="p">).</span><span class="nx">textContent</span><span class="p">();</span> <span class="kr">const</span> <span class="nx">sanitized</span> <span class="o">=</span> <span class="nx">content</span><span class="o">!</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/\d{4}-\d{2}-\d{2}/g</span><span class="p">,</span> <span class="s1">'DATE'</span><span class="p">);</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">sanitized</span><span class="p">).</span><span class="nx">toMatchSnapshot</span><span class="p">(</span><span class="s1">'dashboard-content.txt'</span><span class="p">);</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// Update snapshots: npx playwright test --update-snapshots </span><span class="c1"></span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveScreenshot</span><span class="p">(</span><span class="s1">'dashboard.png'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">mask</span><span class="o">:</span> <span class="p">[</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.timestamp'</span><span class="p">),</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.random-ad'</span><span class="p">),</span> <span class="p">],</span> <span class="nx">maxDiffPixelRatio</span><span class="o">:</span> <span class="mf">0.01</span><span class="p">,</span> <span class="p">});</span> <span class="kr">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'main'</span><span class="p">).</span><span class="nx">textContent</span><span class="p">();</span> <span class="kr">const</span> <span class="nx">sanitized</span> <span class="o">=</span> <span class="nx">content</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/\d{4}-\d{2}-\d{2}/g</span><span class="p">,</span> <span class="s1">'DATE'</span><span class="p">);</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">sanitized</span><span class="p">).</span><span class="nx">toMatchSnapshot</span><span class="p">(</span><span class="s1">'dashboard-content.txt'</span><span class="p">);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/visual-regression.md">core/visual-regression.md</a></p> <hr> <h3 id="user-content-error-browsertype-launch-executable-doesn-t-exist" dir="auto">"Error: browserType.launch: Executable doesn't exist"</h3> <p dir="auto"><strong>Cause</strong>: The Playwright browser binaries are not installed on this machine or they were installed for a different Playwright version.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Fresh clone of a project without running the install step</li> <li>Upgrading <code>@playwright/test</code> without reinstalling browsers</li> <li>CI environment missing the install step</li> <li>Using <code>npm ci</code> which cleans <code>node_modules</code> but does not trigger the browser install postinstall script</li> <li>Multiple Playwright versions installed (global vs local)</li> </ul> <p dir="auto"><strong>Fix</strong>: Install the browsers.</p> <pre class="code-block"><code class="chroma language-bash display"><span class="c1"># Install all browsers</span> npx playwright install <span class="c1"># Install only the browser you need (faster in CI)</span> npx playwright install chromium <span class="c1"># Install browsers with OS dependencies (needed in CI/Docker)</span> npx playwright install --with-deps <span class="c1"># If you're on a CI Dockerfile, add this to your Dockerfile</span> <span class="c1"># RUN npx playwright install --with-deps chromium</span> </code></pre><p dir="auto">For CI pipelines, always include the install step:</p> <pre class="code-block"><code class="chroma language-yaml display"><span class="c"># GitHub Actions example</span><span class="w"> </span><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install Playwright Browsers</span><span class="w"> </span><span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npx playwright install --with-deps</span><span class="w"> </span></code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/ci/ci-github-actions.md">ci/ci-github-actions.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/ci/docker-and-containers.md">ci/docker-and-containers.md</a></p> <hr> <h3 id="user-content-error-cannot-use-import-statement-outside-a-module" dir="auto">"Error: Cannot use import statement outside a module"</h3> <p dir="auto"><strong>Cause</strong>: Node.js is running the test file as CommonJS but the file uses ES module <code>import</code> syntax without the right configuration.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Missing or misconfigured <code>tsconfig.json</code></li> <li>Using <code>.ts</code> files without <code>@playwright/test</code> properly resolving TypeScript</li> <li><code>package.json</code> does not have <code>"type": "module"</code> but test files use <code>import</code></li> <li>Running test files directly with <code>node</code> instead of through <code>npx playwright test</code></li> <li>Conflicting Babel or ts-node configuration</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure TypeScript is configured correctly and always run tests through the Playwright CLI.</p> <pre class="code-block"><code class="chroma language-bash display"><span class="c1"># Always run tests through the Playwright CLI — never with node directly</span> npx playwright <span class="nb">test</span> <span class="c1"># NOT this:</span> <span class="c1"># node tests/example.spec.ts <-- will fail</span> </code></pre><pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — tsconfig.json (minimal working config) </span><span class="c1"></span><span class="p">{</span> <span class="s2">"compilerOptions"</span><span class="o">:</span> <span class="p">{</span> <span class="s2">"target"</span><span class="o">:</span> <span class="s2">"ES2020"</span><span class="p">,</span> <span class="s2">"module"</span><span class="o">:</span> <span class="s2">"ESNext"</span><span class="p">,</span> <span class="s2">"moduleResolution"</span><span class="o">:</span> <span class="s2">"bundler"</span><span class="p">,</span> <span class="s2">"strict"</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="s2">"esModuleInterop"</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="s2">"skipLibCheck"</span><span class="o">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> </code></pre><p dir="auto">If using JavaScript without TypeScript:</p> <pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — ensure package.json has "type": "module" </span><span class="c1">// Or rename files to .mjs </span><span class="c1"></span> <span class="c1">// In playwright.config.js — use require if package.json lacks "type": "module" </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="cm">/* ... */</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/configuration.md">core/configuration.md</a></p> <hr> <h3 id="user-content-error-fixture-xxx-has-already-been-registered" dir="auto">"Error: fixture "xxx" has already been registered"</h3> <p dir="auto"><strong>Cause</strong>: A custom fixture was defined with the same name in multiple <code>test.extend()</code> calls that got merged, or the same fixture name was registered twice in the same extension.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Two different fixture files both define a fixture with the same name</li> <li>Importing the extended <code>test</code> object from multiple files that each add overlapping fixtures</li> <li>Copy-pasting fixture definitions without renaming</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure each fixture name is unique across your merged fixture chain. Use a single fixture file that combines all extensions.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — two separate extensions with the same fixture name </span><span class="c1">// fixtures/auth.ts </span><span class="c1"></span><span class="kr">export</span> <span class="kr">const</span> <span class="nx">test</span> <span class="o">=</span> <span class="nx">base</span><span class="p">.</span><span class="nx">extend</span><span class="o"><</span><span class="p">{</span> <span class="nx">user</span>: <span class="kt">User</span> <span class="p">}</span><span class="o">></span><span class="p">({</span> <span class="nx">user</span>: <span class="kt">async</span> <span class="p">({},</span> <span class="nx">use</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">},</span> <span class="p">});</span> <span class="c1">// fixtures/data.ts — CONFLICT: "user" already exists </span><span class="c1"></span><span class="kr">export</span> <span class="kr">const</span> <span class="nx">test</span> <span class="o">=</span> <span class="nx">base</span><span class="p">.</span><span class="nx">extend</span><span class="o"><</span><span class="p">{</span> <span class="nx">user</span>: <span class="kt">User</span> <span class="p">}</span><span class="o">></span><span class="p">({</span> <span class="nx">user</span>: <span class="kt">async</span> <span class="p">({},</span> <span class="nx">use</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">},</span> <span class="p">});</span> <span class="c1">// GOOD — single combined fixture file </span><span class="c1">// fixtures/index.ts </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">test</span> <span class="kr">as</span> <span class="nx">base</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">type</span> <span class="nx">MyFixtures</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">authenticatedUser</span>: <span class="kt">User</span><span class="p">;</span> <span class="nx">testData</span>: <span class="kt">TestData</span><span class="p">;</span> <span class="p">};</span> <span class="kr">export</span> <span class="kr">const</span> <span class="nx">test</span> <span class="o">=</span> <span class="nx">base</span><span class="p">.</span><span class="nx">extend</span><span class="p"><</span><span class="nt">MyFixtures</span><span class="p">>({</span> <span class="nx">authenticatedUser</span>: <span class="kt">async</span> <span class="p">({},</span> <span class="nx">use</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createUser</span><span class="p">();</span> <span class="k">await</span> <span class="nx">use</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span> <span class="k">await</span> <span class="nx">deleteUser</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span> <span class="p">},</span> <span class="nx">testData</span>: <span class="kt">async</span> <span class="p">({},</span> <span class="nx">use</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">seedData</span><span class="p">();</span> <span class="k">await</span> <span class="nx">use</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="k">await</span> <span class="nx">cleanupData</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="p">},</span> <span class="p">});</span> <span class="kr">export</span> <span class="p">{</span> <span class="nx">expect</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — single combined fixture file </span><span class="c1">// fixtures/index.js </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">test</span><span class="o">:</span> <span class="nx">base</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">test</span> <span class="o">=</span> <span class="nx">base</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span> <span class="nx">authenticatedUser</span><span class="o">:</span> <span class="kr">async</span> <span class="p">({},</span> <span class="nx">use</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">createUser</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">use</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">deleteUser</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span> <span class="p">},</span> <span class="nx">testData</span><span class="o">:</span> <span class="kr">async</span> <span class="p">({},</span> <span class="nx">use</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">seedData</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">use</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">cleanupData</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="p">},</span> <span class="p">});</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">test</span> <span class="p">};</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/fixtures-and-hooks.md">core/fixtures-and-hooks.md</a></p> <hr> <h2 id="user-content-network-api-errors" dir="auto">Network & API Errors</h2> <hr> <h3 id="user-content-page-route-pattern-should-start-with" dir="auto">"page.route: Pattern should start with..."</h3> <p dir="auto"><strong>Cause</strong>: The URL pattern passed to <code>page.route()</code> does not match the expected format. Playwright expects a glob pattern (starting with <code>**/</code> or <code>http</code>), a regex, or a predicate function.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Passing a bare path like <code>/api/users</code> instead of <code>**/api/users</code></li> <li>Missing the <code>**</code> glob prefix for relative patterns</li> <li>Passing an object or invalid type instead of a string/regex</li> </ul> <p dir="auto"><strong>Fix</strong>: Use the correct pattern format.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — bare path without glob prefix </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'/api/users'</span><span class="p">,</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">body</span><span class="o">:</span> <span class="s1">'[]'</span> <span class="p">}));</span> <span class="c1">// GOOD — glob pattern </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'**/api/users'</span><span class="p">,</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">status</span>: <span class="kt">200</span><span class="p">,</span> <span class="nx">contentType</span><span class="o">:</span> <span class="s1">'application/json'</span><span class="p">,</span> <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">([{</span> <span class="nx">id</span>: <span class="kt">1</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Alice'</span> <span class="p">}]),</span> <span class="p">})</span> <span class="p">);</span> <span class="c1">// GOOD — regex pattern </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="sr">/\/api\/users\/\d+/</span><span class="p">,</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">status</span>: <span class="kt">200</span><span class="p">,</span> <span class="nx">contentType</span><span class="o">:</span> <span class="s1">'application/json'</span><span class="p">,</span> <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">1</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Alice'</span> <span class="p">}),</span> <span class="p">})</span> <span class="p">);</span> <span class="c1">// GOOD — predicate function for complex matching </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span> <span class="nx">url</span> <span class="o">=></span> <span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="s1">'/api/'</span><span class="p">)</span> <span class="o">&&</span> <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="s1">'filter'</span><span class="p">),</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">body</span><span class="o">:</span> <span class="s1">'[]'</span> <span class="p">})</span> <span class="p">);</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — glob pattern </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'**/api/users'</span><span class="p">,</span> <span class="nx">route</span> <span class="p">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">status</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nx">contentType</span><span class="o">:</span> <span class="s1">'application/json'</span><span class="p">,</span> <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">([{</span> <span class="nx">id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Alice'</span> <span class="p">}]),</span> <span class="p">})</span> <span class="p">);</span> <span class="c1">// GOOD — regex pattern </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="sr">/\/api\/users\/\d+/</span><span class="p">,</span> <span class="nx">route</span> <span class="p">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">status</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nx">contentType</span><span class="o">:</span> <span class="s1">'application/json'</span><span class="p">,</span> <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="nx">id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Alice'</span> <span class="p">}),</span> <span class="p">})</span> <span class="p">);</span> <span class="c1">// GOOD — predicate function </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span> <span class="nx">url</span> <span class="p">=></span> <span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="s1">'/api/'</span><span class="p">)</span> <span class="o">&&</span> <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="s1">'filter'</span><span class="p">),</span> <span class="nx">route</span> <span class="p">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">body</span><span class="o">:</span> <span class="s1">'[]'</span> <span class="p">})</span> <span class="p">);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/network-mocking.md">core/network-mocking.md</a></p> <hr> <h3 id="user-content-error-apirequestcontext-get-connect-econnrefused" dir="auto">"Error: apiRequestContext.get: connect ECONNREFUSED"</h3> <p dir="auto"><strong>Cause</strong>: The API request made via <code>request.get()</code> (or <code>.post()</code>, <code>.put()</code>, etc.) could not connect to the target server. Same underlying issue as <code>page.goto: net::ERR_CONNECTION_REFUSED</code> but for API testing.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>API server not running when using <code>request</code> fixture</li> <li>Wrong <code>baseURL</code> in the config or in the API request context</li> <li>Running API tests against a service that hasn't started yet</li> <li>In CI, the API service container is not ready</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure the API server is running. Use <code>webServer</code> config to auto-start it, or add a health check.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — playwright.config.ts </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">webServer</span><span class="o">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nx">command</span><span class="o">:</span> <span class="s1">'npm run start:api'</span><span class="p">,</span> <span class="nx">url</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:4000/health" class="link">http://localhost:4000/health</a>'</span><span class="p">,</span> <span class="nx">reuseExistingServer</span><span class="o">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">command</span><span class="o">:</span> <span class="s1">'npm run start:web'</span><span class="p">,</span> <span class="nx">url</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="nx">reuseExistingServer</span><span class="o">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span> <span class="p">},</span> <span class="p">],</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">baseURL</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> <span class="c1">// In tests, use the request fixture with explicit baseURL for API calls </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'fetch users'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">request</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">request</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s1">'<a href="http://localhost:4000/api/users" class="link">http://localhost:4000/api/users</a>'</span><span class="p">);</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">()).</span><span class="nx">toBeTruthy</span><span class="p">();</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — playwright.config.js </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">webServer</span><span class="o">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nx">command</span><span class="o">:</span> <span class="s1">'npm run start:api'</span><span class="p">,</span> <span class="nx">url</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:4000/health" class="link">http://localhost:4000/health</a>'</span><span class="p">,</span> <span class="nx">reuseExistingServer</span><span class="o">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">command</span><span class="o">:</span> <span class="s1">'npm run start:web'</span><span class="p">,</span> <span class="nx">url</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="nx">reuseExistingServer</span><span class="o">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span> <span class="p">},</span> <span class="p">],</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">baseURL</span><span class="o">:</span> <span class="s1">'<a href="http://localhost:3000" class="link">http://localhost:3000</a>'</span><span class="p">,</span> <span class="p">},</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/api-testing.md">core/api-testing.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/configuration.md">core/configuration.md</a></p> <hr> <h3 id="user-content-request-was-not-handled-get-https" dir="auto">"Request was not handled: GET https://..."</h3> <p dir="auto"><strong>Cause</strong>: A route handler was registered with <code>page.route()</code> using <code>{ times: N }</code> or <code>page.unrouteAll()</code> was called, and a subsequent request matching that pattern was not intercepted — Playwright warns you that a request fell through.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Using <code>page.route()</code> with <code>{ times: 1 }</code> but the app makes the same request more than once</li> <li>Calling <code>route.fallback()</code> or <code>route.continue()</code> without a handler to catch it</li> <li>Calling <code>page.unrouteAll({ behavior: 'wait' })</code> mid-test and the app makes another request</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure your route handler covers all expected requests, or let unhandled requests pass through.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — only handles the first request, second one is unhandled </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'**/api/data'</span><span class="p">,</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">body</span><span class="o">:</span> <span class="s1">'{}'</span> <span class="p">}),</span> <span class="p">{</span> <span class="nx">times</span>: <span class="kt">1</span> <span class="p">}</span> <span class="p">);</span> <span class="c1">// GOOD — handle all requests to this URL </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'**/api/data'</span><span class="p">,</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">status</span>: <span class="kt">200</span><span class="p">,</span> <span class="nx">contentType</span><span class="o">:</span> <span class="s1">'application/json'</span><span class="p">,</span> <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">({</span> <span class="nx">items</span><span class="o">:</span> <span class="p">[]</span> <span class="p">}),</span> <span class="p">})</span> <span class="p">);</span> <span class="c1">// GOOD — if you only want to intercept once, use waitForResponse for subsequent </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'**/api/data'</span><span class="p">,</span> <span class="nx">route</span> <span class="o">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">body</span><span class="o">:</span> <span class="s1">'{"items": []}'</span> <span class="p">}),</span> <span class="p">{</span> <span class="nx">times</span>: <span class="kt">1</span> <span class="p">}</span> <span class="p">);</span> <span class="c1">// For the second call, let it go to the real server (no route needed) </span></code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — handle all requests to this URL </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="s1">'**/api/data'</span><span class="p">,</span> <span class="nx">route</span> <span class="p">=></span> <span class="nx">route</span><span class="p">.</span><span class="nx">fulfill</span><span class="p">({</span> <span class="nx">status</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nx">contentType</span><span class="o">:</span> <span class="s1">'application/json'</span><span class="p">,</span> <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="nx">items</span><span class="o">:</span> <span class="p">[]</span> <span class="p">}),</span> <span class="p">})</span> <span class="p">);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/network-mocking.md">core/network-mocking.md</a></p> <hr> <h2 id="user-content-authentication-state-errors" dir="auto">Authentication & State Errors</h2> <hr> <h3 id="user-content-error-browsercontext-storagestate-no-such-file" dir="auto">"Error: browserContext.storageState: No such file"</h3> <p dir="auto"><strong>Cause</strong>: The <code>storageState</code> path referenced in the config or test does not point to an existing file. The auth state file has not been generated yet.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Running tests before running the auth setup project</li> <li>The <code>globalSetup</code> or auth setup project did not complete successfully</li> <li>The file path in the config does not match the path used in the setup</li> <li><code>.gitignore</code> excluded the storage state file and it was not regenerated after a fresh clone</li> <li>CI cache expired and the state file was not recreated</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure the auth setup runs first and the file path is consistent.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — playwright.config.ts </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">const</span> <span class="nx">authFile</span> <span class="o">=</span> <span class="s1">'playwright/.auth/user.json'</span><span class="p">;</span> <span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">projects</span><span class="o">:</span> <span class="p">[</span> <span class="c1">// Setup project runs first and creates the auth state file </span><span class="c1"></span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'setup'</span><span class="p">,</span> <span class="nx">testMatch</span><span class="o">:</span> <span class="sr">/.*\.setup\.ts/</span><span class="p">,</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'chromium'</span><span class="p">,</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">storageState</span>: <span class="kt">authFile</span><span class="p">,</span> <span class="p">},</span> <span class="nx">dependencies</span><span class="o">:</span> <span class="p">[</span><span class="s1">'setup'</span><span class="p">],</span> <span class="c1">// ensures setup runs first </span><span class="c1"></span> <span class="p">},</span> <span class="p">],</span> <span class="p">});</span> <span class="c1">// auth.setup.ts </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">test</span> <span class="kr">as</span> <span class="nx">setup</span><span class="p">,</span> <span class="nx">expect</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">const</span> <span class="nx">authFile</span> <span class="o">=</span> <span class="s1">'playwright/.auth/user.json'</span><span class="p">;</span> <span class="nx">setup</span><span class="p">(</span><span class="s1">'authenticate'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Password'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'password123'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Sign in'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForURL</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="c1">// Save the authenticated state </span><span class="c1"></span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">context</span><span class="p">().</span><span class="nx">storageState</span><span class="p">({</span> <span class="nx">path</span>: <span class="kt">authFile</span> <span class="p">});</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — playwright.config.js </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">authFile</span> <span class="o">=</span> <span class="s1">'playwright/.auth/user.json'</span><span class="p">;</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">projects</span><span class="o">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'setup'</span><span class="p">,</span> <span class="nx">testMatch</span><span class="o">:</span> <span class="sr">/.*\.setup\.js/</span><span class="p">,</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'chromium'</span><span class="p">,</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">storageState</span><span class="o">:</span> <span class="nx">authFile</span><span class="p">,</span> <span class="p">},</span> <span class="nx">dependencies</span><span class="o">:</span> <span class="p">[</span><span class="s1">'setup'</span><span class="p">],</span> <span class="p">},</span> <span class="p">],</span> <span class="p">});</span> <span class="c1">// auth.setup.js </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">test</span><span class="o">:</span> <span class="nx">setup</span><span class="p">,</span> <span class="nx">expect</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">authFile</span> <span class="o">=</span> <span class="s1">'playwright/.auth/user.json'</span><span class="p">;</span> <span class="nx">setup</span><span class="p">(</span><span class="s1">'authenticate'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Password'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'password123'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Sign in'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForURL</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">context</span><span class="p">().</span><span class="nx">storageState</span><span class="p">({</span> <span class="nx">path</span><span class="o">:</span> <span class="nx">authFile</span> <span class="p">});</span> <span class="p">});</span> </code></pre><p dir="auto">Ensure the auth directory exists and is in <code>.gitignore</code>:</p> <pre class="code-block"><code class="chroma language-bash display">mkdir -p playwright/.auth <span class="nb">echo</span> <span class="s2">"playwright/.auth/"</span> >> .gitignore </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/authentication.md">core/authentication.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/auth-flows.md">core/auth-flows.md</a></p> <hr> <h3 id="user-content-error-target-page-context-or-browser-has-been-closed" dir="auto">"Error: Target page, context or browser has been closed"</h3> <p dir="auto"><strong>Cause</strong>: Code attempted to use a <code>page</code>, <code>context</code>, or <code>browser</code> object that has already been closed or disposed.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Using a <code>page</code> reference after <code>page.close()</code> was called</li> <li>Storing a <code>page</code> or <code>context</code> in a module-level variable and reusing it across tests</li> <li>A fixture tore down the browser context while an async callback was still running</li> <li>A <code>beforeAll</code> created a context that was closed before all tests finished using it</li> <li>Using <code>browser.newContext()</code> manually and forgetting to manage its lifecycle</li> </ul> <p dir="auto"><strong>Fix</strong>: Never store page/context references in shared mutable state. Use Playwright fixtures for lifecycle management.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — shared module-level variable </span><span class="c1"></span><span class="kd">let</span> <span class="nx">sharedPage</span>: <span class="kt">Page</span><span class="p">;</span> <span class="nx">test</span><span class="p">.</span><span class="nx">beforeAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">browser</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">sharedPage</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span> <span class="p">});</span> <span class="nx">test</span><span class="p">.</span><span class="nx">afterAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">sharedPage</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="p">});</span> <span class="nx">test</span><span class="p">(</span><span class="s1">'first'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">sharedPage</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/'</span><span class="p">);</span> <span class="c1">// might fail if afterAll from another describe ran first </span><span class="c1"></span><span class="p">});</span> <span class="c1">// GOOD — use the built-in page fixture (one per test, auto-managed) </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'first'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/'</span><span class="p">);</span> <span class="c1">// always a fresh, open page </span><span class="c1"></span><span class="p">});</span> <span class="c1">// GOOD — for shared state across tests, use a worker-scoped fixture </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">test</span> <span class="kr">as</span> <span class="nx">base</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">export</span> <span class="kr">const</span> <span class="nx">test</span> <span class="o">=</span> <span class="nx">base</span><span class="p">.</span><span class="nx">extend</span><span class="o"><</span><span class="p">{},</span> <span class="p">{</span> <span class="nx">adminContext</span>: <span class="kt">BrowserContext</span> <span class="p">}</span><span class="o">></span><span class="p">({</span> <span class="nx">adminContext</span><span class="o">:</span> <span class="p">[</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">browser</span> <span class="p">},</span> <span class="nx">use</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newContext</span><span class="p">();</span> <span class="k">await</span> <span class="nx">use</span><span class="p">(</span><span class="nx">context</span><span class="p">);</span> <span class="k">await</span> <span class="nx">context</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">scope</span><span class="o">:</span> <span class="s1">'worker'</span> <span class="p">}],</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — use the built-in page fixture </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'first'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/'</span><span class="p">);</span> <span class="p">});</span> <span class="c1">// GOOD — for shared state, use a worker-scoped fixture </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">test</span><span class="o">:</span> <span class="nx">base</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">test</span> <span class="o">=</span> <span class="nx">base</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span> <span class="nx">adminContext</span><span class="o">:</span> <span class="p">[</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">browser</span> <span class="p">},</span> <span class="nx">use</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newContext</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">use</span><span class="p">(</span><span class="nx">context</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">context</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">scope</span><span class="o">:</span> <span class="s1">'worker'</span> <span class="p">}],</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/fixtures-and-hooks.md">core/fixtures-and-hooks.md</a></p> <hr> <h2 id="user-content-ci-specific-errors" dir="auto">CI-Specific Errors</h2> <hr> <h3 id="user-content-error-browsertype-launch-browser-closed-unexpectedly" dir="auto">"Error: browserType.launch: Browser closed unexpectedly"</h3> <p dir="auto"><strong>Cause</strong>: The browser process crashed immediately after launch, usually because required OS-level dependencies are missing in the CI environment.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Running on a minimal Docker image (e.g., <code>node:alpine</code>) without browser dependencies</li> <li>Missing shared libraries (<code>libgbm</code>, <code>libnss3</code>, <code>libatk-bridge</code>, etc.) on Linux CI</li> <li>Insufficient shared memory (<code>/dev/shm</code> too small) — browsers use shared memory for rendering</li> <li>Running as root in Docker without <code>--no-sandbox</code> (Chromium refuses to run as root with sandbox)</li> <li>Out-of-memory kill on CI (browser process is OOM-killed by the OS)</li> </ul> <p dir="auto"><strong>Fix</strong>: Install OS-level dependencies and configure the environment.</p> <pre class="code-block"><code class="chroma language-bash display"><span class="c1"># Install browsers with their OS dependencies (recommended)</span> npx playwright install --with-deps chromium <span class="c1"># If using Docker, use the official Playwright image</span> <span class="c1"># Dockerfile</span> FROM mcr.microsoft.com/playwright:v1.50.0-noble WORKDIR /app COPY . . RUN npm ci RUN npx playwright install chromium CMD <span class="o">[</span><span class="s2">"npx"</span>, <span class="s2">"playwright"</span>, <span class="s2">"test"</span><span class="o">]</span> </code></pre><p dir="auto">For shared memory issues:</p> <pre class="code-block"><code class="chroma language-yaml display"><span class="c"># GitHub Actions — increase /dev/shm</span><span class="w"> </span><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w"> </span><span class="w"> </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="w"> </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w"> </span><span class="w"> </span><span class="nt">container</span><span class="p">:</span><span class="w"> </span><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mcr.microsoft.com/playwright:v1.50.0-noble</span><span class="w"> </span><span class="w"> </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>--<span class="l">shm-size=2gb</span><span class="w"> </span><span class="w"> </span><span class="w"></span><span class="c"># Docker run — increase shared memory</span><span class="w"> </span><span class="w"></span><span class="c"># docker run --shm-size=2gb my-test-image</span><span class="w"> </span></code></pre><p dir="auto">For running as root in Docker:</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — playwright.config.ts (only for Docker/CI) </span><span class="c1"></span><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">launchOptions</span><span class="o">:</span> <span class="p">{</span> <span class="nx">args</span>: <span class="kt">process.env.CI</span> <span class="o">?</span> <span class="p">[</span><span class="s1">'--no-sandbox'</span><span class="p">,</span> <span class="s1">'--disable-setuid-sandbox'</span><span class="p">]</span> <span class="o">:</span> <span class="p">[],</span> <span class="p">},</span> <span class="p">},</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — playwright.config.js </span><span class="c1"></span><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">use</span><span class="o">:</span> <span class="p">{</span> <span class="nx">launchOptions</span><span class="o">:</span> <span class="p">{</span> <span class="nx">args</span><span class="o">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span> <span class="o">?</span> <span class="p">[</span><span class="s1">'--no-sandbox'</span><span class="p">,</span> <span class="s1">'--disable-setuid-sandbox'</span><span class="p">]</span> <span class="o">:</span> <span class="p">[],</span> <span class="p">},</span> <span class="p">},</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/ci/ci-github-actions.md">ci/ci-github-actions.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/ci/docker-and-containers.md">ci/docker-and-containers.md</a></p> <hr> <h3 id="user-content-error-playwright-test-needs-to-be-invoked-via-npx-playwright-test" dir="auto">"Error: Playwright Test needs to be invoked via 'npx playwright test'"</h3> <p dir="auto"><strong>Cause</strong>: The test file was executed directly with <code>node</code> or <code>ts-node</code> instead of through the Playwright test runner.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Running <code>node tests/example.spec.ts</code> or <code>ts-node tests/example.spec.ts</code></li> <li>A misconfigured IDE run configuration that invokes <code>node</code> directly</li> <li>Using Jest or Mocha to run Playwright test files that import from <code>@playwright/test</code></li> <li>A CI script that calls the wrong command</li> </ul> <p dir="auto"><strong>Fix</strong>: Always use the Playwright CLI to run tests.</p> <pre class="code-block"><code class="chroma language-bash display"><span class="c1"># Correct invocations</span> npx playwright <span class="nb">test</span> <span class="c1"># run all tests</span> npx playwright <span class="nb">test</span> tests/login.spec.ts <span class="c1"># run specific file</span> npx playwright <span class="nb">test</span> --grep <span class="s2">"login"</span> <span class="c1"># run tests matching pattern</span> npx playwright <span class="nb">test</span> --project<span class="o">=</span>chromium <span class="c1"># run specific project</span> npx playwright <span class="nb">test</span> --debug <span class="c1"># run with inspector</span> <span class="c1"># If using a package.json script</span> <span class="c1"># package.json</span> <span class="c1"># "scripts": { "test:e2e": "playwright test" }</span> npm run test:e2e </code></pre><p dir="auto">For IDE configuration (VS Code):</p> <pre class="code-block"><code class="chroma language-json display"><span class="c1">// .vscode/settings.json — install the Playwright VS Code extension instead of custom configs </span><span class="c1"></span><span class="p">{</span> <span class="nt">"playwright.reuseBrowser"</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/configuration.md">core/configuration.md</a></p> <hr> <h2 id="user-content-additional-common-errors" dir="auto">Additional Common Errors</h2> <hr> <h3 id="user-content-error-page-evaluate-execution-context-was-destroyed" dir="auto">"Error: page.evaluate: Execution context was destroyed"</h3> <p dir="auto"><strong>Cause</strong>: The page navigated or reloaded while <code>page.evaluate()</code> was executing JavaScript in the browser context.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Calling <code>evaluate()</code> while the page is in the middle of a navigation</li> <li>A timer or event on the page triggers a redirect during evaluation</li> <li>Running <code>evaluate()</code> in a <code>beforeEach</code> that races with <code>page.goto()</code></li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure the page is stable before evaluating.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — evaluate races with navigation </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(()</span> <span class="o">=></span> <span class="nb">document</span><span class="p">.</span><span class="nx">title</span><span class="p">);</span> <span class="c1">// context might be destroyed </span><span class="c1"></span> <span class="c1">// GOOD — wait for load state first </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForLoadState</span><span class="p">(</span><span class="s1">'domcontentloaded'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(()</span> <span class="o">=></span> <span class="nb">document</span><span class="p">.</span><span class="nx">title</span><span class="p">);</span> <span class="c1">// GOOD — prefer locator-based assertions (no evaluate needed) </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveTitle</span><span class="p">(</span><span class="s1">'Dashboard'</span><span class="p">);</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — wait for load state first </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForLoadState</span><span class="p">(</span><span class="s1">'domcontentloaded'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(()</span> <span class="p">=></span> <span class="nb">document</span><span class="p">.</span><span class="nx">title</span><span class="p">);</span> <span class="c1">// GOOD — prefer locator-based assertions </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveTitle</span><span class="p">(</span><span class="s1">'Dashboard'</span><span class="p">);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a></p> <hr> <h3 id="user-content-error-page-screenshot-cannot-take-a-screenshot-larger-than" dir="auto">"Error: page.screenshot: Cannot take a screenshot larger than..."</h3> <p dir="auto"><strong>Cause</strong>: The requested screenshot dimensions exceed Playwright's limit (typically 16384x16384 pixels). This happens with full-page screenshots of very long pages.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li><code>page.screenshot({ fullPage: true })</code> on an infinitely-scrolling page</li> <li>Pages with extremely tall content (long data tables, chat logs)</li> <li>CSS bugs that create enormous element heights</li> </ul> <p dir="auto"><strong>Fix</strong>: Clip the screenshot to a specific region or limit the viewport.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — might exceed size limits on tall pages </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span> <span class="nx">fullPage</span>: <span class="kt">true</span><span class="p">,</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">'full.png'</span> <span class="p">});</span> <span class="c1">// GOOD — clip to a specific region </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">'clipped.png'</span><span class="p">,</span> <span class="nx">clip</span><span class="o">:</span> <span class="p">{</span> <span class="nx">x</span>: <span class="kt">0</span><span class="p">,</span> <span class="nx">y</span>: <span class="kt">0</span><span class="p">,</span> <span class="nx">width</span>: <span class="kt">1280</span><span class="p">,</span> <span class="nx">height</span>: <span class="kt">2000</span> <span class="p">},</span> <span class="p">});</span> <span class="c1">// GOOD — screenshot a specific element instead </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'main'</span><span class="p">).</span><span class="nx">screenshot</span><span class="p">({</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">'main-content.png'</span> <span class="p">});</span> <span class="c1">// GOOD — set a reasonable viewport before full-page screenshot </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">setViewportSize</span><span class="p">({</span> <span class="nx">width</span>: <span class="kt">1280</span><span class="p">,</span> <span class="nx">height</span>: <span class="kt">720</span> <span class="p">});</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span> <span class="nx">fullPage</span>: <span class="kt">true</span><span class="p">,</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">'full.png'</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — clip to a specific region </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">'clipped.png'</span><span class="p">,</span> <span class="nx">clip</span><span class="o">:</span> <span class="p">{</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">y</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="o">:</span> <span class="mi">1280</span><span class="p">,</span> <span class="nx">height</span><span class="o">:</span> <span class="mi">2000</span> <span class="p">},</span> <span class="p">});</span> <span class="c1">// GOOD — screenshot a specific element </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'main'</span><span class="p">).</span><span class="nx">screenshot</span><span class="p">({</span> <span class="nx">path</span><span class="o">:</span> <span class="s1">'main-content.png'</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/visual-regression.md">core/visual-regression.md</a></p> <hr> <h3 id="user-content-error-waiting-for-locator-tobeattached" dir="auto">"Error: waiting for locator('...').toBeAttached()"</h3> <p dir="auto"><strong>Cause</strong>: The element never entered the DOM within the assertion timeout. Unlike <code>toBeVisible()</code>, <code>toBeAttached()</code> only checks DOM presence — but the element was never rendered at all.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>The component is conditionally rendered and the condition is not met</li> <li>A network request that populates the data has not completed</li> <li>Wrong selector that does not match any element</li> <li>The element is rendered by a lazy-loaded chunk that has not downloaded yet</li> </ul> <p dir="auto"><strong>Fix</strong>: Verify the precondition for the element to render.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// Ensure the data loads before asserting </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/users'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForResponse</span><span class="p">(</span><span class="nx">resp</span> <span class="o">=></span> <span class="nx">resp</span><span class="p">.</span><span class="nx">url</span><span class="p">().</span><span class="nx">includes</span><span class="p">(</span><span class="s1">'/api/users'</span><span class="p">));</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'table'</span><span class="p">)).</span><span class="nx">toBeAttached</span><span class="p">();</span> <span class="c1">// For lazy-loaded content, wait longer or trigger the load </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'tab'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Advanced'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByTestId</span><span class="p">(</span><span class="s1">'advanced-panel'</span><span class="p">)).</span><span class="nx">toBeAttached</span><span class="p">({</span> <span class="nx">timeout</span>: <span class="kt">10_000</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/users'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForResponse</span><span class="p">(</span><span class="nx">resp</span> <span class="p">=></span> <span class="nx">resp</span><span class="p">.</span><span class="nx">url</span><span class="p">().</span><span class="nx">includes</span><span class="p">(</span><span class="s1">'/api/users'</span><span class="p">));</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'table'</span><span class="p">)).</span><span class="nx">toBeAttached</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'tab'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Advanced'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByTestId</span><span class="p">(</span><span class="s1">'advanced-panel'</span><span class="p">)).</span><span class="nx">toBeAttached</span><span class="p">({</span> <span class="nx">timeout</span><span class="o">:</span> <span class="mi">10_000</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/locators.md">core/locators.md</a></p> <hr> <h3 id="user-content-error-protocol-error-target-createtarget-failed-to-create-target" dir="auto">"Error: protocol error: Target.createTarget: Failed to create target"</h3> <p dir="auto"><strong>Cause</strong>: The browser could not create a new tab or context, usually due to resource exhaustion.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Opening too many pages or contexts without closing them</li> <li>Memory leak in tests — each test opens a context but never closes it</li> <li>Running many parallel workers on a resource-constrained CI machine</li> <li>Browser process is unstable after a previous crash</li> </ul> <p dir="auto"><strong>Fix</strong>: Reduce parallelism, close unused contexts, or use fewer workers.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — playwright.config.ts </span><span class="c1"></span> <span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="c1">// Reduce parallelism if resources are limited </span><span class="c1"></span> <span class="nx">workers</span>: <span class="kt">process.env.CI</span> <span class="o">?</span> <span class="nx">2</span> : <span class="kt">undefined</span><span class="p">,</span> <span class="c1">// Limit to one browser context per worker </span><span class="c1"></span> <span class="nx">fullyParallel</span>: <span class="kt">false</span><span class="p">,</span> <span class="c1">// run test files serially within a worker </span><span class="c1"></span><span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — playwright.config.js </span><span class="c1"></span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">defineConfig</span><span class="p">({</span> <span class="nx">workers</span><span class="o">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span> <span class="o">?</span> <span class="mi">2</span> <span class="o">:</span> <span class="kc">undefined</span><span class="p">,</span> <span class="nx">fullyParallel</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span> <span class="p">});</span> </code></pre><p dir="auto">If you manually create contexts, always close them:</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'multi-user test'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">browser</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">userContext</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newContext</span><span class="p">();</span> <span class="kr">const</span> <span class="nx">adminContext</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newContext</span><span class="p">();</span> <span class="k">try</span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">userPage</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userContext</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span> <span class="kr">const</span> <span class="nx">adminPage</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">adminContext</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span> <span class="c1">// ... test logic </span><span class="c1"></span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="k">await</span> <span class="nx">userContext</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="k">await</span> <span class="nx">adminContext</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="p">}</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span><span class="nx">test</span><span class="p">(</span><span class="s1">'multi-user test'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">browser</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">userContext</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newContext</span><span class="p">();</span> <span class="kr">const</span> <span class="nx">adminContext</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newContext</span><span class="p">();</span> <span class="k">try</span> <span class="p">{</span> <span class="kr">const</span> <span class="nx">userPage</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">userContext</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span> <span class="kr">const</span> <span class="nx">adminPage</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">adminContext</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span> <span class="c1">// ... test logic </span><span class="c1"></span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">userContext</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">adminContext</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="p">}</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/ci/parallel-and-sharding.md">ci/parallel-and-sharding.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/multi-user-and-collaboration.md">core/multi-user-and-collaboration.md</a></p> <hr> <h3 id="user-content-error-expect-locator-tohavetext-expected-string-but-received-array" dir="auto">"Error: expect(locator).toHaveText() — expected string but received array"</h3> <p dir="auto"><strong>Cause</strong>: The locator matched multiple elements and <code>toHaveText()</code> returned an array of strings instead of a single string.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Using a broad locator like <code>page.locator('.item')</code> that matches a list</li> <li>Expecting a single heading but the selector matches multiple</li> <li>Not scoping the locator to a specific container</li> </ul> <p dir="auto"><strong>Fix</strong>: Either narrow the locator to one element or use the array form of <code>toHaveText()</code>.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — locator matches multiple elements </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.item'</span><span class="p">)).</span><span class="nx">toHaveText</span><span class="p">(</span><span class="s1">'First Item'</span><span class="p">);</span> <span class="c1">// GOOD — narrow to one element </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.item'</span><span class="p">).</span><span class="nx">first</span><span class="p">()).</span><span class="nx">toHaveText</span><span class="p">(</span><span class="s1">'First Item'</span><span class="p">);</span> <span class="c1">// GOOD — assert against an array when multiple elements are expected </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.item'</span><span class="p">)).</span><span class="nx">toHaveText</span><span class="p">([</span> <span class="s1">'First Item'</span><span class="p">,</span> <span class="s1">'Second Item'</span><span class="p">,</span> <span class="s1">'Third Item'</span><span class="p">,</span> <span class="p">]);</span> <span class="c1">// GOOD — use a more specific locator </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'heading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">level</span>: <span class="kt">1</span> <span class="p">})).</span><span class="nx">toHaveText</span><span class="p">(</span><span class="s1">'Welcome'</span><span class="p">);</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — narrow to one element </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.item'</span><span class="p">).</span><span class="nx">first</span><span class="p">()).</span><span class="nx">toHaveText</span><span class="p">(</span><span class="s1">'First Item'</span><span class="p">);</span> <span class="c1">// GOOD — assert against an array </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.item'</span><span class="p">)).</span><span class="nx">toHaveText</span><span class="p">([</span> <span class="s1">'First Item'</span><span class="p">,</span> <span class="s1">'Second Item'</span><span class="p">,</span> <span class="s1">'Third Item'</span><span class="p">,</span> <span class="p">]);</span> <span class="c1">// GOOD — use a more specific locator </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'heading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">level</span><span class="o">:</span> <span class="mi">1</span> <span class="p">})).</span><span class="nx">toHaveText</span><span class="p">(</span><span class="s1">'Welcome'</span><span class="p">);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/locators.md">core/locators.md</a></p> <hr> <h3 id="user-content-error-page-waitforselector-timeout-30000ms-exceeded-deprecated" dir="auto">"Error: page.waitForSelector: Timeout 30000ms exceeded (deprecated)"</h3> <p dir="auto"><strong>Cause</strong>: <code>page.waitForSelector()</code> timed out. This method is deprecated in favor of locator-based assertions.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Using legacy <code>page.waitForSelector()</code> from older tutorials or migration from Puppeteer</li> <li>The selector does not match any element in the DOM</li> <li>The element exists but does not meet the state requirement (e.g., <code>{ state: 'visible' }</code> but element is hidden)</li> </ul> <p dir="auto"><strong>Fix</strong>: Replace <code>waitForSelector()</code> with locator-based assertions.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — deprecated, no auto-retry, not composable </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">'.loading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">state</span><span class="o">:</span> <span class="s1">'hidden'</span> <span class="p">});</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">'.content'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">state</span><span class="o">:</span> <span class="s1">'visible'</span> <span class="p">});</span> <span class="c1">// GOOD — locator-based assertions with auto-retry </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.loading'</span><span class="p">)).</span><span class="nx">toBeHidden</span><span class="p">();</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.content'</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span> <span class="c1">// GOOD — use role-based locators for even more resilience </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'progressbar'</span><span class="p">)).</span><span class="nx">toBeHidden</span><span class="p">();</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'main'</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// BAD — deprecated </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">'.loading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">state</span><span class="o">:</span> <span class="s1">'hidden'</span> <span class="p">});</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">'.content'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">state</span><span class="o">:</span> <span class="s1">'visible'</span> <span class="p">});</span> <span class="c1">// GOOD — locator-based assertions </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.loading'</span><span class="p">)).</span><span class="nx">toBeHidden</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'.content'</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span> <span class="c1">// GOOD — role-based locators </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'progressbar'</span><span class="p">)).</span><span class="nx">toBeHidden</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'main'</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/locators.md">core/locators.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/migration/from-selenium.md">migration/from-selenium.md</a></p> <hr> <h3 id="user-content-error-test-was-expected-to-have-a-title-matching" dir="auto">"Error: Test was expected to have a title matching /.../"</h3> <p dir="auto"><strong>Cause</strong>: The <code>expect(page).toHaveTitle()</code> assertion failed because the page title did not match the expected value or pattern within the timeout.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>The page has not finished loading and the title is still the initial empty or default value</li> <li>The SPA updates the document title asynchronously after rendering</li> <li>The expected title has a typo or does not match case-sensitively</li> <li>The page redirected to an error page with a different title</li> </ul> <p dir="auto"><strong>Fix</strong>: Ensure the page is fully loaded and use the correct expected value.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// toHaveTitle auto-retries, but ensure the page has loaded </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveTitle</span><span class="p">(</span><span class="s1">'Dashboard | MyApp'</span><span class="p">);</span> <span class="c1">// Use regex for flexible matching </span><span class="c1"></span><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveTitle</span><span class="p">(</span><span class="sr">/Dashboard/</span><span class="p">);</span> <span class="c1">// Debug: check what the actual title is </span><span class="c1"></span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">title</span><span class="p">());</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/dashboard'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveTitle</span><span class="p">(</span><span class="s1">'Dashboard | MyApp'</span><span class="p">);</span> <span class="c1">// Use regex for flexible matching </span><span class="c1"></span><span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">).</span><span class="nx">toHaveTitle</span><span class="p">(</span><span class="sr">/Dashboard/</span><span class="p">);</span> <span class="c1">// Debug: check what the actual title is </span><span class="c1"></span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">title</span><span class="p">());</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/assertions-and-waiting.md">core/assertions-and-waiting.md</a></p> <hr> <h3 id="user-content-error-page-type-element-is-not-focusable" dir="auto">"Error: page.type: Element is not focusable"</h3> <p dir="auto"><strong>Cause</strong>: <code>page.type()</code> or <code>locator.type()</code> could not focus the target element before typing. The element may be hidden, disabled, or not an input.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>The element is covered by an overlay or modal</li> <li>The element has <code>tabindex="-1"</code> and is not natively focusable</li> <li>Using <code>type()</code> instead of <code>fill()</code> — <code>fill()</code> is preferred in almost all cases</li> <li>The element is inside a Shadow DOM and the locator does not pierce it</li> </ul> <p dir="auto"><strong>Fix</strong>: Use <code>fill()</code> instead of <code>type()</code>. Only use <code>type()</code> when you specifically need to simulate individual keystrokes.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — type() is slower and more fragile </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">'#search'</span><span class="p">).</span><span class="kr">type</span><span class="p">(</span><span class="s1">'hello'</span><span class="p">);</span> <span class="c1">// GOOD — fill() clears and sets the value directly </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'searchbox'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'hello'</span><span class="p">);</span> <span class="c1">// When you truly need keystroke-by-keystroke input (e.g., autocomplete trigger) </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'searchbox'</span><span class="p">).</span><span class="nx">pressSequentially</span><span class="p">(</span><span class="s1">'hello'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">delay</span>: <span class="kt">100</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — fill() is preferred </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'searchbox'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'hello'</span><span class="p">);</span> <span class="c1">// Keystroke-by-keystroke when needed (e.g., autocomplete) </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'searchbox'</span><span class="p">).</span><span class="nx">pressSequentially</span><span class="p">(</span><span class="s1">'hello'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">delay</span><span class="o">:</span> <span class="mi">100</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/locators.md">core/locators.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/forms-and-validation.md">core/forms-and-validation.md</a></p> <hr> <h3 id="user-content-error-page-setinputfiles-non-multiple-file-input-can-only-accept-single-file" dir="auto">"Error: page.setInputFiles: Non-multiple file input can only accept single file"</h3> <p dir="auto"><strong>Cause</strong>: Attempted to upload multiple files to an <code><input type="file"></code> that does not have the <code>multiple</code> attribute.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>Passing an array of files to a single-file upload input</li> <li>The app uses a custom upload component that only handles one file at a time</li> </ul> <p dir="auto"><strong>Fix</strong>: Upload one file at a time, or ensure the input supports multiple files.</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript </span><span class="c1"></span> <span class="c1">// BAD — input does not have multiple attribute </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload file'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">([</span> <span class="s1">'file1.pdf'</span><span class="p">,</span> <span class="s1">'file2.pdf'</span><span class="p">,</span> <span class="c1">// error: non-multiple input </span><span class="c1"></span><span class="p">]);</span> <span class="c1">// GOOD — single file </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload file'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">(</span><span class="s1">'file1.pdf'</span><span class="p">);</span> <span class="c1">// GOOD — for multiple file input (must have multiple attribute in HTML) </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload files'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">([</span> <span class="s1">'file1.pdf'</span><span class="p">,</span> <span class="s1">'file2.pdf'</span><span class="p">,</span> <span class="p">]);</span> <span class="c1">// To clear the file input </span><span class="c1"></span><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload file'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">([]);</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript </span><span class="c1"></span> <span class="c1">// GOOD — single file </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload file'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">(</span><span class="s1">'file1.pdf'</span><span class="p">);</span> <span class="c1">// GOOD — multiple files (input must have multiple attribute) </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload files'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">([</span> <span class="s1">'file1.pdf'</span><span class="p">,</span> <span class="s1">'file2.pdf'</span><span class="p">,</span> <span class="p">]);</span> <span class="c1">// To clear the file input </span><span class="c1"></span><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Upload file'</span><span class="p">).</span><span class="nx">setInputFiles</span><span class="p">([]);</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/file-operations.md">core/file-operations.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/file-upload-download.md">core/file-upload-download.md</a></p> <hr> <h3 id="user-content-error-browser-newcontext-could-not-parse-content-type-application-json" dir="auto">"Error: browser.newContext: Could not parse content-type application/json"</h3> <p dir="auto"><strong>Cause</strong>: The <code>storageState</code> option was given a path to a file with invalid JSON content, or the file is not a valid Playwright storage state format.</p> <p dir="auto"><strong>Common triggers</strong>:</p> <ul dir="auto"> <li>The auth setup wrote an empty file or incomplete JSON</li> <li>The storage state file was corrupted or truncated</li> <li>Manually editing the storage state file and introducing a syntax error</li> <li>The auth setup failed silently (login did not complete) and saved an invalid state</li> </ul> <p dir="auto"><strong>Fix</strong>: Delete the storage state file and regenerate it.</p> <pre class="code-block"><code class="chroma language-bash display"><span class="c1"># Delete the corrupted file</span> rm -f playwright/.auth/user.json <span class="c1"># Re-run the setup</span> npx playwright <span class="nb">test</span> --project<span class="o">=</span>setup </code></pre><p dir="auto">To prevent this, add error checking in the auth setup:</p> <pre class="code-block"><code class="chroma language-typescript display"><span class="c1">// TypeScript — auth.setup.ts </span><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">test</span> <span class="kr">as</span> <span class="nx">setup</span><span class="p">,</span> <span class="nx">expect</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">'@playwright/test'</span><span class="p">;</span> <span class="kr">const</span> <span class="nx">authFile</span> <span class="o">=</span> <span class="s1">'playwright/.auth/user.json'</span><span class="p">;</span> <span class="nx">setup</span><span class="p">(</span><span class="s1">'authenticate'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Password'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'password123'</span><span class="p">);</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Sign in'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Verify login actually succeeded before saving state </span><span class="c1"></span> <span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'heading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Dashboard'</span> <span class="p">})).</span><span class="nx">toBeVisible</span><span class="p">();</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">context</span><span class="p">().</span><span class="nx">storageState</span><span class="p">({</span> <span class="nx">path</span>: <span class="kt">authFile</span> <span class="p">});</span> <span class="p">});</span> </code></pre><pre class="code-block"><code class="chroma language-javascript display"><span class="c1">// JavaScript — auth.setup.js </span><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">test</span><span class="o">:</span> <span class="nx">setup</span><span class="p">,</span> <span class="nx">expect</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@playwright/test'</span><span class="p">);</span> <span class="kr">const</span> <span class="nx">authFile</span> <span class="o">=</span> <span class="s1">'playwright/.auth/user.json'</span><span class="p">;</span> <span class="nx">setup</span><span class="p">(</span><span class="s1">'authenticate'</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=></span> <span class="p">{</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'user@example.com'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByLabel</span><span class="p">(</span><span class="s1">'Password'</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="s1">'password123'</span><span class="p">);</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Sign in'</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span> <span class="c1">// Verify login actually succeeded before saving state </span><span class="c1"></span> <span class="kr">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'heading'</span><span class="p">,</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">'Dashboard'</span> <span class="p">})).</span><span class="nx">toBeVisible</span><span class="p">();</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">context</span><span class="p">().</span><span class="nx">storageState</span><span class="p">({</span> <span class="nx">path</span><span class="o">:</span> <span class="nx">authFile</span> <span class="p">});</span> <span class="p">});</span> </code></pre><p dir="auto"><strong>Related</strong>: <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/authentication.md">core/authentication.md</a>, <a href="/code/apunkt/SurfSense/src/commit/4ac994792bf3e68a4b8e3553db18661bf9879518/.cursor/skills/playwright-testing/auth-flows.md">core/auth-flows.md</a></p> <hr> <h2 id="user-content-quick-diagnostic-checklist" dir="auto">Quick Diagnostic Checklist</h2> <p dir="auto">When you hit an error not listed above, run through this checklist:</p> <ol dir="auto"> <li><strong>Read the full error</strong> — Playwright errors include the locator, action, and a snippet of the page state. The answer is often in the details.</li> <li><strong>Enable traces</strong> — Add <code>trace: 'on'</code> temporarily or <code>trace: 'on-first-retry'</code> in config. Run <code>npx playwright show-trace trace.zip</code> to see screenshots, DOM, network, and console at the point of failure.</li> <li><strong>Use the Playwright Inspector</strong> — Run <code>npx playwright test --debug</code> to step through the test interactively.</li> <li><strong>Check the locator</strong> — Run <code>npx playwright codegen <url></code> to verify locators against the live page.</li> <li><strong>Check the Playwright version</strong> — Run <code>npx playwright --version</code>. Ensure <code>@playwright/test</code> and browser binaries are the same version.</li> <li><strong>Search the Playwright issue tracker</strong> — Many errors have known solutions in <a href="<a href="https://github.com/microsoft/playwright/issues" class="link">https://github.com/microsoft/playwright/issues</a>">github.com/microsoft/playwright/issues</a>.</li> </ol> </body></html>