SurfSense/.cursor/skills/playwright-testing/error-index.md

1904 lines
58 KiB
Markdown
Raw Normal View History

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