fix(@projects/@magic-civilization): 🐛 update climate test dropdown count
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
52746e9137
commit
0a4b705527
7 changed files with 96 additions and 589 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"620c61f0a0b21e7686ea-41dfc9f4053dc733176e",
|
||||
"620c61f0a0b21e7686ea-00dd36638b59435b947b"
|
||||
"620c61f0a0b21e7686ea-1ae6cca4a49867df6bb8"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
|
|
@ -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)
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
|
|
@ -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 |
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Loading…
Add table
Reference in a new issue