Files

97 lines
4.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { isStaleChunk, ONE_SLASH, RELOADED, STALE } from '../../lib/head'
import { noise } from '../../lib/analytics/noise'
/**
* WHAT THE HEAD SETTLES BEFORE ANY MODULE RUNS, run against a stand-in window:
* the address has one slash at the front, and a page from before a deploy
* reloads once when a chunk it names is gone, and not again.
*/
function head(script: string, pathname = '/', search = '', hash = '') {
const writes: string[] = []
const listeners: Record<string, ((e: unknown) => void)[]> = {}
const stored: Record<string, string> = {}
const win: Record<string, unknown> = {}
let reloads = 0
const location = { pathname, search, hash, reload: () => reloads++ }
const history = { replaceState: (_s: unknown, _t: string, url: string) => writes.push(url) }
const sessionStorage = { getItem: (k: string) => stored[k] ?? null, setItem: (k: string, v: string) => (stored[k] = v) }
const addEventListener = (type: string, fn: (e: unknown) => void) => (listeners[type] ??= []).push(fn)
new Function('location', 'history', 'sessionStorage', 'addEventListener', 'window', script)(
location,
history,
sessionStorage,
addEventListener,
win,
)
return {
writes,
stored,
win,
reloads: () => reloads,
fire: (type: string, e: unknown) => (listeners[type] ?? []).forEach((fn) => fn(e)),
}
}
test('a doubled slash at the front of the address becomes one, keeping the query and fragment', () => {
expect(head(ONE_SLASH, '//models/openai/gpt-5-mini', '?q=1', '#a').writes).toEqual(['/models/openai/gpt-5-mini?q=1#a'])
expect(head(ONE_SLASH, '///pricing').writes).toEqual(['/pricing'])
expect(head(ONE_SLASH, '/models/zen').writes, 'an ordinary address is left alone').toEqual([])
expect(head(ONE_SLASH, '/a//b').writes, 'only the front is the host-shaped part').toEqual([])
})
test('a chunk that is gone reloads the page once, and a second failure reports instead', () => {
const failed = (file: string) =>
Object.assign(new Error(`Failed to load chunk /_next/static/chunks/${file} from module 83283`), { name: 'ChunkLoadError' })
const chunk = failed('0abc123xyz.js')
const page = head(STALE)
page.fire('unhandledrejection', { reason: chunk })
expect(page.reloads()).toBe(1)
expect(page.win.hzStale, 'the reporter is told the failure is being recovered').toBe(true)
expect(page.stored[RELOADED]).toBeTruthy()
// The reloaded page fails the same way: inside the minute, it does not reload again.
const again = head(STALE)
Object.assign(again.stored, page.stored)
again.fire('error', { error: chunk })
expect(again.reloads()).toBe(0)
expect(again.win.hzStale).toBeUndefined()
const css = head(STALE)
css.fire('error', { error: failed('0def456uvw.css') })
expect(css.reloads(), 'a stylesheet chunk is the same failure').toBe(1)
const esm = head(STALE)
esm.fire('unhandledrejection', { reason: new TypeError('Failed to fetch dynamically imported module: https://hanzo.ai/_next/static/chunks/abc.js') })
expect(esm.reloads(), 'a native ESM dynamic import failure is the same stale chunk').toBe(1)
expect(isStaleChunk(chunk)).toBe(true)
expect(isStaleChunk(new TypeError('Importing a module script failed.'))).toBe(true)
expect(isStaleChunk(new TypeError('x is undefined'))).toBe(false)
const other = head(STALE)
other.fire('error', { error: new TypeError('x is undefined') })
other.fire('unhandledrejection', { reason: undefined })
expect(other.reloads(), 'any other error is not a stale page').toBe(0)
})
test('what is not this site’s error is not reported, and the rest is', () => {
for (const err of [
'Object Not Found Matching Id:1, MethodName:update, ParamCount:4',
new Error('Object Not Found Matching Id:2, MethodName:update, ParamCount:4'),
'Script error.',
new TypeError("undefined is not an object (evaluating 'window.ethereum.selectedAddress = undefined')"),
'ResizeObserver loop completed with undelivered notifications.',
'ResizeObserver loop limit exceeded',
])
expect(noise(err), String(err)).toBe(true)
for (const err of [
new Error('Minified React error #185'),
new TypeError('Cannot read properties of null (reading "input")'),
"Script error in /chat: the reply did not parse",
undefined,
])
expect(noise(err), String(err)).toBe(false)
})