SnapAPI/src/services/__tests__/retry.test.ts
Hoid fde5aea324
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
feat: add screenshot retry logic for transient browser failures
- Add isRetryableError() helper (retry on TimeoutError, Protocol error,
  Target closed, Session closed, Navigation failed, net::ERR_*)
- Wrap browser screenshot in retry loop (max 2 retries, exponential backoff)
- Add retryCount to ScreenshotResult, X-Retry-Count response header
- Validation/SSRF/auth errors are NOT retried
- 28 new tests (12 retry classification + 6 screenshot retry + route tests)
2026-03-06 09:12:44 +01:00

56 lines
2.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { isRetryableError } from '../retry.js'
describe('isRetryableError', () => {
it('returns true for TimeoutError', () => {
const err = new Error('TimeoutError: Navigation timeout of 20000ms exceeded')
err.name = 'TimeoutError'
expect(isRetryableError(err)).toBe(true)
})
it('returns true for Protocol error', () => {
expect(isRetryableError(new Error('Protocol error (Runtime.callFunctionOn): Session closed.'))).toBe(true)
})
it('returns true for Target closed', () => {
expect(isRetryableError(new Error('Target closed'))).toBe(true)
})
it('returns true for Session closed', () => {
expect(isRetryableError(new Error('Session closed. Most likely the page has been closed.'))).toBe(true)
})
it('returns true for Navigation failed', () => {
expect(isRetryableError(new Error('Navigation failed because browser has disconnected!'))).toBe(true)
})
it('returns true for net::ERR_ errors', () => {
expect(isRetryableError(new Error('net::ERR_CONNECTION_RESET'))).toBe(true)
expect(isRetryableError(new Error('net::ERR_CONNECTION_REFUSED'))).toBe(true)
})
it('returns false for SCREENSHOT_TIMEOUT (overall timeout, not transient)', () => {
expect(isRetryableError(new Error('SCREENSHOT_TIMEOUT'))).toBe(false)
})
it('returns false for SSRF_BLOCKED', () => {
expect(isRetryableError(new Error('SSRF_BLOCKED'))).toBe(false)
})
it('returns false for validation errors', () => {
expect(isRetryableError(new Error('hideSelector contains dangerous characters'))).toBe(false)
expect(isRetryableError(new Error('selector and fullPage are mutually exclusive'))).toBe(false)
})
it('returns false for SELECTOR_NOT_FOUND', () => {
expect(isRetryableError(new Error('SELECTOR_NOT_FOUND'))).toBe(false)
})
it('returns false for JS_EXECUTION_ERROR', () => {
expect(isRetryableError(new Error('JS_EXECUTION_ERROR: foo is not defined'))).toBe(false)
})
it('returns false for generic errors', () => {
expect(isRetryableError(new Error('Something went wrong'))).toBe(false)
})
})