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 index 2ec1a204..22b8213c 100644 --- 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 @@ -7,7 +7,7 @@ # Test info - Name: simulator.spec.ts >> Climate simulator >> simulation completes and canvas renders -- Location: e2e/simulator.spec.ts:82:3 +- Location: e2e/simulator.spec.ts:86:3 # Error details @@ -72,7 +72,7 @@ Call log: - generic [ref=e57]: - generic [ref=e58]: Playback buffer - generic [ref=e59]: Encoding frames for smooth playback - - generic [ref=e60]: 58.6s + - generic [ref=e60]: 59.1s ``` # Test source @@ -105,81 +105,85 @@ Call log: 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 | }) + 28 | * + 29 | * Tests 2 and 3 use .or(canvas) because with a warm dev-server cache and only + 30 | * 50 turns, the simulation may complete before the loading overlay is ever observed. + 31 | * Both the transient loading state AND the final canvas prove the phase succeeded. + 32 | */ + 33 | + 34 | const BASE_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 35 | + 36 | test.describe('Climate simulator', () => { + 37 | test('starts without console errors', async ({ page }) => { + 38 | const errors: string[] = [] + 39 | page.on('console', msg => { + 40 | if (msg.type() === 'error') errors.push(msg.text()) + 41 | }) + 42 | page.on('pageerror', err => errors.push(err.message)) + 43 | + 44 | await page.goto(BASE_URL) + 45 | await page.waitForTimeout(3000) + 46 | + 47 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 48 | }) + 49 | + 50 | test('world generation completes', async ({ page }) => { + 51 | const errors: string[] = [] + 52 | page.on('console', msg => { + 53 | if (msg.type() === 'error') errors.push(msg.text()) + 54 | }) + 55 | page.on('pageerror', err => errors.push(err.message)) + 56 | + 57 | await page.goto(BASE_URL) + 58 | + 59 | // Accept either the transient ✓ on "World generation" (visible during loading) + 60 | // OR the canvas (visible once simulation finishes) — both prove world gen ran. + 61 | const worldGenCheckmark = page.locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) + 62 | const canvas = page.locator('canvas').first() + 63 | await expect(worldGenCheckmark.or(canvas)).toBeVisible({ timeout: 30_000 }) 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 }) + 65 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 66 | }) + 67 | + 68 | test('scenario simulation reaches > 0% progress', async ({ page }) => { + 69 | const errors: string[] = [] + 70 | page.on('console', msg => { + 71 | if (msg.type() === 'error') errors.push(msg.text()) + 72 | }) + 73 | page.on('pageerror', err => errors.push(err.message)) + 74 | + 75 | await page.goto(BASE_URL) + 76 | + 77 | // Accept either the transient "Turn N / M" counter in the loading overlay + 78 | // OR the completed canvas — both prove the scenario simulation ran. + 79 | const turnCounter = page.locator('text=/Turn \\d+/') + 80 | const canvas = page.locator('canvas').first() + 81 | await expect(turnCounter.or(canvas)).toBeVisible({ timeout: 60_000 }) + 82 | + 83 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 84 | }) + 85 | + 86 | test('simulation completes and canvas renders', async ({ page }) => { + 87 | const errors: string[] = [] + 88 | page.on('console', msg => { + 89 | if (msg.type() === 'error') errors.push(msg.text()) + 90 | }) + 91 | page.on('pageerror', err => errors.push(err.message)) + 92 | + 93 | await page.goto(BASE_URL) + 94 | + 95 | // When bufferReady becomes true the loading overlay is replaced by the WebGL canvas. + 96 | // canvas is only rendered once a frame is delivered — wait for it to be visible. + 97 | const canvas = page.locator('canvas').first() +> 98 | 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 | }) + 99 | const box = await canvas.boundingBox() + 100 | expect(box, 'canvas has no bounding box').not.toBeNull() + 101 | expect(box!.width, 'canvas width').toBeGreaterThan(0) + 102 | expect(box!.height, 'canvas height').toBeGreaterThan(0) 103 | + 104 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 105 | }) + 106 | }) + 107 | ``` \ 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 index b4839cc7..9551dd96 100644 --- 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 @@ -7,21 +7,21 @@ # Test info - Name: simulator.spec.ts >> Climate simulator >> scenario simulation reaches > 0% progress -- Location: e2e/simulator.spec.ts:65:3 +- Location: e2e/simulator.spec.ts:68:3 # Error details ``` Error: expect(locator).toBeVisible() failed -Locator: locator('text=/Turn \\d+/') +Locator: locator('text=/Turn \\d+/').or(locator('canvas').first()) Expected: visible Timeout: 60000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 60000ms - - waiting for locator('text=/Turn \\d+/') + - waiting for locator('text=/Turn \\d+/').or(locator('canvas').first()) ``` @@ -72,7 +72,7 @@ Call log: - generic [ref=e57]: - generic [ref=e58]: Playback buffer - generic [ref=e59]: Encoding frames for smooth playback - - generic [ref=e60]: 58.9s + - generic [ref=e60]: 59.1s ``` # Test source @@ -105,81 +105,85 @@ Call log: 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 | }) + 28 | * + 29 | * Tests 2 and 3 use .or(canvas) because with a warm dev-server cache and only + 30 | * 50 turns, the simulation may complete before the loading overlay is ever observed. + 31 | * Both the transient loading state AND the final canvas prove the phase succeeded. + 32 | */ + 33 | + 34 | const BASE_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 35 | + 36 | test.describe('Climate simulator', () => { + 37 | test('starts without console errors', async ({ page }) => { + 38 | const errors: string[] = [] + 39 | page.on('console', msg => { + 40 | if (msg.type() === 'error') errors.push(msg.text()) + 41 | }) + 42 | page.on('pageerror', err => errors.push(err.message)) + 43 | + 44 | await page.goto(BASE_URL) + 45 | await page.waitForTimeout(3000) + 46 | + 47 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 48 | }) + 49 | + 50 | test('world generation completes', async ({ page }) => { + 51 | const errors: string[] = [] + 52 | page.on('console', msg => { + 53 | if (msg.type() === 'error') errors.push(msg.text()) + 54 | }) + 55 | page.on('pageerror', err => errors.push(err.message)) + 56 | + 57 | await page.goto(BASE_URL) + 58 | + 59 | // Accept either the transient ✓ on "World generation" (visible during loading) + 60 | // OR the canvas (visible once simulation finishes) — both prove world gen ran. + 61 | const worldGenCheckmark = page.locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) + 62 | const canvas = page.locator('canvas').first() + 63 | await expect(worldGenCheckmark.or(canvas)).toBeVisible({ timeout: 30_000 }) 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 | }) + 65 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 66 | }) + 67 | + 68 | test('scenario simulation reaches > 0% progress', async ({ page }) => { + 69 | const errors: string[] = [] + 70 | page.on('console', msg => { + 71 | if (msg.type() === 'error') errors.push(msg.text()) + 72 | }) + 73 | page.on('pageerror', err => errors.push(err.message)) + 74 | + 75 | await page.goto(BASE_URL) + 76 | + 77 | // Accept either the transient "Turn N / M" counter in the loading overlay + 78 | // OR the completed canvas — both prove the scenario simulation ran. + 79 | const turnCounter = page.locator('text=/Turn \\d+/') + 80 | const canvas = page.locator('canvas').first() +> 81 | await expect(turnCounter.or(canvas)).toBeVisible({ timeout: 60_000 }) + | ^ Error: expect(locator).toBeVisible() failed + 82 | + 83 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 84 | }) + 85 | + 86 | test('simulation completes and canvas renders', async ({ page }) => { + 87 | const errors: string[] = [] + 88 | page.on('console', msg => { + 89 | if (msg.type() === 'error') errors.push(msg.text()) + 90 | }) + 91 | page.on('pageerror', err => errors.push(err.message)) + 92 | + 93 | await page.goto(BASE_URL) + 94 | + 95 | // When bufferReady becomes true the loading overlay is replaced by the WebGL canvas. + 96 | // canvas is only rendered once a frame is delivered — wait for it to be visible. + 97 | const canvas = page.locator('canvas').first() + 98 | await expect(canvas).toBeVisible({ timeout: 60_000 }) + 99 | const box = await canvas.boundingBox() + 100 | expect(box, 'canvas has no bounding box').not.toBeNull() + 101 | expect(box!.width, 'canvas width').toBeGreaterThan(0) + 102 | expect(box!.height, 'canvas height').toBeGreaterThan(0) 103 | + 104 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 105 | }) + 106 | }) + 107 | ``` \ 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 index f3114393..ac213d9e 100644 --- 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 @@ -7,21 +7,21 @@ # Test info - Name: simulator.spec.ts >> Climate simulator >> world generation completes -- Location: e2e/simulator.spec.ts:46:3 +- Location: e2e/simulator.spec.ts:50:3 # Error details ``` Error: expect(locator).toBeVisible() failed -Locator: locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) +Locator: locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }).or(locator('canvas').first()) 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' }) + - waiting for locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }).or(locator('canvas').first()) ``` @@ -72,7 +72,7 @@ Call log: - generic [ref=e57]: - generic [ref=e58]: Playback buffer - generic [ref=e59]: Encoding frames for smooth playback - - generic [ref=e60]: 28.2s + - generic [ref=e60]: 28.9s ``` # Test source @@ -105,81 +105,85 @@ Call log: 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 | }) + 28 | * + 29 | * Tests 2 and 3 use .or(canvas) because with a warm dev-server cache and only + 30 | * 50 turns, the simulation may complete before the loading overlay is ever observed. + 31 | * Both the transient loading state AND the final canvas prove the phase succeeded. + 32 | */ + 33 | + 34 | const BASE_URL = '/climate/simulation?noGui=true&skip=welcome&totalTurns=50&buffer=0' + 35 | + 36 | test.describe('Climate simulator', () => { + 37 | test('starts without console errors', async ({ page }) => { + 38 | const errors: string[] = [] + 39 | page.on('console', msg => { + 40 | if (msg.type() === 'error') errors.push(msg.text()) + 41 | }) + 42 | page.on('pageerror', err => errors.push(err.message)) + 43 | + 44 | await page.goto(BASE_URL) + 45 | await page.waitForTimeout(3000) + 46 | + 47 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 48 | }) + 49 | + 50 | test('world generation completes', async ({ page }) => { + 51 | const errors: string[] = [] + 52 | page.on('console', msg => { + 53 | if (msg.type() === 'error') errors.push(msg.text()) + 54 | }) + 55 | page.on('pageerror', err => errors.push(err.message)) + 56 | + 57 | await page.goto(BASE_URL) + 58 | + 59 | // Accept either the transient ✓ on "World generation" (visible during loading) + 60 | // OR the canvas (visible once simulation finishes) — both prove world gen ran. + 61 | const worldGenCheckmark = page.locator(':text-is("✓")').locator('..').filter({ hasText: 'World generation' }) + 62 | const canvas = page.locator('canvas').first() +> 63 | await expect(worldGenCheckmark.or(canvas)).toBeVisible({ timeout: 30_000 }) + | ^ Error: expect(locator).toBeVisible() failed 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 | }) + 65 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 66 | }) + 67 | + 68 | test('scenario simulation reaches > 0% progress', async ({ page }) => { + 69 | const errors: string[] = [] + 70 | page.on('console', msg => { + 71 | if (msg.type() === 'error') errors.push(msg.text()) + 72 | }) + 73 | page.on('pageerror', err => errors.push(err.message)) + 74 | + 75 | await page.goto(BASE_URL) + 76 | + 77 | // Accept either the transient "Turn N / M" counter in the loading overlay + 78 | // OR the completed canvas — both prove the scenario simulation ran. + 79 | const turnCounter = page.locator('text=/Turn \\d+/') + 80 | const canvas = page.locator('canvas').first() + 81 | await expect(turnCounter.or(canvas)).toBeVisible({ timeout: 60_000 }) + 82 | + 83 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 84 | }) + 85 | + 86 | test('simulation completes and canvas renders', async ({ page }) => { + 87 | const errors: string[] = [] + 88 | page.on('console', msg => { + 89 | if (msg.type() === 'error') errors.push(msg.text()) + 90 | }) + 91 | page.on('pageerror', err => errors.push(err.message)) + 92 | + 93 | await page.goto(BASE_URL) + 94 | + 95 | // When bufferReady becomes true the loading overlay is replaced by the WebGL canvas. + 96 | // canvas is only rendered once a frame is delivered — wait for it to be visible. + 97 | const canvas = page.locator('canvas').first() + 98 | await expect(canvas).toBeVisible({ timeout: 60_000 }) + 99 | const box = await canvas.boundingBox() + 100 | expect(box, 'canvas has no bounding box').not.toBeNull() + 101 | expect(box!.width, 'canvas width').toBeGreaterThan(0) + 102 | expect(box!.height, 'canvas height').toBeGreaterThan(0) 103 | + 104 | expect(errors, `Console errors:\n${errors.join('\n')}`).toHaveLength(0) + 105 | }) + 106 | }) + 107 | ``` \ No newline at end of file diff --git a/src/packages/guide/src/components/climate-sim/ClimateSimDisplay.tsx b/src/packages/guide/src/components/climate-sim/ClimateSimDisplay.tsx index c390767b..ae1f5f0e 100644 --- a/src/packages/guide/src/components/climate-sim/ClimateSimDisplay.tsx +++ b/src/packages/guide/src/components/climate-sim/ClimateSimDisplay.tsx @@ -163,7 +163,9 @@ export function ClimateSimDisplay({ easterEggs }: ClimateSimDisplayProps): React // Ref mirror of activeScenarioId so the RAF tick closure always reads the current value // without the RAF effect needing activeScenarioId in its dependency array. const activeScenarioIdRef = useRef(activeScenarioId) - + // Tracks the last (scenarioId:seed:turns:bufferFrames) combo sent to the worker. + // Prevents the run effect from firing again on every workerScenarios state update. + const lastStartedRef = useRef('') useEffect(() => { pausedRef.current = paused }, [paused]) useEffect(() => { speedRef.current = simSpeed }, [simSpeed]) @@ -196,11 +198,16 @@ export function ClimateSimDisplay({ easterEggs }: ClimateSimDisplayProps): React } = useSimulationWorker() // ── kick off simulation when worker is ready or scenario changes ────── + // NOTE: workerScenarios is intentionally NOT in this dep array — it updates on every + // progress message and would cause an infinite restart loop. The ref guard ensures we + // send exactly one 'run' message per unique (scenarioId, seed, turns, bufferFrames) combo. useEffect(() => { if (!isReady) return - if (workerScenarios.get(activeScenarioId)?.bufferReady) return + const key = `${activeScenarioId}:${worldSeed}:${initialTurns}:${bufferFrames}` + if (lastStartedRef.current === key) return + lastStartedRef.current = key workerRun(activeScenarioId, initialTurns, bufferFrames, worldSeed) - }, [isReady, activeScenarioId, workerScenarios, workerRun, initialTurns, bufferFrames, worldSeed]) + }, [isReady, activeScenarioId, worldSeed, initialTurns, bufferFrames, workerRun]) // eslint-disable-line react-hooks/exhaustive-deps // ── request frame whenever turn changes (only after buffer is ready) ── const scenarioData = workerScenarios.get(activeScenarioId)