.agents/skills/playwright-best-practices/debugging/console-errors.md
test('capture console logs', async ({page}) => {
const logs: string[] = []
page.on('console', (msg) => {
logs.push(`${msg.type()}: ${msg.text()}`)
})
await page.goto('/')
// Check what was logged
console.log('Captured logs:', logs)
})
test('capture specific console types', async ({page}) => {
const errors: string[] = []
const warnings: string[] = []
const infos: string[] = []
page.on('console', (msg) => {
switch (msg.type()) {
case 'error':
errors.push(msg.text())
break
case 'warning':
warnings.push(msg.text())
break
case 'info':
case 'log':
infos.push(msg.text())
break
}
})
await page.goto('/dashboard')
expect(errors).toHaveLength(0)
console.log('Warnings:', warnings)
})
test('capture errors with location', async ({page}) => {
const errors: {message: string; location?: string}[] = []
page.on('console', async (msg) => {
if (msg.type() === 'error') {
const location = msg.location()
errors.push({
message: msg.text(),
location: location ? `${location.url}:${location.lineNumber}` : undefined,
})
}
})
await page.goto('/buggy-page')
// Log errors with source location
errors.forEach((e) => {
console.log(`Error: ${e.message}`)
if (e.location) console.log(` at ${e.location}`)
})
})
test('no console errors allowed', async ({page}) => {
const errors: string[] = []
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text())
}
})
await page.goto('/')
await page.getByRole('button', {name: 'Load Data'}).click()
// Fail if any console errors
expect(errors, `Console errors found:\n${errors.join('\n')}`).toHaveLength(0)
})
test('no unexpected console errors', async ({page}) => {
const allowedErrors = [/Failed to load resource.*favicon/, /ResizeObserver loop/]
const unexpectedErrors: string[] = []
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text()
const isAllowed = allowedErrors.some((pattern) => pattern.test(text))
if (!isAllowed) {
unexpectedErrors.push(text)
}
}
})
await page.goto('/')
expect(
unexpectedErrors,
`Unexpected console errors:\n${unexpectedErrors.join('\n')}`,
).toHaveLength(0)
})
// fixtures/console.fixture.ts
type ConsoleFixtures = {
failOnConsoleError: void
}
export const test = base.extend<ConsoleFixtures>({
failOnConsoleError: [
async ({page}, use, testInfo) => {
const errors: string[] = []
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text())
}
})
await use()
// After test, check for errors
if (errors.length > 0) {
testInfo.annotations.push({
type: 'console-errors',
description: errors.join('\n'),
})
throw new Error(`Console errors detected:\n${errors.join('\n')}`)
}
},
{auto: true}, // Runs for every test
],
})
test('no uncaught exceptions', async ({page}) => {
const pageErrors: Error[] = []
page.on('pageerror', (error) => {
pageErrors.push(error)
})
await page.goto('/')
await page.getByRole('button', {name: 'Trigger Action'}).click()
expect(
pageErrors,
`Uncaught exceptions:\n${pageErrors.map((e) => e.message).join('\n')}`,
).toHaveLength(0)
})
test('capture JS error details', async ({page}) => {
const errors: {message: string; stack?: string}[] = []
page.on('pageerror', (error) => {
errors.push({
message: error.message,
stack: error.stack,
})
})
await page.goto('/error-page')
if (errors.length > 0) {
console.log('JavaScript errors:')
errors.forEach((e) => {
console.log(` Message: ${e.message}`)
console.log(` Stack: ${e.stack}`)
})
}
})
test('error boundary catches render error', async ({page}) => {
let errorCaught = false
page.on('pageerror', () => {
// Note: React error boundaries catch errors before they become pageerrors
// This would only fire for unhandled errors
errorCaught = true
})
// Trigger component error via props
await page.route(
'**/api/data',
(route) => route.fulfill({json: null}), // Will cause "cannot read property of null"
)
await page.goto('/dashboard')
// Error boundary should show fallback, not crash
await expect(page.getByText('Something went wrong')).toBeVisible()
expect(errorCaught).toBe(false) // Error was caught by boundary
})
test('no deprecation warnings', async ({page}) => {
const deprecations: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (msg.type() === 'warning' && (text.includes('deprecated') || text.includes('Deprecation'))) {
deprecations.push(text)
}
})
await page.goto('/')
if (deprecations.length > 0) {
console.warn('Deprecation warnings found:')
deprecations.forEach((d) => console.warn(` - ${d}`))
}
// Optionally fail
// expect(deprecations).toHaveLength(0);
})
test('no React warnings', async ({page}) => {
const reactWarnings: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (msg.type() === 'warning' && (text.includes('Warning:') || text.includes('React'))) {
reactWarnings.push(text)
}
})
await page.goto('/')
// Common React warnings to check
const criticalWarnings = reactWarnings.filter(
(w) =>
w.includes('Each child in a list should have a unique') ||
w.includes('Cannot update a component') ||
w.includes("Can't perform a React state update"),
)
expect(criticalWarnings, `React warnings:\n${criticalWarnings.join('\n')}`).toHaveLength(0)
})
// fixtures/console.fixture.ts
type ConsoleMessage = {
type: string
text: string
location?: {url: string; line: number}
timestamp: number
}
type ConsoleFixtures = {
consoleMessages: ConsoleMessage[]
getConsoleErrors: () => ConsoleMessage[]
getConsoleWarnings: () => ConsoleMessage[]
assertNoErrors: (allowedPatterns?: RegExp[]) => void
}
export const test = base.extend<ConsoleFixtures>({
consoleMessages: async ({page}, use) => {
const messages: ConsoleMessage[] = []
page.on('console', (msg) => {
const location = msg.location()
messages.push({
type: msg.type(),
text: msg.text(),
location: location ? {url: location.url, line: location.lineNumber} : undefined,
timestamp: Date.now(),
})
})
await use(messages)
},
getConsoleErrors: async ({consoleMessages}, use) => {
await use(() => consoleMessages.filter((m) => m.type === 'error'))
},
getConsoleWarnings: async ({consoleMessages}, use) => {
await use(() => consoleMessages.filter((m) => m.type === 'warning'))
},
assertNoErrors: async ({getConsoleErrors}, use) => {
await use((allowedPatterns = []) => {
const errors = getConsoleErrors()
const unexpected = errors.filter((e) => !allowedPatterns.some((p) => p.test(e.text)))
if (unexpected.length > 0) {
throw new Error(`Unexpected console errors:\n${unexpected.map((e) => e.text).join('\n')}`)
}
})
},
})
// Usage
test('page loads without errors', async ({page, assertNoErrors}) => {
await page.goto('/dashboard')
await page.getByRole('button', {name: 'Load'}).click()
assertNoErrors([/favicon/]) // Allow favicon errors
})
test('capture console for debugging', async ({page}, testInfo) => {
const logs: string[] = []
page.on('console', (msg) => {
logs.push(`[${msg.type()}] ${msg.text()}`)
})
page.on('pageerror', (error) => {
logs.push(`[EXCEPTION] ${error.message}`)
})
await page.goto('/')
// ... test actions
// Attach console log to test report
await testInfo.attach('console-log', {
body: logs.join('\n'),
contentType: 'text/plain',
})
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Ignoring console errors | Bugs go unnoticed | Check for errors in tests |
| Too strict error checking | Tests fail on minor issues | Allow known/expected errors |
| Not capturing stack traces | Hard to debug | Include location info |
| Checking only at end | Miss errors during actions | Capture continuously |