diff --git a/public/games/age-of-dwarves/guide/e2e/simulator.spec.ts b/public/games/age-of-dwarves/guide/e2e/simulator.spec.ts index 30a16aeb..cd051cbb 100644 --- a/public/games/age-of-dwarves/guide/e2e/simulator.spec.ts +++ b/public/games/age-of-dwarves/guide/e2e/simulator.spec.ts @@ -240,7 +240,7 @@ test.describe('Climate simulator', () => { await expect(dropdown).toBeVisible({ timeout: 3_000 }) const options = dropdown.getByRole('option') - await expect(options).toHaveCount(3) + await expect(options).toHaveCount(4) // Close without switching — the dropdown uses a mousedown-outside listener, // so clicking somewhere neutral (the scenario nav) dismisses it. diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json index 235f8294..a0bae229 100644 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json +++ b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json @@ -1,7 +1,6 @@ { "status": "failed", "failedTests": [ - "620c61f0a0b21e7686ea-41dfc9f4053dc733176e", - "620c61f0a0b21e7686ea-00dd36638b59435b947b" + "620c61f0a0b21e7686ea-1ae6cca4a49867df6bb8" ] } \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-51f09-es-do-not-accumulate-errors-chromium/test-failed-1.png b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-51f09-es-do-not-accumulate-errors-chromium/test-failed-1.png deleted file mode 100644 index ece2780b..00000000 Binary files a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-51f09-es-do-not-accumulate-errors-chromium/test-failed-1.png and /dev/null differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-931c4-and-shows-all-three-planets-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-931c4-and-shows-all-three-planets-chromium/error-context.md deleted file mode 100644 index 2b5698b0..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-931c4-and-shows-all-three-planets-chromium/error-context.md +++ /dev/null @@ -1,442 +0,0 @@ -# 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: simulator.spec.ts >> Climate simulator >> planet selector opens and shows all three planets -- Location: e2e/simulator.spec.ts:231:3 - -# Error details - -``` -Error: expect(locator).toHaveCount(expected) failed - -Locator: getByRole('listbox', { name: 'Select planet' }).getByRole('option') -Expected: 3 -Received: 4 -Timeout: 10000ms - -Call log: - - Expect "toHaveCount" with timeout 10000ms - - waiting for getByRole('listbox', { name: 'Select planet' }).getByRole('option') - 13 × locator resolved to 4 elements - - unexpected value "4" - -``` - -# Page snapshot - -```yaml -- generic [ref=e5]: - - generic [ref=e6]: - - generic [ref=e7]: - - button "◉ Earth ▾" [expanded] [active] [ref=e8] [cursor=pointer]: - - generic [ref=e9]: ◉ - - generic [ref=e10]: Earth - - generic [ref=e11]: ▾ - - listbox "Select planet" [ref=e12]: - - option "◉ Earth" [selected] [ref=e13] [cursor=pointer]: - - generic [ref=e14]: ◉ - - generic [ref=e15]: Earth - - option "⛰ Khazad Prime" [ref=e16] [cursor=pointer]: - - generic [ref=e17]: ⛰ - - generic [ref=e18]: Khazad Prime - - option "◎ Mars" [ref=e19] [cursor=pointer]: - - generic [ref=e20]: ◎ - - generic [ref=e21]: Mars - - option "◈ Venus" [ref=e22] [cursor=pointer]: - - generic [ref=e23]: ◈ - - generic [ref=e24]: Venus - - tablist "Simulation category" [ref=e26]: - - tab "Environment" [selected] [ref=e27] [cursor=pointer] - - tab "Life" [ref=e28] [cursor=pointer] - - navigation "Climate scenario groups" [ref=e29]: - - button "Baseline Scenarios Baseline" [ref=e31] [cursor=pointer]: - - generic [ref=e33]: - - generic [ref=e34]: Baseline Scenarios - - generic [ref=e35]: Baseline - - generic [ref=e36]: ▾ - - button "Eco Disaster" [ref=e38] [cursor=pointer]: - - generic [ref=e41]: Eco Disaster - - generic [ref=e42]: ▾ - - button "ET Disaster" [ref=e44] [cursor=pointer]: - - generic [ref=e47]: ET Disaster - - generic [ref=e48]: ▾ - - button "Lifeless Worlds" [ref=e50] [cursor=pointer]: - - generic [ref=e53]: Lifeless Worlds - - generic [ref=e54]: ▾ - - paragraph [ref=e56]: Reference climate scenario — pure solar forcing, no overrides. - - generic [ref=e57]: - - generic "Climate simulation hex map" [ref=e58] - - generic [ref=e60]: - - button "Layers" [ref=e61] [cursor=pointer]: - - generic: Layers - - generic [ref=e62]: - - generic [ref=e63]: - - checkbox "🗺 Terrain" [checked] [ref=e64] [cursor=pointer]: - - generic [ref=e65]: 🗺 - - generic: Terrain - - checkbox "🌡 Temperature" [ref=e67] [cursor=pointer]: - - generic [ref=e68]: 🌡 - - generic: Temperature - - checkbox "💧 Moisture" [ref=e70] [cursor=pointer]: - - generic [ref=e71]: 💧 - - generic: Moisture - - checkbox "💨 Wind" [ref=e73] [cursor=pointer]: - - generic [ref=e74]: 💨 - - generic: Wind - - checkbox "⬡ Pressure" [ref=e76] [cursor=pointer]: - - generic [ref=e77]: ⬡ - - generic: Pressure - - checkbox "🌊 Rivers" [ref=e79] [cursor=pointer]: - - generic [ref=e80]: 🌊 - - generic: Rivers - - checkbox "Δ Delta" [ref=e82] [cursor=pointer]: - - generic [ref=e83]: Δ - - generic: Delta - - generic: View Center - - generic [ref=e86]: - - radio "🌍 Equator" [checked] [ref=e87] [cursor=pointer]: - - generic [ref=e88]: 🌍 - - generic: Equator - - radio "⬆ N Pole" [ref=e90] [cursor=pointer]: - - generic [ref=e91]: ⬆ - - generic: N Pole - - radio "⬇ S Pole" [ref=e93] [cursor=pointer]: - - generic [ref=e94]: ⬇ - - generic: S Pole - - button "Legend Open biome reference" [ref=e98] [cursor=pointer]: - - generic: Legend - - button "Open biome reference" [ref=e100]: ⓘ - - generic [ref=e102]: - - button "Play simulation" [ref=e103] [cursor=pointer]: ▶ - - button "1x" [ref=e104] [cursor=pointer] - - generic: - - text: "50" - - generic [ref=e105]: /50 - - button "Enable loop" [ref=e106] [cursor=pointer]: ↻ - - generic "Drag to resize map · Double-click to reset" [ref=e107] - - generic [ref=e109]: - - button "Events ▸" [ref=e111] [cursor=pointer]: - - generic [ref=e113]: Events - - generic [ref=e114]: ▸ - - generic [ref=e115]: - - heading "Event Console" [level=3] [ref=e116] - - generic [ref=e117]: - - generic [ref=e118]: - - generic [ref=e120]: Asteroid Impact - - combobox [ref=e121] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - option "T6 — Continental" - - option "T7 — Sub-Global" - - option "T8 — Global" - - option "T9 — Extinction-Level" - - option "T10 — Doomsday" - - button "Fire" [ref=e122] [cursor=pointer] - - generic [ref=e123]: - - generic [ref=e125]: Solar Event - - combobox [ref=e126] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - option "T6 — Continental" - - option "T7 — Sub-Global" - - option "T8 — Global" - - option "T9 — Extinction-Level" - - option "T10 — Doomsday" - - button "Fire" [ref=e127] [cursor=pointer] - - generic [ref=e128]: - - generic [ref=e130]: Volcanic - - combobox [ref=e131] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - option "T6 — Continental" - - option "T7 — Sub-Global" - - option "T8 — Global" - - option "T9 — Extinction-Level" - - option "T10 — Doomsday" - - button "Fire" [ref=e132] [cursor=pointer] - - generic [ref=e133]: - - generic [ref=e135]: Marine - - combobox [ref=e136] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - option "T6 — Continental" - - option "T7 — Sub-Global" - - option "T8 — Global" - - option "T9 — Extinction-Level" - - option "T10 — Doomsday" - - button "Fire" [ref=e137] [cursor=pointer] - - generic [ref=e138]: - - generic [ref=e140]: Ecological - - combobox [ref=e141] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - option "T6 — Continental" - - option "T7 — Sub-Global" - - option "T8 — Global" - - option "T9 — Extinction-Level" - - option "T10 — Doomsday" - - button "Fire" [ref=e142] [cursor=pointer] - - generic [ref=e143]: - - generic [ref=e145]: Atmospheric - - combobox [ref=e146] [cursor=pointer]: - - option "T7 — Sub-Global" [selected] - - option "T8 — Global" - - option "T9 — Extinction-Level" - - option "T10 — Doomsday" - - button "Fire" [ref=e147] [cursor=pointer] - - generic [ref=e148]: - - generic [ref=e150]: Wildfire - - combobox [ref=e151] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - button "Fire" [ref=e152] [cursor=pointer] - - generic [ref=e153]: - - generic [ref=e155]: Plague - - combobox [ref=e156] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - button "Fire" [ref=e157] [cursor=pointer] - - generic [ref=e158]: - - generic [ref=e160]: Weather - - combobox [ref=e161] [cursor=pointer]: - - option "T1 — Minor" [selected] - - option "T2 — Moderate" - - option "T3 — Significant" - - option "T4 — Major" - - option "T5 — Severe" - - button "Fire" [ref=e162] [cursor=pointer] - - paragraph [ref=e163]: Events are injected at the current simulation end, then propagated 5 turns. -``` - -# Test source - -```ts - 143 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 144 | - 145 | const controlBtn = sharedPage.getByRole('button', { name: /pause simulation|play simulation/i }) - 146 | await expect(controlBtn).toBeEnabled() - 147 | - 148 | await terrainRow.click() - 149 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 150 | await expect(controlBtn).toBeEnabled() - 151 | - 152 | expect(consoleErrors, `Errors after terrain toggle:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 153 | }) - 154 | - 155 | test('multiple layer toggles do not accumulate errors', async () => { - 156 | const layers = ['Temperature', 'Moisture', 'Wind', 'Rivers'] - 157 | - 158 | for (const label of layers) { - 159 | const row = sharedPage.getByRole('checkbox').filter({ hasText: label }) - 160 | if (await row.isVisible()) { - 161 | // force: true bypasses pointer-event interception from floating chart canvases - 162 | await row.click({ force: true }) - 163 | await row.click({ force: true }) - 164 | } - 165 | } - 166 | - 167 | expect(consoleErrors, `Errors after layer toggles:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 168 | }) - 169 | - 170 | test('layers panel collapses and expands', async () => { - 171 | // LayerPanel wraps in MapOverlayPanel(title="Layers", defaultExpanded) - 172 | // MapOverlayPanel header includes the ▸ chevron char in text content, use substring - 173 | const panelHeader = sharedPage.getByRole('button').filter({ hasText: 'Layers' }) - 174 | await expect(panelHeader).toBeVisible({ timeout: 5_000 }) - 175 | - 176 | // Currently expanded (defaultExpanded=true) — collapse it - 177 | const terrainRow = sharedPage.getByRole('checkbox').filter({ hasText: 'Terrain' }) - 178 | await expect(terrainRow).toBeVisible() - 179 | await panelHeader.click() - 180 | await expect(terrainRow).not.toBeVisible({ timeout: 3_000 }) - 181 | - 182 | // Re-expand - 183 | await panelHeader.click() - 184 | await expect(terrainRow).toBeVisible({ timeout: 3_000 }) - 185 | - 186 | expect(consoleErrors, `Errors after panel collapse:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 187 | }) - 188 | - 189 | test('view center: N Pole and S Pole switch without errors', async () => { - 190 | const canvas = sharedPage.locator('canvas').first() - 191 | - 192 | const nPoleBtn = sharedPage.getByRole('radio').filter({ hasText: 'N Pole' }) - 193 | await expect(nPoleBtn).toBeVisible({ timeout: 5_000 }) - 194 | - 195 | // force: true bypasses pointer-event interception from the MoveableChart canvas - 196 | await nPoleBtn.click({ force: true }) - 197 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 198 | - 199 | const sPoleBtn = sharedPage.getByRole('radio').filter({ hasText: 'S Pole' }) - 200 | await sPoleBtn.click({ force: true }) - 201 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 202 | - 203 | const equatorBtn = sharedPage.getByRole('radio').filter({ hasText: 'Equator' }) - 204 | await equatorBtn.click({ force: true }) - 205 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 206 | - 207 | expect(consoleErrors, `Errors after view switch:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 208 | }) - 209 | - 210 | // ── Category tabs ───────────────────────────────────────────────────────── - 211 | - 212 | test('Environment / Life category tabs switch layer presets', async () => { - 213 | const tabList = sharedPage.getByRole('tablist', { name: 'Simulation category' }) - 214 | await expect(tabList).toBeVisible({ timeout: 5_000 }) - 215 | - 216 | const lifeTab = tabList.getByRole('tab', { name: 'Life' }) - 217 | await lifeTab.click() - 218 | // Life category enables Canopy layer (bit 9) - 219 | await expect(sharedPage.getByRole('checkbox').filter({ hasText: 'Canopy' })).toBeVisible({ timeout: 3_000 }) - 220 | - 221 | // Switch back — Environment only shows climate layers - 222 | const envTab = tabList.getByRole('tab', { name: 'Environment' }) - 223 | await envTab.click() - 224 | await expect(sharedPage.getByRole('checkbox').filter({ hasText: 'Terrain' })).toBeVisible({ timeout: 3_000 }) - 225 | - 226 | expect(consoleErrors, `Errors after category switch:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 227 | }) - 228 | - 229 | // ── Planet selector ──────────────────────────────────────────────────────── - 230 | - 231 | test('planet selector opens and shows all three planets', async () => { - 232 | // Planet button is the first button[aria-haspopup="listbox"] in the DOM — - 233 | // before the ScenarioTabs group buttons which also carry the same attribute. - 234 | const planetBtn = sharedPage.locator('button[aria-haspopup="listbox"]').first() - 235 | await expect(planetBtn).toBeVisible({ timeout: 5_000 }) - 236 | - 237 | await planetBtn.click() - 238 | - 239 | const dropdown = sharedPage.getByRole('listbox', { name: 'Select planet' }) - 240 | await expect(dropdown).toBeVisible({ timeout: 3_000 }) - 241 | - 242 | const options = dropdown.getByRole('option') -> 243 | await expect(options).toHaveCount(3) - | ^ Error: expect(locator).toHaveCount(expected) failed - 244 | - 245 | // Close without switching — the dropdown uses a mousedown-outside listener, - 246 | // so clicking somewhere neutral (the scenario nav) dismisses it. - 247 | await sharedPage.getByRole('navigation', { name: 'Climate scenario groups' }).click() - 248 | await expect(dropdown).not.toBeVisible({ timeout: 3_000 }) - 249 | - 250 | expect(consoleErrors, `Errors after planet dropdown:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 251 | }) - 252 | - 253 | // ── Scenario navigation ──────────────────────────────────────────────────── - 254 | - 255 | test('scenario group nav is present with multiple groups', async () => { - 256 | const nav = sharedPage.getByRole('navigation', { name: 'Climate scenario groups' }) - 257 | await expect(nav).toBeVisible({ timeout: 5_000 }) - 258 | - 259 | const groupButtons = nav.getByRole('button') - 260 | const count = await groupButtons.count() - 261 | expect(count).toBeGreaterThanOrEqual(2) - 262 | }) - 263 | - 264 | test('opening a scenario group dropdown shows options', async () => { - 265 | const nav = sharedPage.getByRole('navigation', { name: 'Climate scenario groups' }) - 266 | const firstGroupBtn = nav.getByRole('button').first() - 267 | - 268 | await firstGroupBtn.click() - 269 | - 270 | const dropdown = sharedPage.getByRole('listbox').first() - 271 | await expect(dropdown).toBeVisible({ timeout: 3_000 }) - 272 | - 273 | const options = dropdown.getByRole('option') - 274 | const count = await options.count() - 275 | expect(count).toBeGreaterThanOrEqual(1) - 276 | - 277 | await sharedPage.click('canvas') - 278 | }) - 279 | - 280 | // ── MoveableChart (terrain composition chart) ──────────────────────────── - 281 | - 282 | test('moveable chart Biomes / Terrain / Elevation tabs all render without errors', async () => { - 283 | const biomesTab = sharedPage.getByRole('button').filter({ hasText: /^Biomes$/ }) - 284 | const terrainTab = sharedPage.getByRole('button').filter({ hasText: /^Terrain$/ }) - 285 | const elevationTab = sharedPage.getByRole('button').filter({ hasText: /^Elevation$/ }) - 286 | - 287 | await expect(biomesTab).toBeVisible({ timeout: 5_000 }) - 288 | await expect(terrainTab).toBeVisible() - 289 | await expect(elevationTab).toBeVisible() - 290 | - 291 | await terrainTab.click({ force: true }) - 292 | await expect(terrainTab).toBeVisible() - 293 | - 294 | await elevationTab.click({ force: true }) - 295 | await expect(elevationTab).toBeVisible() - 296 | - 297 | await biomesTab.click({ force: true }) - 298 | - 299 | expect(consoleErrors, `Errors after chart tab switch:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 300 | }) - 301 | - 302 | // ── Legend panel ────────────────────────────────────────────────────────── - 303 | - 304 | test('legend panel is visible with terrain layer active', async () => { - 305 | // TerrainLegend renders MapOverlayPanel(title="Legend") when terrain (bit 5) is on - 306 | // MapOverlayPanel header includes the ▸ chevron — use substring match - 307 | const legendHeader = sharedPage.getByRole('button').filter({ hasText: 'Legend' }) - 308 | await expect(legendHeader).toBeVisible({ timeout: 5_000 }) - 309 | }) - 310 | - 311 | // ── Stats dashboard ──────────────────────────────────────────────────────── - 312 | - 313 | test('stats dashboard scrubber is present and interactive', async () => { - 314 | const scrubber = sharedPage.locator('input[type="range"]') - 315 | await expect(scrubber).toBeVisible({ timeout: 5_000 }) - 316 | await expect(scrubber).toBeEnabled() - 317 | }) - 318 | - 319 | test('stats dashboard Extend button is present and enabled', async () => { - 320 | const extendBtn = sharedPage.locator('button[title^="Extend by"]') - 321 | await expect(extendBtn).toBeVisible({ timeout: 5_000 }) - 322 | // isSimulating=false after run completes, so the button is enabled - 323 | await expect(extendBtn).toBeEnabled() - 324 | }) - 325 | - 326 | // ── Server mode ──────────────────────────────────────────────────────────── - 327 | - 328 | test('server mode: status endpoint responds when sim-cache server is running', async () => { - 329 | // This test is informational — it probes the status endpoint to detect - 330 | // whether the dev server's sim-cache plugin is running. If not available - 331 | // (e.g. in offline CI) the test is skipped rather than failed. - 332 | const res = await sharedPage.request.get('/__sim-cache/base_no_magic/status?seed=42&turns=2000') - 333 | .catch(() => null) - 334 | - 335 | if (!res || !res.ok()) { - 336 | // Sim-cache server not available; worker mode is in use — nothing to assert. - 337 | return - 338 | } - 339 | - 340 | const body = await res.json() as { ready: boolean; totalTurns: number } - 341 | expect(typeof body.ready).toBe('boolean') - 342 | expect(typeof body.totalTurns).toBe('number') - 343 | expect(body.totalTurns).toBeGreaterThan(0) -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-931c4-and-shows-all-three-planets-chromium/test-failed-1.png b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-931c4-and-shows-all-three-planets-chromium/test-failed-1.png deleted file mode 100644 index e97d2ad2..00000000 Binary files a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-931c4-and-shows-all-three-planets-chromium/test-failed-1.png and /dev/null differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-51f09-es-do-not-accumulate-errors-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-a095c-le-with-non-zero-dimensions-chromium/error-context.md similarity index 68% rename from public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-51f09-es-do-not-accumulate-errors-chromium/error-context.md rename to public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-a095c-le-with-non-zero-dimensions-chromium/error-context.md index 94921652..3ba8df72 100644 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-51f09-es-do-not-accumulate-errors-chromium/error-context.md +++ b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-a095c-le-with-non-zero-dimensions-chromium/error-context.md @@ -6,31 +6,16 @@ # Test info -- Name: simulator.spec.ts >> Climate simulator >> multiple layer toggles do not accumulate errors -- Location: e2e/simulator.spec.ts:155:3 +- Name: simulator.spec.ts >> Climate simulator >> canvas is visible with non-zero dimensions +- Location: e2e/simulator.spec.ts:60:3 # Error details ``` -Error: Errors after layer toggles: -[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error) -[console.error] [SimCache] prefetch turn 50 failed: frame HTTP 500 for base_no_magic:50 -[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error) -[console.error] [SimCache] prefetch turn 51 failed: frame HTTP 500 for base_no_magic:51 -[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error) -[console.error] [SimCache] prefetch turn 50 failed: frame HTTP 500 for base_no_magic:50 -[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error) -[console.error] [SimCache] prefetch turn 50 failed: frame HTTP 500 for base_no_magic:50 -[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error) -[console.error] [SimCache] prefetch turn 52 failed: frame HTTP 500 for base_no_magic:52 -[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error) -[console.error] [SimCache] prefetch turn 51 failed: frame HTTP 500 for base_no_magic:51 +Error: expect(received).toBeGreaterThan(expected) -expect(received).toHaveLength(expected) - -Expected length: 0 -Received length: 12 -Received array: ["[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)", "[console.error] [SimCache] prefetch turn 50 failed: frame HTTP 500 for base_no_magic:50", "[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)", "[console.error] [SimCache] prefetch turn 51 failed: frame HTTP 500 for base_no_magic:51", "[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)", "[console.error] [SimCache] prefetch turn 50 failed: frame HTTP 500 for base_no_magic:50", "[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)", "[console.error] [SimCache] prefetch turn 50 failed: frame HTTP 500 for base_no_magic:50", "[console.error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)", "[console.error] [SimCache] prefetch turn 52 failed: frame HTTP 500 for base_no_magic:52", …] +Expected: > 400 +Received: 200 ``` # Page snapshot @@ -107,7 +92,7 @@ Received array: ["[console.error] Failed to load resource: the server responded - button "Pause simulation" [ref=e90] [cursor=pointer]: ⏸ - button "1x" [ref=e91] [cursor=pointer] - generic: - - text: "34" + - text: "7" - generic [ref=e92]: /50 - button "Enable loop" [ref=e93] [cursor=pointer]: ↻ - generic [ref=e95]: @@ -145,40 +130,40 @@ Received array: ["[console.error] Failed to load resource: the server responded - textbox "Seed digit 6 of 6" [ref=e130]: "2" - button "Random seed" [ref=e131] [cursor=pointer]: ⟳ - button "+500" [ref=e132] [cursor=pointer] - - slider "Turn scrubber" [ref=e134] [cursor=pointer]: "33" + - slider "Turn scrubber" [ref=e134] [cursor=pointer]: "6" - generic [ref=e135]: - generic [ref=e136]: ✦ The Answer - generic [ref=e137]: "\"You already know the question.\"" - - generic [ref=e140]: Temperate + - generic [ref=e140]: Cold - generic [ref=e143]: - generic [ref=e145]: - generic "Average land temperature (0=frozen, 1=scorching). Driven by solar input, albedo, and orbital cycles." [ref=e146]: Temp - - generic [ref=e147]: "0.45" - - generic [ref=e148]: ▲ +0.02 + - generic [ref=e147]: "0.44" + - generic [ref=e148]: ▲ +0.01 - generic [ref=e152]: - generic "Global average wind speed (0=calm, 1=gale). Set by latitude bands + Coriolis at map gen. Dynamic pressure-driven updates (atmosphere.gd) not yet transpiled to TS." [ref=e153]: Wind - generic [ref=e154]: "0.475" - generic [ref=e155]: — 0 - generic [ref=e159]: - generic "Average land moisture (0=bone dry, 1=saturated). Driven by ocean evaporation, wind transport, and precipitation." [ref=e160]: Moisture - - generic [ref=e161]: "0.55" - - generic [ref=e162]: ▼ -0.03 + - generic [ref=e161]: "0.57" + - generic [ref=e162]: ▼ -0.01 - generic [ref=e166]: - generic "Elevation threshold for water. Rises with warming, falls with cooling." [ref=e167]: Sea Lvl - generic [ref=e168]: "0.000" - generic [ref=e169]: — 0 - generic [ref=e173]: - generic "Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it." [ref=e174]: Albedo - - generic [ref=e175]: "0.288" - - generic [ref=e176]: ▼ -0.016 + - generic [ref=e175]: "0.289" + - generic [ref=e176]: ▼ -0.015 - generic [ref=e180]: - generic "Per-turn temperature change. Positive = planet absorbing more heat than it radiates (warming). Negative = cooling." [ref=e181]: Net Energy - - generic [ref=e182]: "-0.0005" + - generic [ref=e182]: "+0.0003" - generic [ref=e183]: — 0 - generic [ref=e187]: - generic "Per-turn moisture change. Positive = planet gaining water (wetting). Negative = losing water (drying)." [ref=e188]: Net Hydro - - generic [ref=e189]: "-0.0000" - - generic [ref=e190]: — 0 + - generic [ref=e189]: "-0.0020" + - generic [ref=e190]: ▼ -0.0020 - generic [ref=e194]: - generic "Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere." [ref=e195]: Aerosol - generic [ref=e196]: "0.0000" @@ -188,44 +173,44 @@ Received array: ["[console.error] Failed to load resource: the server responded - generic [ref=e203]: - generic [ref=e204]: - generic "Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it." [ref=e205]: Albedo - - generic [ref=e208]: "0.288" + - generic [ref=e208]: "0.289" - generic [ref=e209]: - generic "Average absorbed solar energy after albedo reflection. The net heat input driving temperature." [ref=e210]: Solar - - generic [ref=e213]: "0.302" + - generic [ref=e213]: "0.303" - generic [ref=e214]: - generic "Global average wind speed (0=calm, 1=gale). Set by latitude bands + Coriolis at map gen. Dynamic pressure-driven updates (atmosphere.gd) not yet transpiled to TS." [ref=e215]: Wind - generic [ref=e218]: "0.475" - generic [ref=e219]: - generic "Average evapotranspiration across land. Moisture recycled by vegetation per turn." [ref=e220]: ET - - generic [ref=e223]: "0.0003" + - generic [ref=e223]: "0.0001" - generic [ref=e224]: - generic [ref=e225]: - generic "Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere." [ref=e226]: Aerosol - generic [ref=e229]: "0.0000" - generic [ref=e230]: - generic "Per-turn temperature change. Positive = planet absorbing more heat than it radiates (warming). Negative = cooling." [ref=e231]: Net Energy - - generic [ref=e234]: "-0.0005" + - generic [ref=e234]: "+0.0003" - generic [ref=e235]: - generic "Per-turn moisture change. Positive = planet gaining water (wetting). Negative = losing water (drying)." [ref=e236]: Net Hydro - - generic [ref=e239]: "-0.0000" + - generic [ref=e239]: "-0.0020" - generic [ref=e240]: - generic "Average biome quality across land (1-5). Composite of flora health, fauna diversity, biome stability." [ref=e241]: Biome Qlty - - generic [ref=e244]: "2.05" + - generic [ref=e244]: "1.84" - generic [ref=e246]: - generic [ref=e247]: Atmosphere - generic [ref=e248]: - generic [ref=e250]: - 'generic "Atmospheric oxygen fraction as percentage. 21% = modern Earth normal. Below 18%: thin air. Below 10%: life-threatening. Below 5%: lethal." [ref=e251]': O₂ % - - generic [ref=e252]: 34.6% + - generic [ref=e252]: 35.0% - generic [ref=e257]: - 'generic "Atmospheric CO₂ in parts per million (log10 scale). 420 = modern Earth. >1000: unbreathable. >50000: lethal." [ref=e258]': CO₂ ppm - - generic [ref=e259]: 6.0k + - generic [ref=e259]: "320" - generic [ref=e264]: - generic "Atmospheric methane in parts per billion. 1900 = modern Earth. Runaway at >50000 ppb triggers irreversible warming cascade." [ref=e265]: CH₄ ppb - - generic [ref=e266]: 15.3k + - generic [ref=e266]: 1.6k - generic [ref=e271]: - generic "Composite biosphere health index (0-100%). Weighted from O₂ score (35%), fish stock (20%), canopy (20%), fauna habitat (15%), reef health (10%), scaled by ocean state. Below 40% = cascading collapse risk." [ref=e272]: Biosphere - - generic [ref=e273]: 27.0% + - generic [ref=e273]: 45.0% - button "Events ▸" [ref=e278] [cursor=pointer]: - generic [ref=e280]: Events - generic [ref=e281]: ▸ @@ -343,6 +328,73 @@ Received array: ["[console.error] Failed to load resource: the server responded # Test source ```ts + 1 | import { test, expect } from '@playwright/test' + 2 | import type { Page, BrowserContext } from '@playwright/test' + 3 | + 4 | /** + 5 | * Climate simulator E2E tests. + 6 | * + 7 | * All tests share a single browser page to avoid paying the WASM cold-start + 8 | * compilation cost on every test. The beforeAll navigates once and waits for + 9 | * the simulation to complete (canvas visible). Individual tests then assert + 10 | * against the live, running simulation. + 11 | * + 12 | * URL params: + 13 | * ?totalTurns=50 — short run so the wait is bounded + 14 | * &buffer=0 — skip prebuffer, canvas appears as soon as sim finishes + 15 | * &noGui=true — strip the guide layout shell (sidebar, nav) + 16 | * &skip=welcome — skip the race/gender welcome modal + 17 | */ + 18 | + 19 | const SIM_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 20 | + 21 | let sharedPage: Page + 22 | let sharedContext: BrowserContext + 23 | const consoleErrors: string[] = [] + 24 | + 25 | test.describe('Climate simulator', () => { + 26 | test.beforeAll(async ({ browser }) => { + 27 | // Must create a context explicitly — beforeAll doesn't inherit playwright.config `use`. + 28 | sharedContext = await browser.newContext({ + 29 | baseURL: 'http://localhost:5802', + 30 | viewport: { width: 1280, height: 800 }, + 31 | }) + 32 | sharedPage = await sharedContext.newPage() + 33 | + 34 | sharedPage.on('console', (msg) => { + 35 | if (msg.type() === 'error') consoleErrors.push(`[console.error] ${msg.text()}`) + 36 | }) + 37 | sharedPage.on('pageerror', (err) => { + 38 | consoleErrors.push(`[pageerror] ${err.message}`) + 39 | }) + 40 | + 41 | await sharedPage.goto(SIM_URL) + 42 | + 43 | // Canvas appearing means bufferReady fired — world gen + sim run complete. + 44 | // 120s covers WASM compilation + 50-turn run on a slow CI machine. + 45 | await expect(sharedPage.locator('canvas').first()).toBeVisible({ timeout: 120_000 }) + 46 | }) + 47 | + 48 | test.afterAll(async () => { + 49 | await sharedContext.close() + 50 | }) + 51 | + 52 | // ── Startup ──────────────────────────────────────────────────────────────── + 53 | + 54 | test('no console errors during startup and simulation', () => { + 55 | expect(consoleErrors, `Errors:\n${consoleErrors.join('\n')}`).toHaveLength(0) + 56 | }) + 57 | + 58 | // ── Canvas ──────────────────────────────────────────────────────────────── + 59 | + 60 | test('canvas is visible with non-zero dimensions', async () => { + 61 | const canvas = sharedPage.locator('canvas').first() + 62 | await expect(canvas).toBeVisible() + 63 | + 64 | const box = await canvas.boundingBox() + 65 | expect(box, 'canvas has no bounding box').not.toBeNull() +> 66 | expect(box!.width).toBeGreaterThan(400) + | ^ Error: expect(received).toBeGreaterThan(expected) 67 | expect(box!.height).toBeGreaterThan(300) 68 | }) 69 | @@ -443,106 +495,4 @@ Received array: ["[console.error] Failed to load resource: the server responded 164 | } 165 | } 166 | -> 167 | expect(consoleErrors, `Errors after layer toggles:\n${consoleErrors.join('\n')}`).toHaveLength(0) - | ^ Error: Errors after layer toggles: - 168 | }) - 169 | - 170 | test('layers panel collapses and expands', async () => { - 171 | // LayerPanel wraps in MapOverlayPanel(title="Layers", defaultExpanded) - 172 | // MapOverlayPanel header includes the ▸ chevron char in text content, use substring - 173 | const panelHeader = sharedPage.getByRole('button').filter({ hasText: 'Layers' }) - 174 | await expect(panelHeader).toBeVisible({ timeout: 5_000 }) - 175 | - 176 | // Currently expanded (defaultExpanded=true) — collapse it - 177 | const terrainRow = sharedPage.getByRole('checkbox').filter({ hasText: 'Terrain' }) - 178 | await expect(terrainRow).toBeVisible() - 179 | await panelHeader.click() - 180 | await expect(terrainRow).not.toBeVisible({ timeout: 3_000 }) - 181 | - 182 | // Re-expand - 183 | await panelHeader.click() - 184 | await expect(terrainRow).toBeVisible({ timeout: 3_000 }) - 185 | - 186 | expect(consoleErrors, `Errors after panel collapse:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 187 | }) - 188 | - 189 | test('view center: N Pole and S Pole switch without errors', async () => { - 190 | const canvas = sharedPage.locator('canvas').first() - 191 | - 192 | const nPoleBtn = sharedPage.getByRole('radio').filter({ hasText: 'N Pole' }) - 193 | await expect(nPoleBtn).toBeVisible({ timeout: 5_000 }) - 194 | - 195 | // force: true bypasses pointer-event interception from the MoveableChart canvas - 196 | await nPoleBtn.click({ force: true }) - 197 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 198 | - 199 | const sPoleBtn = sharedPage.getByRole('radio').filter({ hasText: 'S Pole' }) - 200 | await sPoleBtn.click({ force: true }) - 201 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 202 | - 203 | const equatorBtn = sharedPage.getByRole('radio').filter({ hasText: 'Equator' }) - 204 | await equatorBtn.click({ force: true }) - 205 | await expect(canvas).toBeVisible({ timeout: 5_000 }) - 206 | - 207 | expect(consoleErrors, `Errors after view switch:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 208 | }) - 209 | - 210 | // ── Category tabs ───────────────────────────────────────────────────────── - 211 | - 212 | test('Environment / Life category tabs switch layer presets', async () => { - 213 | const tabList = sharedPage.getByRole('tablist', { name: 'Simulation category' }) - 214 | await expect(tabList).toBeVisible({ timeout: 5_000 }) - 215 | - 216 | const lifeTab = tabList.getByRole('tab', { name: 'Life' }) - 217 | await lifeTab.click() - 218 | // Life category enables Canopy layer (bit 9) - 219 | await expect(sharedPage.getByRole('checkbox').filter({ hasText: 'Canopy' })).toBeVisible({ timeout: 3_000 }) - 220 | - 221 | // Switch back — Environment only shows climate layers - 222 | const envTab = tabList.getByRole('tab', { name: 'Environment' }) - 223 | await envTab.click() - 224 | await expect(sharedPage.getByRole('checkbox').filter({ hasText: 'Terrain' })).toBeVisible({ timeout: 3_000 }) - 225 | - 226 | expect(consoleErrors, `Errors after category switch:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 227 | }) - 228 | - 229 | // ── Planet selector ──────────────────────────────────────────────────────── - 230 | - 231 | test('planet selector opens and shows all three planets', async () => { - 232 | // Planet button is the first button[aria-haspopup="listbox"] in the DOM — - 233 | // before the ScenarioTabs group buttons which also carry the same attribute. - 234 | const planetBtn = sharedPage.locator('button[aria-haspopup="listbox"]').first() - 235 | await expect(planetBtn).toBeVisible({ timeout: 5_000 }) - 236 | - 237 | await planetBtn.click() - 238 | - 239 | const dropdown = sharedPage.getByRole('listbox', { name: 'Select planet' }) - 240 | await expect(dropdown).toBeVisible({ timeout: 3_000 }) - 241 | - 242 | const options = dropdown.getByRole('option') - 243 | await expect(options).toHaveCount(3) - 244 | - 245 | // Close without switching — the dropdown uses a mousedown-outside listener, - 246 | // so clicking somewhere neutral (the scenario nav) dismisses it. - 247 | await sharedPage.getByRole('navigation', { name: 'Climate scenario groups' }).click() - 248 | await expect(dropdown).not.toBeVisible({ timeout: 3_000 }) - 249 | - 250 | expect(consoleErrors, `Errors after planet dropdown:\n${consoleErrors.join('\n')}`).toHaveLength(0) - 251 | }) - 252 | - 253 | // ── Scenario navigation ──────────────────────────────────────────────────── - 254 | - 255 | test('scenario group nav is present with multiple groups', async () => { - 256 | const nav = sharedPage.getByRole('navigation', { name: 'Climate scenario groups' }) - 257 | await expect(nav).toBeVisible({ timeout: 5_000 }) - 258 | - 259 | const groupButtons = nav.getByRole('button') - 260 | const count = await groupButtons.count() - 261 | expect(count).toBeGreaterThanOrEqual(2) - 262 | }) - 263 | - 264 | test('opening a scenario group dropdown shows options', async () => { - 265 | const nav = sharedPage.getByRole('navigation', { name: 'Climate scenario groups' }) - 266 | const firstGroupBtn = nav.getByRole('button').first() - 267 | ``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-a095c-le-with-non-zero-dimensions-chromium/test-failed-1.png b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-a095c-le-with-non-zero-dimensions-chromium/test-failed-1.png new file mode 100644 index 00000000..452196a4 Binary files /dev/null and b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/simulator-Climate-simulato-a095c-le-with-non-zero-dimensions-chromium/test-failed-1.png differ