Files
hanzo.ai/e2e/unit/head.spec.ts
zeekay 1830fcf955 Dev and the export are built by Turbopack, whose compile takes 4 s warm and 30 s cold on six cores where webpack's took 79 s and 3 minutes
next dev and next build run Turbopack, Next 16's default, and the
webpack() hook and --webpack flags are gone. react-native is
react-native-web by turbopack.resolveAlias, and .web files resolve first
by turbopack.resolveExtensions, which Turbopack applies to the project
and transpilePackages only: react-native-svg joins that list, or its
native build is bundled (Flow source, TurboModuleRegistry) and the build
fails.

The export has the same 1,020 pages and the same 1,463 other files byte
for byte. With chunk names set aside, 973 pages are identical and the
rest differ as two webpack builds differ from each other (gui style
order, a race in the code samples' colors), except /benchmarks, whose
bars move by 1e-14 % because Turbopack parses a JSON float one bit off.
Every head gains a same-origin preconnect and next-size-adjust.

The font preload reads Turbopack's asset hash, the stale-chunk reload
keys on ChunkLoadError, which Turbopack names every failed chunk, and
the deploy carries turbopack/ in its cache in place of webpack/.
2026-10-18 16:25:37 -07:00

89 lines
3.8 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 { 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 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)
})