mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
97 lines
4.3 KiB
TypeScript
97 lines
4.3 KiB
TypeScript
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)
|
||
})
|