9.3 KiB
9.3 KiB
Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
Test info
- Name: all-routes.spec.ts >> route coverage >> home
- Location: e2e/all-routes.spec.ts:135:5
Error details
TimeoutError: page.goto: Timeout 10000ms exceeded.
Call log:
- navigating to "http://localhost:5802/?skip=welcome", waiting until "networkidle"
Test source
38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' },
39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' },
40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' },
41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' },
42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' },
43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' },
44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' },
45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' },
46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' },
47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' },
48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' },
49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' },
50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' },
51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' },
52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' },
53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' },
54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' },
55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' },
56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' },
57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' },
58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' },
59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' },
60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' },
61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' },
62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' },
63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' },
64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' },
65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' },
66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' },
67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' },
68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' },
69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' },
70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' },
71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' },
72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' },
73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' },
74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' },
75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' },
76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' },
77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' },
78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' },
79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' },
80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' },
81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' },
82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' },
83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' },
84 | ] as const satisfies readonly RouteSpec[]
85 |
86 | // Routes that trigger WASM compile + world generation. Keep the budget
87 | // generous but bounded so a hung route fails loudly instead of stalling.
88 | const SLOW_ROUTES = [
89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' },
90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' },
91 | ] as const satisfies readonly RouteSpec[]
92 |
93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few
94 | // dev-only warnings surface at `error` level but are not runtime failures.
95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [
96 | /Download the React DevTools/,
97 | ]
98 |
99 | interface ConsoleErrorCapture {
100 | readonly pageErrors: string[]
101 | readonly consoleErrors: string[]
102 | }
103 |
104 | function attachErrorCapture(page: Page): ConsoleErrorCapture {
105 | const pageErrors: string[] = []
106 | const consoleErrors: string[] = []
107 |
108 | page.on('pageerror', (err: Error) => {
109 | pageErrors.push(`${err.name}: ${err.message}`)
110 | })
111 |
112 | page.on('console', (msg: ConsoleMessage) => {
113 | if (msg.type() !== 'error') return
114 | const text = msg.text()
115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return
116 | consoleErrors.push(text)
117 | })
118 |
119 | return { pageErrors, consoleErrors }
120 | }
121 |
122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void {
123 | const label = `${spec.path} (${spec.label})`
124 | const msgs = [
125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`),
126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`),
127 | ]
128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0)
129 | }
130 |
131 | test.describe('route coverage', () => {
132 | test.describe.configure({ mode: 'parallel' })
133 |
134 | for (const spec of FAST_ROUTES) {
135 | test(spec.label, async ({ page }) => {
136 | const cap = attachErrorCapture(page)
137 | const url = `${spec.path}?skip=welcome`
> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs })
| ^ TimeoutError: page.goto: Timeout 10000ms exceeded.
139 | // Wait an extra tick for any deferred suspense boundary to resolve.
140 | await page.waitForTimeout(200)
141 | assertClean(spec, cap)
142 | })
143 | }
144 |
145 | for (const spec of SLOW_ROUTES) {
146 | test(spec.label, async ({ page }) => {
147 | const cap = attachErrorCapture(page)
148 | const url = `${spec.path}?skip=welcome`
149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs })
150 | // Slow routes: wait for the canvas OR the route's page-heading to appear.
151 | // Either proves the route mounted; we don't require full sim completion.
152 | await Promise.race([
153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
155 | ])
156 | assertClean(spec, cap)
157 | })
158 | }
159 |
160 | test('unknown route redirects to home without error', async ({ page }) => {
161 | const cap = attachErrorCapture(page)
162 | await page.goto('/this-route-does-not-exist?skip=welcome', {
163 | waitUntil: 'networkidle',
164 | timeout: 10_000,
165 | })
166 | // Router's fallback <Navigate to="/"> should land us at the home URL.
167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/')
168 | assertClean(
169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' },
170 | cap,
171 | )
172 | })
173 | })
174 |