diff --git a/games/age-of-dwarves/guide/src/components/icons/CharacterIcons.tsx b/games/age-of-dwarves/guide/src/components/icons/CharacterIcons.tsx new file mode 100644 index 00000000..b1aa75a3 --- /dev/null +++ b/games/age-of-dwarves/guide/src/components/icons/CharacterIcons.tsx @@ -0,0 +1,52 @@ +import type { ReactElement, SVGAttributes } from 'react' + +// Placeholder character icons used in the welcome modal race/gender picker. +// Replace with real sprite art when the sprite generation pipeline produces +// character portraits. + +type IconProps = SVGAttributes + +export function DwarfMaleIcon({ width = 40, height = 40, ...props }: IconProps): ReactElement { + return ( + + ) +} + +export function DwarfFemaleIcon({ width = 40, height = 40, ...props }: IconProps): ReactElement { + return ( + + ) +} diff --git a/games/age-of-dwarves/guide/src/components/welcome/race-icons.ts b/games/age-of-dwarves/guide/src/components/welcome/race-icons.ts index c3d6cf69..193007e6 100644 --- a/games/age-of-dwarves/guide/src/components/welcome/race-icons.ts +++ b/games/age-of-dwarves/guide/src/components/welcome/race-icons.ts @@ -1,21 +1,9 @@ import type { ComponentType, SVGAttributes } from 'react' -import { - DwarfMaleIcon, - DwarfFemaleIcon, - ElfMaleIcon, - ElfFemaleIcon, - KnightMaleIcon, - KnightFemaleIcon, - TrollMaleIcon, - TrollFemaleIcon, -} from '@lilith/spaceship-ui-icons' +import { DwarfMaleIcon, DwarfFemaleIcon } from '@/components/icons/CharacterIcons' import type { ConcreteRace, ConcreteGender } from '@/contexts/PreferencesContext' type RaceIconComponent = ComponentType> export const RACE_ICONS: Record> = { - dwarves: { male: DwarfMaleIcon, female: DwarfFemaleIcon }, - high_elves: { male: ElfMaleIcon, female: ElfFemaleIcon }, - humans: { male: KnightMaleIcon, female: KnightFemaleIcon }, - orcs: { male: TrollMaleIcon, female: TrollFemaleIcon }, + dwarves: { male: DwarfMaleIcon, female: DwarfFemaleIcon }, } diff --git a/games/age-of-dwarves/guide/src/hooks/usePlayerPreferences.ts b/games/age-of-dwarves/guide/src/hooks/usePlayerPreferences.ts index e91f50fe..7cb127fa 100644 --- a/games/age-of-dwarves/guide/src/hooks/usePlayerPreferences.ts +++ b/games/age-of-dwarves/guide/src/hooks/usePlayerPreferences.ts @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react' -export type RacePreference = 'high_elves' | 'humans' | 'dwarves' | 'orcs' | 'random' +export type RacePreference = 'dwarves' | 'random' export type GenderPreference = 'male' | 'female' | 'random' export type ColorMode = 'dark' | 'light' export type FontSize = 'sm' | 'md' | 'lg' | 'xl' diff --git a/games/age-of-dwarves/guide/src/types/spaceship-ui-icons.d.ts b/games/age-of-dwarves/guide/src/types/spaceship-ui-icons.d.ts deleted file mode 100644 index 6a0903cb..00000000 --- a/games/age-of-dwarves/guide/src/types/spaceship-ui-icons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Type stub for @lilith/spaceship-ui-icons. -// -// The package ships TypeScript source (.tsx) with no compiled output. -// With moduleResolution: "bundler", tsc follows the package.json exports -// into the source and type-checks its broken internal types. -// This stub is wired via tsconfig paths to intercept resolution. -import type { ComponentType, SVGAttributes } from 'react' - -type IconComponent = ComponentType> - -export declare const DwarfMaleIcon: IconComponent -export declare const DwarfFemaleIcon: IconComponent -export declare const ElfMaleIcon: IconComponent -export declare const ElfFemaleIcon: IconComponent -export declare const KnightMaleIcon: IconComponent -export declare const KnightFemaleIcon: IconComponent -export declare const TrollMaleIcon: IconComponent -export declare const TrollFemaleIcon: IconComponent diff --git a/games/age-of-dwarves/guide/test-results/.last-run.json b/games/age-of-dwarves/guide/test-results/.last-run.json new file mode 100644 index 00000000..f1a1430a --- /dev/null +++ b/games/age-of-dwarves/guide/test-results/.last-run.json @@ -0,0 +1,8 @@ +{ + "status": "failed", + "failedTests": [ + "620c61f0a0b21e7686ea-7c2dd3ded526a0e2f3a1", + "620c61f0a0b21e7686ea-44fa4f9763aa1dd44e1c", + "620c61f0a0b21e7686ea-d47d81b7a53c851889d4" + ] +} \ No newline at end of file diff --git a/games/age-of-dwarves/guide/test-results/simulator-Climate-simulato-02b34-ompletes-and-canvas-renders-chromium/error-context.md b/games/age-of-dwarves/guide/test-results/simulator-Climate-simulato-02b34-ompletes-and-canvas-renders-chromium/error-context.md new file mode 100644 index 00000000..2ec1a204 --- /dev/null +++ b/games/age-of-dwarves/guide/test-results/simulator-Climate-simulato-02b34-ompletes-and-canvas-renders-chromium/error-context.md @@ -0,0 +1,185 @@ +# 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 >> simulation completes and canvas renders +- Location: e2e/simulator.spec.ts:82:3 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator('canvas').first() +Expected: visible +Timeout: 60000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 60000ms + - waiting for locator('canvas').first() + +``` + +# Page snapshot + +```yaml +- generic [ref=e5]: + - generic [ref=e6]: + - button "◉ Earth ▾" [ref=e8] [cursor=pointer]: + - generic [ref=e9]: ◉ + - generic [ref=e10]: Earth + - generic [ref=e11]: ▾ + - tablist "Simulation category" [ref=e13]: + - tab "Environment" [selected] [ref=e14] [cursor=pointer] + - tab "Life" [ref=e15] [cursor=pointer] + - navigation "Climate scenario groups" [ref=e16]: + - button "Lifeless Worlds Hadean Earth" [ref=e18] [cursor=pointer]: + - generic [ref=e20]: + - generic [ref=e21]: Lifeless Worlds + - generic [ref=e22]: Hadean Earth + - generic [ref=e23]: ▾ + - button "Eco Disaster" [ref=e25] [cursor=pointer]: + - generic [ref=e28]: Eco Disaster + - generic [ref=e29]: ▾ + - button "ET Disaster" [ref=e31] [cursor=pointer]: + - generic [ref=e34]: ET Disaster + - generic [ref=e35]: ▾ + - paragraph [ref=e37]: "Planet at formation: extreme heat, no liquid water, no biology. Deep Earth injection and radiative cooling slowly build the first ocean." + - generic [ref=e38]: + - generic [ref=e39]: + - heading "Hadean Earth" [level=3] [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: Primordial atmosphere + - generic [ref=e43]: No biology + - generic [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: ○ + - generic [ref=e47]: + - generic [ref=e48]: World generation + - generic [ref=e49]: Continents, oceans, rivers, wind patterns + - generic [ref=e50]: + - generic [ref=e51]: ○ + - generic [ref=e52]: + - generic [ref=e53]: Scenario simulation + - generic [ref=e54]: 50 turns + - generic [ref=e55]: + - generic [ref=e56]: ○ + - generic [ref=e57]: + - generic [ref=e58]: Playback buffer + - generic [ref=e59]: Encoding frames for smooth playback + - generic [ref=e60]: 58.6s +``` + +# Test source + +```ts + 1 | import { test, expect } from '@playwright/test' + 2 | + 3 | /** + 4 | * Climate simulator E2E tests. + 5 | * + 6 | * These tests require the dev server running on port 5800: + 7 | * pnpm dev + 8 | * + 9 | * Run with: + 10 | * npx playwright test + 11 | * + 12 | * All tests use ?totalTurns=50&buffer=0 so the simulation completes in seconds + 13 | * rather than waiting for the default 2000 turns + 10s prebuffer. + 14 | * + 15 | * DOM structure of the loading overlay (from live snapshot): + 16 | * step-row + 17 | * icon-div ("✓" | "◉" | "○") + 18 | * text-div + 19 | * name-div ("World generation" | "Scenario simulation" | "Playback buffer") + 20 | * desc-div (description or turn progress) + 21 | * [pct-div] ("4%" — only when simulation is active) + 22 | * + 23 | * Note: "Playback buffer" never shows ✓ — it's either active (◉) or pending (○). + 24 | * When bufferReady becomes true the entire loading overlay is replaced by the canvas. + 25 | * + 26 | * Selectors use :text-is("✓") to precisely match the icon element (exact text), + 27 | * then navigate to its parent (the step row) and filter by step name. + 28 | */ + 29 | + 30 | const BASE_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 31 | + 32 | test.describe('Climate simulator', () => { + 33 | test('starts without console errors', async ({ page }) => { + 34 | const errors: string[] = [] + 35 | page.on('console', msg => { + 36 | if (msg.type() === 'error') errors.push(msg.text()) + 37 | }) + 38 | page.on('pageerror', err => errors.push(err.message)) + 39 | + 40 | await page.goto(BASE_URL) + 41 | await page.waitForTimeout(3000) + 42 | + 43 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 44 | }) + 45 | + 46 | test('world generation completes', async ({ page }) => { + 47 | const errors: string[] = [] + 48 | page.on('console', msg => { + 49 | if (msg.type() === 'error') errors.push(msg.text()) + 50 | }) + 51 | page.on('pageerror', err => errors.push(err.message)) + 52 | + 53 | await page.goto(BASE_URL) + 54 | + 55 | // :text-is("✓") matches only elements whose full text is exactly ✓ (the icon divs) + 56 | // .locator('..') gets the parent step-row div + 57 | // .filter({ hasText }) picks the row whose descendants include "World generation" + 58 | await expect( + 59 | page.locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) + 60 | ).toBeVisible({ timeout: 30_000 }) + 61 | + 62 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 63 | }) + 64 | + 65 | test('scenario simulation reaches > 0% progress', async ({ page }) => { + 66 | const errors: string[] = [] + 67 | page.on('console', msg => { + 68 | if (msg.type() === 'error') errors.push(msg.text()) + 69 | }) + 70 | page.on('pageerror', err => errors.push(err.message)) + 71 | + 72 | await page.goto(BASE_URL) + 73 | + 74 | // The turn counter "Turn N / M" appears only when scenario simulation is active + 75 | await expect( + 76 | page.locator('text=/Turn \\d+/') + 77 | ).toBeVisible({ timeout: 60_000 }) + 78 | + 79 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 80 | }) + 81 | + 82 | test('simulation completes and canvas renders', async ({ page }) => { + 83 | const errors: string[] = [] + 84 | page.on('console', msg => { + 85 | if (msg.type() === 'error') errors.push(msg.text()) + 86 | }) + 87 | page.on('pageerror', err => errors.push(err.message)) + 88 | + 89 | await page.goto(BASE_URL) + 90 | + 91 | // When bufferReady becomes true the loading overlay is replaced by the WebGL canvas. + 92 | // canvas is only rendered once a frame is delivered — wait for it to be visible. + 93 | const canvas = page.locator('canvas').first() +> 94 | await expect(canvas).toBeVisible({ timeout: 60_000 }) + | ^ Error: expect(locator).toBeVisible() failed + 95 | const box = await canvas.boundingBox() + 96 | expect(box, 'canvas has no bounding box').not.toBeNull() + 97 | expect(box!.width, 'canvas width').toBeGreaterThan(0) + 98 | expect(box!.height, 'canvas height').toBeGreaterThan(0) + 99 | + 100 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 101 | }) + 102 | }) + 103 | +``` \ No newline at end of file diff --git a/games/age-of-dwarves/guide/test-results/simulator-Climate-simulato-0cd4c-mulation-reaches-0-progress-chromium/error-context.md b/games/age-of-dwarves/guide/test-results/simulator-Climate-simulato-0cd4c-mulation-reaches-0-progress-chromium/error-context.md new file mode 100644 index 00000000..b4839cc7 --- /dev/null +++ b/games/age-of-dwarves/guide/test-results/simulator-Climate-simulato-0cd4c-mulation-reaches-0-progress-chromium/error-context.md @@ -0,0 +1,185 @@ +# 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 >> scenario simulation reaches > 0% progress +- Location: e2e/simulator.spec.ts:65:3 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator('text=/Turn \\d+/') +Expected: visible +Timeout: 60000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 60000ms + - waiting for locator('text=/Turn \\d+/') + +``` + +# Page snapshot + +```yaml +- generic [ref=e5]: + - generic [ref=e6]: + - button "◉ Earth ▾" [ref=e8] [cursor=pointer]: + - generic [ref=e9]: ◉ + - generic [ref=e10]: Earth + - generic [ref=e11]: ▾ + - tablist "Simulation category" [ref=e13]: + - tab "Environment" [selected] [ref=e14] [cursor=pointer] + - tab "Life" [ref=e15] [cursor=pointer] + - navigation "Climate scenario groups" [ref=e16]: + - button "Lifeless Worlds Hadean Earth" [ref=e18] [cursor=pointer]: + - generic [ref=e20]: + - generic [ref=e21]: Lifeless Worlds + - generic [ref=e22]: Hadean Earth + - generic [ref=e23]: ▾ + - button "Eco Disaster" [ref=e25] [cursor=pointer]: + - generic [ref=e28]: Eco Disaster + - generic [ref=e29]: ▾ + - button "ET Disaster" [ref=e31] [cursor=pointer]: + - generic [ref=e34]: ET Disaster + - generic [ref=e35]: ▾ + - paragraph [ref=e37]: "Planet at formation: extreme heat, no liquid water, no biology. Deep Earth injection and radiative cooling slowly build the first ocean." + - generic [ref=e38]: + - generic [ref=e39]: + - heading "Hadean Earth" [level=3] [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: Primordial atmosphere + - generic [ref=e43]: No biology + - generic [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: ○ + - generic [ref=e47]: + - generic [ref=e48]: World generation + - generic [ref=e49]: Continents, oceans, rivers, wind patterns + - generic [ref=e50]: + - generic [ref=e51]: ○ + - generic [ref=e52]: + - generic [ref=e53]: Scenario simulation + - generic [ref=e54]: 50 turns + - generic [ref=e55]: + - generic [ref=e56]: ○ + - generic [ref=e57]: + - generic [ref=e58]: Playback buffer + - generic [ref=e59]: Encoding frames for smooth playback + - generic [ref=e60]: 58.9s +``` + +# Test source + +```ts + 1 | import { test, expect } from '@playwright/test' + 2 | + 3 | /** + 4 | * Climate simulator E2E tests. + 5 | * + 6 | * These tests require the dev server running on port 5800: + 7 | * pnpm dev + 8 | * + 9 | * Run with: + 10 | * npx playwright test + 11 | * + 12 | * All tests use ?totalTurns=50&buffer=0 so the simulation completes in seconds + 13 | * rather than waiting for the default 2000 turns + 10s prebuffer. + 14 | * + 15 | * DOM structure of the loading overlay (from live snapshot): + 16 | * step-row + 17 | * icon-div ("✓" | "◉" | "○") + 18 | * text-div + 19 | * name-div ("World generation" | "Scenario simulation" | "Playback buffer") + 20 | * desc-div (description or turn progress) + 21 | * [pct-div] ("4%" — only when simulation is active) + 22 | * + 23 | * Note: "Playback buffer" never shows ✓ — it's either active (◉) or pending (○). + 24 | * When bufferReady becomes true the entire loading overlay is replaced by the canvas. + 25 | * + 26 | * Selectors use :text-is("✓") to precisely match the icon element (exact text), + 27 | * then navigate to its parent (the step row) and filter by step name. + 28 | */ + 29 | + 30 | const BASE_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 31 | + 32 | test.describe('Climate simulator', () => { + 33 | test('starts without console errors', async ({ page }) => { + 34 | const errors: string[] = [] + 35 | page.on('console', msg => { + 36 | if (msg.type() === 'error') errors.push(msg.text()) + 37 | }) + 38 | page.on('pageerror', err => errors.push(err.message)) + 39 | + 40 | await page.goto(BASE_URL) + 41 | await page.waitForTimeout(3000) + 42 | + 43 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 44 | }) + 45 | + 46 | test('world generation completes', async ({ page }) => { + 47 | const errors: string[] = [] + 48 | page.on('console', msg => { + 49 | if (msg.type() === 'error') errors.push(msg.text()) + 50 | }) + 51 | page.on('pageerror', err => errors.push(err.message)) + 52 | + 53 | await page.goto(BASE_URL) + 54 | + 55 | // :text-is("✓") matches only elements whose full text is exactly ✓ (the icon divs) + 56 | // .locator('..') gets the parent step-row div + 57 | // .filter({ hasText }) picks the row whose descendants include "World generation" + 58 | await expect( + 59 | page.locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) + 60 | ).toBeVisible({ timeout: 30_000 }) + 61 | + 62 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 63 | }) + 64 | + 65 | test('scenario simulation reaches > 0% progress', async ({ page }) => { + 66 | const errors: string[] = [] + 67 | page.on('console', msg => { + 68 | if (msg.type() === 'error') errors.push(msg.text()) + 69 | }) + 70 | page.on('pageerror', err => errors.push(err.message)) + 71 | + 72 | await page.goto(BASE_URL) + 73 | + 74 | // The turn counter "Turn N / M" appears only when scenario simulation is active + 75 | await expect( + 76 | page.locator('text=/Turn \\d+/') +> 77 | ).toBeVisible({ timeout: 60_000 }) + | ^ Error: expect(locator).toBeVisible() failed + 78 | + 79 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 80 | }) + 81 | + 82 | test('simulation completes and canvas renders', async ({ page }) => { + 83 | const errors: string[] = [] + 84 | page.on('console', msg => { + 85 | if (msg.type() === 'error') errors.push(msg.text()) + 86 | }) + 87 | page.on('pageerror', err => errors.push(err.message)) + 88 | + 89 | await page.goto(BASE_URL) + 90 | + 91 | // When bufferReady becomes true the loading overlay is replaced by the WebGL canvas. + 92 | // canvas is only rendered once a frame is delivered — wait for it to be visible. + 93 | const canvas = page.locator('canvas').first() + 94 | await expect(canvas).toBeVisible({ timeout: 60_000 }) + 95 | const box = await canvas.boundingBox() + 96 | expect(box, 'canvas has no bounding box').not.toBeNull() + 97 | expect(box!.width, 'canvas width').toBeGreaterThan(0) + 98 | expect(box!.height, 'canvas height').toBeGreaterThan(0) + 99 | + 100 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 101 | }) + 102 | }) + 103 | +``` \ No newline at end of file diff --git a/games/age-of-dwarves/guide/test-results/simulator-Climate-simulator-world-generation-completes-chromium/error-context.md b/games/age-of-dwarves/guide/test-results/simulator-Climate-simulator-world-generation-completes-chromium/error-context.md new file mode 100644 index 00000000..f3114393 --- /dev/null +++ b/games/age-of-dwarves/guide/test-results/simulator-Climate-simulator-world-generation-completes-chromium/error-context.md @@ -0,0 +1,185 @@ +# 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 >> world generation completes +- Location: e2e/simulator.spec.ts:46:3 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) +Expected: visible +Timeout: 30000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 30000ms + - waiting for locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) + +``` + +# Page snapshot + +```yaml +- generic [ref=e5]: + - generic [ref=e6]: + - button "◉ Earth ▾" [ref=e8] [cursor=pointer]: + - generic [ref=e9]: ◉ + - generic [ref=e10]: Earth + - generic [ref=e11]: ▾ + - tablist "Simulation category" [ref=e13]: + - tab "Environment" [selected] [ref=e14] [cursor=pointer] + - tab "Life" [ref=e15] [cursor=pointer] + - navigation "Climate scenario groups" [ref=e16]: + - button "Lifeless Worlds Hadean Earth" [ref=e18] [cursor=pointer]: + - generic [ref=e20]: + - generic [ref=e21]: Lifeless Worlds + - generic [ref=e22]: Hadean Earth + - generic [ref=e23]: ▾ + - button "Eco Disaster" [ref=e25] [cursor=pointer]: + - generic [ref=e28]: Eco Disaster + - generic [ref=e29]: ▾ + - button "ET Disaster" [ref=e31] [cursor=pointer]: + - generic [ref=e34]: ET Disaster + - generic [ref=e35]: ▾ + - paragraph [ref=e37]: "Planet at formation: extreme heat, no liquid water, no biology. Deep Earth injection and radiative cooling slowly build the first ocean." + - generic [ref=e38]: + - generic [ref=e39]: + - heading "Hadean Earth" [level=3] [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: Primordial atmosphere + - generic [ref=e43]: No biology + - generic [ref=e44]: + - generic [ref=e45]: + - generic [ref=e46]: ○ + - generic [ref=e47]: + - generic [ref=e48]: World generation + - generic [ref=e49]: Continents, oceans, rivers, wind patterns + - generic [ref=e50]: + - generic [ref=e51]: ○ + - generic [ref=e52]: + - generic [ref=e53]: Scenario simulation + - generic [ref=e54]: 50 turns + - generic [ref=e55]: + - generic [ref=e56]: ○ + - generic [ref=e57]: + - generic [ref=e58]: Playback buffer + - generic [ref=e59]: Encoding frames for smooth playback + - generic [ref=e60]: 28.2s +``` + +# Test source + +```ts + 1 | import { test, expect } from '@playwright/test' + 2 | + 3 | /** + 4 | * Climate simulator E2E tests. + 5 | * + 6 | * These tests require the dev server running on port 5800: + 7 | * pnpm dev + 8 | * + 9 | * Run with: + 10 | * npx playwright test + 11 | * + 12 | * All tests use ?totalTurns=50&buffer=0 so the simulation completes in seconds + 13 | * rather than waiting for the default 2000 turns + 10s prebuffer. + 14 | * + 15 | * DOM structure of the loading overlay (from live snapshot): + 16 | * step-row + 17 | * icon-div ("✓" | "◉" | "○") + 18 | * text-div + 19 | * name-div ("World generation" | "Scenario simulation" | "Playback buffer") + 20 | * desc-div (description or turn progress) + 21 | * [pct-div] ("4%" — only when simulation is active) + 22 | * + 23 | * Note: "Playback buffer" never shows ✓ — it's either active (◉) or pending (○). + 24 | * When bufferReady becomes true the entire loading overlay is replaced by the canvas. + 25 | * + 26 | * Selectors use :text-is("✓") to precisely match the icon element (exact text), + 27 | * then navigate to its parent (the step row) and filter by step name. + 28 | */ + 29 | + 30 | const BASE_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 31 | + 32 | test.describe('Climate simulator', () => { + 33 | test('starts without console errors', async ({ page }) => { + 34 | const errors: string[] = [] + 35 | page.on('console', msg => { + 36 | if (msg.type() === 'error') errors.push(msg.text()) + 37 | }) + 38 | page.on('pageerror', err => errors.push(err.message)) + 39 | + 40 | await page.goto(BASE_URL) + 41 | await page.waitForTimeout(3000) + 42 | + 43 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 44 | }) + 45 | + 46 | test('world generation completes', async ({ page }) => { + 47 | const errors: string[] = [] + 48 | page.on('console', msg => { + 49 | if (msg.type() === 'error') errors.push(msg.text()) + 50 | }) + 51 | page.on('pageerror', err => errors.push(err.message)) + 52 | + 53 | await page.goto(BASE_URL) + 54 | + 55 | // :text-is("✓") matches only elements whose full text is exactly ✓ (the icon divs) + 56 | // .locator('..') gets the parent step-row div + 57 | // .filter({ hasText }) picks the row whose descendants include "World generation" + 58 | await expect( + 59 | page.locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) +> 60 | ).toBeVisible({ timeout: 30_000 }) + | ^ Error: expect(locator).toBeVisible() failed + 61 | + 62 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 63 | }) + 64 | + 65 | test('scenario simulation reaches > 0% progress', async ({ page }) => { + 66 | const errors: string[] = [] + 67 | page.on('console', msg => { + 68 | if (msg.type() === 'error') errors.push(msg.text()) + 69 | }) + 70 | page.on('pageerror', err => errors.push(err.message)) + 71 | + 72 | await page.goto(BASE_URL) + 73 | + 74 | // The turn counter "Turn N / M" appears only when scenario simulation is active + 75 | await expect( + 76 | page.locator('text=/Turn \\d+/') + 77 | ).toBeVisible({ timeout: 60_000 }) + 78 | + 79 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 80 | }) + 81 | + 82 | test('simulation completes and canvas renders', async ({ page }) => { + 83 | const errors: string[] = [] + 84 | page.on('console', msg => { + 85 | if (msg.type() === 'error') errors.push(msg.text()) + 86 | }) + 87 | page.on('pageerror', err => errors.push(err.message)) + 88 | + 89 | await page.goto(BASE_URL) + 90 | + 91 | // When bufferReady becomes true the loading overlay is replaced by the WebGL canvas. + 92 | // canvas is only rendered once a frame is delivered — wait for it to be visible. + 93 | const canvas = page.locator('canvas').first() + 94 | await expect(canvas).toBeVisible({ timeout: 60_000 }) + 95 | const box = await canvas.boundingBox() + 96 | expect(box, 'canvas has no bounding box').not.toBeNull() + 97 | expect(box!.width, 'canvas width').toBeGreaterThan(0) + 98 | expect(box!.height, 'canvas height').toBeGreaterThan(0) + 99 | + 100 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 101 | }) + 102 | }) + 103 | +``` \ No newline at end of file