Back to Sanity

Browser Console & JavaScript Error Handling

.agents/skills/playwright-best-practices/debugging/console-errors.md

5.24.09.5 KB
Original Source

Browser Console & JavaScript Error Handling

Table of Contents

  1. Capturing Console Messages
  2. Failing on Console Errors
  3. JavaScript Error Detection
  4. Monitoring Warnings
  5. Console Fixtures

Capturing Console Messages

Basic Console Capture

typescript
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)
})

Capture by Type

typescript
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)
})

Capture with Stack Trace

typescript
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}`)
  })
})

Failing on Console Errors

Fail Test on Any Error

typescript
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)
})

Fail with Allowed Exceptions

typescript
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)
})

Auto-Fail Fixture

typescript
// 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
  ],
})

JavaScript Error Detection

Catch Uncaught Exceptions

typescript
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)
})

Capture Error Details

typescript
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 Triggers

typescript
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
})

Monitoring Warnings

Capture Deprecation Warnings

typescript
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);
})

React Development Warnings

typescript
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)
})

Console Fixtures

Comprehensive Console Fixture

typescript
// 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
})

Attach Console to Report

typescript
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-Patterns to Avoid

Anti-PatternProblemSolution
Ignoring console errorsBugs go unnoticedCheck for errors in tests
Too strict error checkingTests fail on minor issuesAllow known/expected errors
Not capturing stack tracesHard to debugInclude location info
Checking only at endMiss errors during actionsCapture continuously