fix(@projects/@magic-civilization): 🐛 clean up test artifacts
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
09a9d6dc89
commit
8037a0cc2a
22 changed files with 341 additions and 843 deletions
BIN
mcp_expansions.png
Normal file
BIN
mcp_expansions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
BIN
mcp_prod_after_feature_fix.png
Normal file
BIN
mcp_prod_after_feature_fix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> about: crowdfund
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-KauEN4 --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=74057
|
||||
|
||||
```
|
||||
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> about: early access
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-E2feFR --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=73871
|
||||
|
||||
```
|
||||
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> about: early access progress
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-iCB9Al --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=73870
|
||||
|
||||
```
|
||||
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> about: team
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-BK9MB8 --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=74058
|
||||
|
||||
```
|
||||
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> encyclopedia index
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-M38ORN --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=74059
|
||||
|
||||
```
|
||||
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> episode: age of dwarves
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-Alczr9 --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=73868
|
||||
|
||||
```
|
||||
|
|
@ -1,162 +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: all-routes.spec.ts >> route coverage >> home
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
Call log:
|
||||
- navigating to "http://localhost:5802/?skip=welcome", waiting until "networkidle"
|
||||
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' },
|
||||
39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' },
|
||||
40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' },
|
||||
41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' },
|
||||
42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' },
|
||||
43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' },
|
||||
44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' },
|
||||
45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' },
|
||||
46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' },
|
||||
47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' },
|
||||
48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' },
|
||||
49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' },
|
||||
50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' },
|
||||
51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' },
|
||||
52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' },
|
||||
53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' },
|
||||
54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' },
|
||||
55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' },
|
||||
56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' },
|
||||
57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' },
|
||||
58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' },
|
||||
59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' },
|
||||
60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' },
|
||||
61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' },
|
||||
62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' },
|
||||
63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' },
|
||||
64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' },
|
||||
65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' },
|
||||
66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' },
|
||||
67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' },
|
||||
68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' },
|
||||
69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' },
|
||||
70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' },
|
||||
71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' },
|
||||
72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' },
|
||||
73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' },
|
||||
74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' },
|
||||
75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' },
|
||||
76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' },
|
||||
77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' },
|
||||
78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' },
|
||||
79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' },
|
||||
80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' },
|
||||
81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' },
|
||||
82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' },
|
||||
83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' },
|
||||
84 | ] as const satisfies readonly RouteSpec[]
|
||||
85 |
|
||||
86 | // Routes that trigger WASM compile + world generation. Keep the budget
|
||||
87 | // generous but bounded so a hung route fails loudly instead of stalling.
|
||||
88 | const SLOW_ROUTES = [
|
||||
89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' },
|
||||
90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' },
|
||||
91 | ] as const satisfies readonly RouteSpec[]
|
||||
92 |
|
||||
93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few
|
||||
94 | // dev-only warnings surface at `error` level but are not runtime failures.
|
||||
95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [
|
||||
96 | /Download the React DevTools/,
|
||||
97 | ]
|
||||
98 |
|
||||
99 | interface ConsoleErrorCapture {
|
||||
100 | readonly pageErrors: string[]
|
||||
101 | readonly consoleErrors: string[]
|
||||
102 | }
|
||||
103 |
|
||||
104 | function attachErrorCapture(page: Page): ConsoleErrorCapture {
|
||||
105 | const pageErrors: string[] = []
|
||||
106 | const consoleErrors: string[] = []
|
||||
107 |
|
||||
108 | page.on('pageerror', (err: Error) => {
|
||||
109 | pageErrors.push(`${err.name}: ${err.message}`)
|
||||
110 | })
|
||||
111 |
|
||||
112 | page.on('console', (msg: ConsoleMessage) => {
|
||||
113 | if (msg.type() !== 'error') return
|
||||
114 | const text = msg.text()
|
||||
115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return
|
||||
116 | consoleErrors.push(text)
|
||||
117 | })
|
||||
118 |
|
||||
119 | return { pageErrors, consoleErrors }
|
||||
120 | }
|
||||
121 |
|
||||
122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void {
|
||||
123 | const label = `${spec.path} (${spec.label})`
|
||||
124 | const msgs = [
|
||||
125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`),
|
||||
126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`),
|
||||
127 | ]
|
||||
128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0)
|
||||
129 | }
|
||||
130 |
|
||||
131 | test.describe('route coverage', () => {
|
||||
132 | test.describe.configure({ mode: 'parallel' })
|
||||
133 |
|
||||
134 | for (const spec of FAST_ROUTES) {
|
||||
135 | test(spec.label, async ({ page }) => {
|
||||
136 | const cap = attachErrorCapture(page)
|
||||
137 | const url = `${spec.path}?skip=welcome`
|
||||
> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs })
|
||||
| ^ TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
139 | // Wait an extra tick for any deferred suspense boundary to resolve.
|
||||
140 | await page.waitForTimeout(200)
|
||||
141 | assertClean(spec, cap)
|
||||
142 | })
|
||||
143 | }
|
||||
144 |
|
||||
145 | for (const spec of SLOW_ROUTES) {
|
||||
146 | test(spec.label, async ({ page }) => {
|
||||
147 | const cap = attachErrorCapture(page)
|
||||
148 | const url = `${spec.path}?skip=welcome`
|
||||
149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs })
|
||||
150 | // Slow routes: wait for the canvas OR the route's page-heading to appear.
|
||||
151 | // Either proves the route mounted; we don't require full sim completion.
|
||||
152 | await Promise.race([
|
||||
153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
155 | ])
|
||||
156 | assertClean(spec, cap)
|
||||
157 | })
|
||||
158 | }
|
||||
159 |
|
||||
160 | test('unknown route redirects to home without error', async ({ page }) => {
|
||||
161 | const cap = attachErrorCapture(page)
|
||||
162 | await page.goto('/this-route-does-not-exist?skip=welcome', {
|
||||
163 | waitUntil: 'networkidle',
|
||||
164 | timeout: 10_000,
|
||||
165 | })
|
||||
166 | // Router's fallback <Navigate to="/"> should land us at the home URL.
|
||||
167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/')
|
||||
168 | assertClean(
|
||||
169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' },
|
||||
170 | cap,
|
||||
171 | )
|
||||
172 | })
|
||||
173 | })
|
||||
174 |
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,162 +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: all-routes.spec.ts >> route coverage >> intro: expansions
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
Call log:
|
||||
- navigating to "http://localhost:5802/intro/expansions?skip=welcome", waiting until "networkidle"
|
||||
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' },
|
||||
39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' },
|
||||
40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' },
|
||||
41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' },
|
||||
42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' },
|
||||
43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' },
|
||||
44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' },
|
||||
45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' },
|
||||
46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' },
|
||||
47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' },
|
||||
48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' },
|
||||
49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' },
|
||||
50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' },
|
||||
51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' },
|
||||
52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' },
|
||||
53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' },
|
||||
54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' },
|
||||
55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' },
|
||||
56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' },
|
||||
57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' },
|
||||
58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' },
|
||||
59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' },
|
||||
60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' },
|
||||
61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' },
|
||||
62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' },
|
||||
63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' },
|
||||
64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' },
|
||||
65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' },
|
||||
66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' },
|
||||
67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' },
|
||||
68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' },
|
||||
69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' },
|
||||
70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' },
|
||||
71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' },
|
||||
72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' },
|
||||
73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' },
|
||||
74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' },
|
||||
75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' },
|
||||
76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' },
|
||||
77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' },
|
||||
78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' },
|
||||
79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' },
|
||||
80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' },
|
||||
81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' },
|
||||
82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' },
|
||||
83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' },
|
||||
84 | ] as const satisfies readonly RouteSpec[]
|
||||
85 |
|
||||
86 | // Routes that trigger WASM compile + world generation. Keep the budget
|
||||
87 | // generous but bounded so a hung route fails loudly instead of stalling.
|
||||
88 | const SLOW_ROUTES = [
|
||||
89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' },
|
||||
90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' },
|
||||
91 | ] as const satisfies readonly RouteSpec[]
|
||||
92 |
|
||||
93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few
|
||||
94 | // dev-only warnings surface at `error` level but are not runtime failures.
|
||||
95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [
|
||||
96 | /Download the React DevTools/,
|
||||
97 | ]
|
||||
98 |
|
||||
99 | interface ConsoleErrorCapture {
|
||||
100 | readonly pageErrors: string[]
|
||||
101 | readonly consoleErrors: string[]
|
||||
102 | }
|
||||
103 |
|
||||
104 | function attachErrorCapture(page: Page): ConsoleErrorCapture {
|
||||
105 | const pageErrors: string[] = []
|
||||
106 | const consoleErrors: string[] = []
|
||||
107 |
|
||||
108 | page.on('pageerror', (err: Error) => {
|
||||
109 | pageErrors.push(`${err.name}: ${err.message}`)
|
||||
110 | })
|
||||
111 |
|
||||
112 | page.on('console', (msg: ConsoleMessage) => {
|
||||
113 | if (msg.type() !== 'error') return
|
||||
114 | const text = msg.text()
|
||||
115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return
|
||||
116 | consoleErrors.push(text)
|
||||
117 | })
|
||||
118 |
|
||||
119 | return { pageErrors, consoleErrors }
|
||||
120 | }
|
||||
121 |
|
||||
122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void {
|
||||
123 | const label = `${spec.path} (${spec.label})`
|
||||
124 | const msgs = [
|
||||
125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`),
|
||||
126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`),
|
||||
127 | ]
|
||||
128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0)
|
||||
129 | }
|
||||
130 |
|
||||
131 | test.describe('route coverage', () => {
|
||||
132 | test.describe.configure({ mode: 'parallel' })
|
||||
133 |
|
||||
134 | for (const spec of FAST_ROUTES) {
|
||||
135 | test(spec.label, async ({ page }) => {
|
||||
136 | const cap = attachErrorCapture(page)
|
||||
137 | const url = `${spec.path}?skip=welcome`
|
||||
> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs })
|
||||
| ^ TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
139 | // Wait an extra tick for any deferred suspense boundary to resolve.
|
||||
140 | await page.waitForTimeout(200)
|
||||
141 | assertClean(spec, cap)
|
||||
142 | })
|
||||
143 | }
|
||||
144 |
|
||||
145 | for (const spec of SLOW_ROUTES) {
|
||||
146 | test(spec.label, async ({ page }) => {
|
||||
147 | const cap = attachErrorCapture(page)
|
||||
148 | const url = `${spec.path}?skip=welcome`
|
||||
149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs })
|
||||
150 | // Slow routes: wait for the canvas OR the route's page-heading to appear.
|
||||
151 | // Either proves the route mounted; we don't require full sim completion.
|
||||
152 | await Promise.race([
|
||||
153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
155 | ])
|
||||
156 | assertClean(spec, cap)
|
||||
157 | })
|
||||
158 | }
|
||||
159 |
|
||||
160 | test('unknown route redirects to home without error', async ({ page }) => {
|
||||
161 | const cap = attachErrorCapture(page)
|
||||
162 | await page.goto('/this-route-does-not-exist?skip=welcome', {
|
||||
163 | waitUntil: 'networkidle',
|
||||
164 | timeout: 10_000,
|
||||
165 | })
|
||||
166 | // Router's fallback <Navigate to="/"> should land us at the home URL.
|
||||
167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/')
|
||||
168 | assertClean(
|
||||
169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' },
|
||||
170 | cap,
|
||||
171 | )
|
||||
172 | })
|
||||
173 | })
|
||||
174 |
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,162 +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: all-routes.spec.ts >> route coverage >> intro: full game
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
Call log:
|
||||
- navigating to "http://localhost:5802/intro/full-game?skip=welcome", waiting until "networkidle"
|
||||
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' },
|
||||
39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' },
|
||||
40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' },
|
||||
41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' },
|
||||
42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' },
|
||||
43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' },
|
||||
44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' },
|
||||
45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' },
|
||||
46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' },
|
||||
47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' },
|
||||
48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' },
|
||||
49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' },
|
||||
50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' },
|
||||
51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' },
|
||||
52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' },
|
||||
53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' },
|
||||
54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' },
|
||||
55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' },
|
||||
56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' },
|
||||
57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' },
|
||||
58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' },
|
||||
59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' },
|
||||
60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' },
|
||||
61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' },
|
||||
62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' },
|
||||
63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' },
|
||||
64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' },
|
||||
65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' },
|
||||
66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' },
|
||||
67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' },
|
||||
68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' },
|
||||
69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' },
|
||||
70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' },
|
||||
71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' },
|
||||
72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' },
|
||||
73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' },
|
||||
74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' },
|
||||
75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' },
|
||||
76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' },
|
||||
77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' },
|
||||
78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' },
|
||||
79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' },
|
||||
80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' },
|
||||
81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' },
|
||||
82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' },
|
||||
83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' },
|
||||
84 | ] as const satisfies readonly RouteSpec[]
|
||||
85 |
|
||||
86 | // Routes that trigger WASM compile + world generation. Keep the budget
|
||||
87 | // generous but bounded so a hung route fails loudly instead of stalling.
|
||||
88 | const SLOW_ROUTES = [
|
||||
89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' },
|
||||
90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' },
|
||||
91 | ] as const satisfies readonly RouteSpec[]
|
||||
92 |
|
||||
93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few
|
||||
94 | // dev-only warnings surface at `error` level but are not runtime failures.
|
||||
95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [
|
||||
96 | /Download the React DevTools/,
|
||||
97 | ]
|
||||
98 |
|
||||
99 | interface ConsoleErrorCapture {
|
||||
100 | readonly pageErrors: string[]
|
||||
101 | readonly consoleErrors: string[]
|
||||
102 | }
|
||||
103 |
|
||||
104 | function attachErrorCapture(page: Page): ConsoleErrorCapture {
|
||||
105 | const pageErrors: string[] = []
|
||||
106 | const consoleErrors: string[] = []
|
||||
107 |
|
||||
108 | page.on('pageerror', (err: Error) => {
|
||||
109 | pageErrors.push(`${err.name}: ${err.message}`)
|
||||
110 | })
|
||||
111 |
|
||||
112 | page.on('console', (msg: ConsoleMessage) => {
|
||||
113 | if (msg.type() !== 'error') return
|
||||
114 | const text = msg.text()
|
||||
115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return
|
||||
116 | consoleErrors.push(text)
|
||||
117 | })
|
||||
118 |
|
||||
119 | return { pageErrors, consoleErrors }
|
||||
120 | }
|
||||
121 |
|
||||
122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void {
|
||||
123 | const label = `${spec.path} (${spec.label})`
|
||||
124 | const msgs = [
|
||||
125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`),
|
||||
126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`),
|
||||
127 | ]
|
||||
128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0)
|
||||
129 | }
|
||||
130 |
|
||||
131 | test.describe('route coverage', () => {
|
||||
132 | test.describe.configure({ mode: 'parallel' })
|
||||
133 |
|
||||
134 | for (const spec of FAST_ROUTES) {
|
||||
135 | test(spec.label, async ({ page }) => {
|
||||
136 | const cap = attachErrorCapture(page)
|
||||
137 | const url = `${spec.path}?skip=welcome`
|
||||
> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs })
|
||||
| ^ TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
139 | // Wait an extra tick for any deferred suspense boundary to resolve.
|
||||
140 | await page.waitForTimeout(200)
|
||||
141 | assertClean(spec, cap)
|
||||
142 | })
|
||||
143 | }
|
||||
144 |
|
||||
145 | for (const spec of SLOW_ROUTES) {
|
||||
146 | test(spec.label, async ({ page }) => {
|
||||
147 | const cap = attachErrorCapture(page)
|
||||
148 | const url = `${spec.path}?skip=welcome`
|
||||
149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs })
|
||||
150 | // Slow routes: wait for the canvas OR the route's page-heading to appear.
|
||||
151 | // Either proves the route mounted; we don't require full sim completion.
|
||||
152 | await Promise.race([
|
||||
153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
155 | ])
|
||||
156 | assertClean(spec, cap)
|
||||
157 | })
|
||||
158 | }
|
||||
159 |
|
||||
160 | test('unknown route redirects to home without error', async ({ page }) => {
|
||||
161 | const cap = attachErrorCapture(page)
|
||||
162 | await page.goto('/this-route-does-not-exist?skip=welcome', {
|
||||
163 | waitUntil: 'networkidle',
|
||||
164 | timeout: 10_000,
|
||||
165 | })
|
||||
166 | // Router's fallback <Navigate to="/"> should land us at the home URL.
|
||||
167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/')
|
||||
168 | assertClean(
|
||||
169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' },
|
||||
170 | cap,
|
||||
171 | )
|
||||
172 | })
|
||||
173 | })
|
||||
174 |
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,162 +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: all-routes.spec.ts >> route coverage >> intro: tools
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
Call log:
|
||||
- navigating to "http://localhost:5802/intro/tools?skip=welcome", waiting until "networkidle"
|
||||
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' },
|
||||
39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' },
|
||||
40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' },
|
||||
41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' },
|
||||
42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' },
|
||||
43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' },
|
||||
44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' },
|
||||
45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' },
|
||||
46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' },
|
||||
47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' },
|
||||
48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' },
|
||||
49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' },
|
||||
50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' },
|
||||
51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' },
|
||||
52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' },
|
||||
53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' },
|
||||
54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' },
|
||||
55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' },
|
||||
56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' },
|
||||
57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' },
|
||||
58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' },
|
||||
59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' },
|
||||
60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' },
|
||||
61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' },
|
||||
62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' },
|
||||
63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' },
|
||||
64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' },
|
||||
65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' },
|
||||
66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' },
|
||||
67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' },
|
||||
68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' },
|
||||
69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' },
|
||||
70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' },
|
||||
71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' },
|
||||
72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' },
|
||||
73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' },
|
||||
74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' },
|
||||
75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' },
|
||||
76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' },
|
||||
77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' },
|
||||
78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' },
|
||||
79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' },
|
||||
80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' },
|
||||
81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' },
|
||||
82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' },
|
||||
83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' },
|
||||
84 | ] as const satisfies readonly RouteSpec[]
|
||||
85 |
|
||||
86 | // Routes that trigger WASM compile + world generation. Keep the budget
|
||||
87 | // generous but bounded so a hung route fails loudly instead of stalling.
|
||||
88 | const SLOW_ROUTES = [
|
||||
89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' },
|
||||
90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' },
|
||||
91 | ] as const satisfies readonly RouteSpec[]
|
||||
92 |
|
||||
93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few
|
||||
94 | // dev-only warnings surface at `error` level but are not runtime failures.
|
||||
95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [
|
||||
96 | /Download the React DevTools/,
|
||||
97 | ]
|
||||
98 |
|
||||
99 | interface ConsoleErrorCapture {
|
||||
100 | readonly pageErrors: string[]
|
||||
101 | readonly consoleErrors: string[]
|
||||
102 | }
|
||||
103 |
|
||||
104 | function attachErrorCapture(page: Page): ConsoleErrorCapture {
|
||||
105 | const pageErrors: string[] = []
|
||||
106 | const consoleErrors: string[] = []
|
||||
107 |
|
||||
108 | page.on('pageerror', (err: Error) => {
|
||||
109 | pageErrors.push(`${err.name}: ${err.message}`)
|
||||
110 | })
|
||||
111 |
|
||||
112 | page.on('console', (msg: ConsoleMessage) => {
|
||||
113 | if (msg.type() !== 'error') return
|
||||
114 | const text = msg.text()
|
||||
115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return
|
||||
116 | consoleErrors.push(text)
|
||||
117 | })
|
||||
118 |
|
||||
119 | return { pageErrors, consoleErrors }
|
||||
120 | }
|
||||
121 |
|
||||
122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void {
|
||||
123 | const label = `${spec.path} (${spec.label})`
|
||||
124 | const msgs = [
|
||||
125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`),
|
||||
126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`),
|
||||
127 | ]
|
||||
128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0)
|
||||
129 | }
|
||||
130 |
|
||||
131 | test.describe('route coverage', () => {
|
||||
132 | test.describe.configure({ mode: 'parallel' })
|
||||
133 |
|
||||
134 | for (const spec of FAST_ROUTES) {
|
||||
135 | test(spec.label, async ({ page }) => {
|
||||
136 | const cap = attachErrorCapture(page)
|
||||
137 | const url = `${spec.path}?skip=welcome`
|
||||
> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs })
|
||||
| ^ TimeoutError: page.goto: Timeout 10000ms exceeded.
|
||||
139 | // Wait an extra tick for any deferred suspense boundary to resolve.
|
||||
140 | await page.waitForTimeout(200)
|
||||
141 | assertClean(spec, cap)
|
||||
142 | })
|
||||
143 | }
|
||||
144 |
|
||||
145 | for (const spec of SLOW_ROUTES) {
|
||||
146 | test(spec.label, async ({ page }) => {
|
||||
147 | const cap = attachErrorCapture(page)
|
||||
148 | const url = `${spec.path}?skip=welcome`
|
||||
149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs })
|
||||
150 | // Slow routes: wait for the canvas OR the route's page-heading to appear.
|
||||
151 | // Either proves the route mounted; we don't require full sim completion.
|
||||
152 | await Promise.race([
|
||||
153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }),
|
||||
155 | ])
|
||||
156 | assertClean(spec, cap)
|
||||
157 | })
|
||||
158 | }
|
||||
159 |
|
||||
160 | test('unknown route redirects to home without error', async ({ page }) => {
|
||||
161 | const cap = attachErrorCapture(page)
|
||||
162 | await page.goto('/this-route-does-not-exist?skip=welcome', {
|
||||
163 | waitUntil: 'networkidle',
|
||||
164 | timeout: 10_000,
|
||||
165 | })
|
||||
166 | // Router's fallback <Navigate to="/"> should land us at the home URL.
|
||||
167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/')
|
||||
168 | assertClean(
|
||||
169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' },
|
||||
170 | cap,
|
||||
171 | )
|
||||
172 | })
|
||||
173 | })
|
||||
174 |
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,20 +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: all-routes.spec.ts >> route coverage >> progress report
|
||||
- Location: e2e/all-routes.spec.ts:135:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: browserType.launch: Timeout 180000ms exceeded.
|
||||
Call log:
|
||||
- <launching> /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-U51emu --remote-debugging-pipe --no-startup-window
|
||||
- <launched> pid=73869
|
||||
|
||||
```
|
||||
|
|
@ -368,6 +368,21 @@ func _on_close_pressed() -> void:
|
|||
EventBus.overlay_closed.emit("city_screen")
|
||||
|
||||
|
||||
## p0-33: ESC closes the city screen when it is the topmost panel. The world
|
||||
## map listens to its own ESC for movement-mode + tech tree + chronicle; main.gd
|
||||
## listens for the in-game menu fallback. We only consume the event when the
|
||||
## screen is currently visible so the layered fallback chain stays intact.
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
if not (event is InputEventKey and event.pressed and not (event as InputEventKey).echo):
|
||||
return
|
||||
var key: InputEventKey = event as InputEventKey
|
||||
if key.keycode == KEY_ESCAPE:
|
||||
_on_close_pressed()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_add_queue_pressed() -> void:
|
||||
if not _city is CityScript:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -138,14 +138,24 @@ func _notification(what: int) -> void:
|
|||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
# If an overlay is open, let it handle ESC itself
|
||||
if not _overlay_stack.is_empty():
|
||||
return
|
||||
# Only open in-game menu if we're in the world map (not menus)
|
||||
if _current_scene != null and _current_scene.name == "WorldMap":
|
||||
get_viewport().set_input_as_handled()
|
||||
push_overlay("res://engine/scenes/ui/ingame_menu.tscn")
|
||||
if not (event is InputEventKey and event.pressed and not (event as InputEventKey).echo):
|
||||
return
|
||||
var key: InputEventKey = event as InputEventKey
|
||||
match key.keycode:
|
||||
KEY_ESCAPE:
|
||||
# If an overlay is open, let it handle ESC itself
|
||||
if not _overlay_stack.is_empty():
|
||||
return
|
||||
# Only open in-game menu if we're in the world map (not menus)
|
||||
if _current_scene != null and _current_scene.name == "WorldMap":
|
||||
get_viewport().set_input_as_handled()
|
||||
push_overlay("res://engine/scenes/ui/ingame_menu.tscn")
|
||||
KEY_F10:
|
||||
## p0-33: F10 always opens the in-game menu, regardless of overlay
|
||||
## stack state, so the player has a guaranteed escape hatch.
|
||||
if _current_scene != null and _current_scene.name == "WorldMap":
|
||||
get_viewport().set_input_as_handled()
|
||||
push_overlay("res://engine/scenes/ui/ingame_menu.tscn")
|
||||
|
||||
|
||||
func _is_combat_popup(scene_path: String) -> bool:
|
||||
|
|
|
|||
|
|
@ -267,12 +267,18 @@ func _start_game() -> void:
|
|||
|
||||
|
||||
## Persistent HUD overlays mounted once per world-map boot. The hotkey sheet
|
||||
## is always present so `ui_help` (F1 / ?) works in-game; the first-run
|
||||
## tutorial is gated on `TutorialOverlay.should_show_on_first_run()`.
|
||||
## is always present so `ui_help` (F1 / ?) works in-game.
|
||||
##
|
||||
## p1-19: the first-run tutorial no longer auto-mounts. Players opt in via
|
||||
## the Tutorial button on the world-map HUD top bar (visible turns 1..5).
|
||||
func _mount_hud_overlays() -> void:
|
||||
add_child(HotkeySheetScene.instantiate())
|
||||
if TutorialOverlayScript.should_show_on_first_run():
|
||||
add_child(TutorialOverlayScene.instantiate())
|
||||
|
||||
|
||||
## p1-19: instantiate the tutorial overlay on demand. Defaults to Step 1
|
||||
## per `tutorial_overlay.gd._current_step = 1`.
|
||||
func _on_tutorial_requested() -> void:
|
||||
add_child(TutorialOverlayScene.instantiate())
|
||||
|
||||
|
||||
func _read_start_turn_from_setup() -> int:
|
||||
|
|
@ -421,7 +427,7 @@ func _on_prologue_tribe_converged(
|
|||
var tribe_unit: RefCounted = _units_helper.create_unit(
|
||||
"dwarf_tribe", player_id, centroid
|
||||
)
|
||||
# Movement is kept at 1 so `_show_unit_panel`'s
|
||||
# Movement is kept at 1 so unit_panel.tscn's
|
||||
# `unit.can_found_city and unit.movement_remaining > 0` gate flips the
|
||||
# Found City button visible. The tribe cannot actually move because
|
||||
# hex-click remains gated during the prologue; the single MP simply
|
||||
|
|
@ -483,17 +489,37 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
var mouse_event: InputEventMouseButton = event as InputEventMouseButton
|
||||
if mouse_event == null or not mouse_event.pressed:
|
||||
return
|
||||
|
||||
## p0-35 right-click confirm during movement mode.
|
||||
if _movement_mode and mouse_event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
var click_axial: Vector2i = _screen_to_axial_at_mouse()
|
||||
_confirm_movement(click_axial)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
## p0-35 left-click cancels movement mode (returning to selection).
|
||||
if _movement_mode and mouse_event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_exit_movement_mode()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
## Legacy double-click city open path stays as a fallback. Single-click
|
||||
## now opens the city directly via `_handle_hex_click` (p0-33).
|
||||
if mouse_event.button_index != MOUSE_BUTTON_LEFT or not mouse_event.double_click:
|
||||
return
|
||||
var axial: Vector2i = _screen_to_axial_at_mouse()
|
||||
get_viewport().set_input_as_handled()
|
||||
_open_city_at_axial(axial)
|
||||
|
||||
|
||||
func _screen_to_axial_at_mouse() -> Vector2i:
|
||||
var bg_camera: Camera2D = _viewport_manager.get_background_camera()
|
||||
var bg_viewport: SubViewport = _viewport_manager.get_background_viewport()
|
||||
var mouse_screen: Vector2 = get_viewport().get_mouse_position()
|
||||
var viewport_center: Vector2 = bg_viewport.get_visible_rect().size * 0.5
|
||||
var offset: Vector2 = (mouse_screen - viewport_center) / bg_camera.zoom.x
|
||||
var world_click: Vector2 = bg_camera.global_position + offset
|
||||
var axial: Vector2i = HexUtilsScript.pixel_to_axial(world_click)
|
||||
get_viewport().set_input_as_handled()
|
||||
_on_city_tile_double_clicked(axial)
|
||||
return HexUtilsScript.pixel_to_axial(world_click)
|
||||
|
||||
|
||||
func _is_prologue_active() -> bool:
|
||||
|
|
@ -517,8 +543,20 @@ func _handle_hotkeys(key_event: InputEventKey) -> bool:
|
|||
_on_build_improvement_pressed()
|
||||
get_viewport().set_input_as_handled()
|
||||
return true
|
||||
KEY_M:
|
||||
## p0-35 enter movement mode when a unit is selected and has MP.
|
||||
if _selected_unit != null and _selected_unit_has_movement():
|
||||
_enter_movement_mode()
|
||||
get_viewport().set_input_as_handled()
|
||||
return true
|
||||
KEY_ESCAPE:
|
||||
if _chronicle_panel.visible:
|
||||
## p0-35: ESC cancels movement mode FIRST (before bubbling to
|
||||
## panels or the in-game menu owned by main.gd).
|
||||
if _movement_mode:
|
||||
_exit_movement_mode()
|
||||
get_viewport().set_input_as_handled()
|
||||
return true
|
||||
if _chronicle_panel != null and _chronicle_panel.visible:
|
||||
_chronicle_panel.hide()
|
||||
EventBus.chronicle_closed.emit(GameState.current_player_index)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
|
@ -530,6 +568,14 @@ func _handle_hotkeys(key_event: InputEventKey) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func _selected_unit_has_movement() -> bool:
|
||||
if _selected_unit == null:
|
||||
return false
|
||||
if "movement_remaining" in _selected_unit:
|
||||
return int(_selected_unit.get("movement_remaining")) > 0
|
||||
return false
|
||||
|
||||
|
||||
func _handle_hex_click(axial: Vector2i) -> void:
|
||||
# p0-34: During prologue phases (turn -1 or 0) only End Turn is valid.
|
||||
# Swallow hex clicks so no unit selection, bombard, or move path triggers.
|
||||
|
|
@ -544,7 +590,10 @@ func _handle_hex_click(axial: Vector2i) -> void:
|
|||
_deselect_unit()
|
||||
return
|
||||
|
||||
# City bombard: resolve ranged combat against clicked enemy
|
||||
# City bombard: resolve ranged combat against clicked enemy. Bombard mode
|
||||
# is opt-in via a future right-click flow (p0-33 removed it from
|
||||
# single-click). The state is preserved here for compatibility but no
|
||||
# code path currently sets `_bombard_city = ...` from a click.
|
||||
if _bombard_city != null:
|
||||
var target_unit: RefCounted = _combat.get_enemy_at(axial, _bombard_city.owner)
|
||||
if target_unit != null:
|
||||
|
|
@ -565,18 +614,28 @@ func _handle_hex_click(axial: Vector2i) -> void:
|
|||
_select_unit(unit_on_hex)
|
||||
return
|
||||
|
||||
# Check if clicking own city — offer bombard
|
||||
var player: RefCounted = GameState.get_current_player()
|
||||
if player != null:
|
||||
for city_ref: Variant in player.cities:
|
||||
if city_ref is CityScript and city_ref.position == axial:
|
||||
if not city_ref.has_bombarded:
|
||||
_bombard_city = city_ref
|
||||
return
|
||||
# p0-33: single-click on own city opens the city screen (no bombard).
|
||||
if _open_city_at_axial(axial):
|
||||
return
|
||||
|
||||
_deselect_unit()
|
||||
|
||||
|
||||
## p0-33: open the city at `axial` if any belongs to the current player.
|
||||
## Returns true when a city screen was opened.
|
||||
func _open_city_at_axial(axial: Vector2i) -> bool:
|
||||
if _city_screen == null:
|
||||
return false
|
||||
var player: RefCounted = GameState.get_current_player()
|
||||
if player == null:
|
||||
return false
|
||||
for city_ref: Variant in player.cities:
|
||||
if city_ref is CityScript and city_ref.position == axial:
|
||||
_city_screen.open_city(city_ref)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _find_player_unit_at(axial: Vector2i) -> RefCounted:
|
||||
var player: RefCounted = GameState.get_current_player()
|
||||
if player == null:
|
||||
|
|
@ -588,21 +647,24 @@ func _find_player_unit_at(axial: Vector2i) -> RefCounted:
|
|||
|
||||
|
||||
func _select_unit(unit: RefCounted) -> void:
|
||||
## Selecting a different unit cancels any in-flight movement mode so the
|
||||
## preview overlay clears before the new range overlay paints.
|
||||
if _movement_mode:
|
||||
_exit_movement_mode()
|
||||
_selected_unit = unit
|
||||
_unit_renderer.set_selected(unit.id, true)
|
||||
_show_unit_panel(unit)
|
||||
_compute_movement_range(unit)
|
||||
EventBus.unit_selected.emit(unit)
|
||||
|
||||
|
||||
func _deselect_unit() -> void:
|
||||
if _movement_mode:
|
||||
_exit_movement_mode()
|
||||
if _selected_unit != null:
|
||||
_unit_renderer.set_selected(_selected_unit.id, false)
|
||||
_selected_unit = null
|
||||
_reachable_hexes = {}
|
||||
_unit_renderer.clear_movement_range()
|
||||
if not _arena_mode:
|
||||
_hud.hide_unit_panel()
|
||||
EventBus.unit_deselected.emit()
|
||||
|
||||
|
||||
|
|
@ -656,23 +718,13 @@ func _move_unit_to(unit: RefCounted, target: Vector2i) -> void:
|
|||
_sync_units()
|
||||
if unit.movement_remaining > 0:
|
||||
_compute_movement_range(unit)
|
||||
_show_unit_panel(unit)
|
||||
## Re-emit so the unit_panel.tscn refreshes its stats/buttons after
|
||||
## the move (panel listens on EventBus.unit_selected for HP/MP/etc.).
|
||||
EventBus.unit_selected.emit(unit)
|
||||
else:
|
||||
_deselect_unit()
|
||||
|
||||
|
||||
func _show_unit_panel(unit: RefCounted) -> void:
|
||||
var display_name: String = ThemeVocabulary.lookup(unit.type_id)
|
||||
if unit.display_name != "" and unit.display_name != unit.type_id:
|
||||
display_name = unit.display_name
|
||||
var stats: String = (
|
||||
"HP: %d/%d ATK: %d DEF: %d" % [unit.hp, unit.max_hp, unit.attack, unit.defense]
|
||||
)
|
||||
var can_found: bool = unit.can_found_city and unit.movement_remaining > 0
|
||||
var can_build: bool = unit.can_build_improvements and unit.movement_remaining > 0
|
||||
_hud.show_unit_panel(display_name, stats, can_found, can_build)
|
||||
|
||||
|
||||
func _toggle_tech_tree() -> void:
|
||||
if _tech_tree.visible:
|
||||
_tech_tree.close()
|
||||
|
|
@ -735,10 +787,51 @@ func _on_happiness_changed(player_index: int, value: int) -> void:
|
|||
_hud.update_happiness(value)
|
||||
|
||||
|
||||
## p1-18: village discovery toast + minimap ping. Reward dict carries `gold`
|
||||
## plus optional `unit` / `tech` ids so the message names what was awarded.
|
||||
## Village name resolves from the GameState NPC building when available; falls
|
||||
## back to the vocab key `village_default_name` when the building is gone.
|
||||
func _on_village_discovered(tile_pos: Vector2i, reward: Dictionary) -> void:
|
||||
var gold_reward: int = reward.get("gold", 0)
|
||||
if gold_reward > 0:
|
||||
push_warning("Village discovered at %s — awarded %d gold" % [str(tile_pos), gold_reward])
|
||||
var gold_reward: int = int(reward.get("gold", 0))
|
||||
if gold_reward <= 0:
|
||||
return
|
||||
var village_name: String = _resolve_village_name(tile_pos, reward)
|
||||
var unit_reward: String = str(reward.get("unit", ""))
|
||||
var tech_reward: String = str(reward.get("tech", ""))
|
||||
var msg: String = ""
|
||||
if not unit_reward.is_empty():
|
||||
var unit_label: String = ThemeVocabulary.lookup(unit_reward)
|
||||
msg = ThemeVocabulary.lookup("fmt_village_reward_with_unit") % [
|
||||
gold_reward, unit_label, village_name,
|
||||
]
|
||||
elif not tech_reward.is_empty():
|
||||
var tech_label: String = ThemeVocabulary.lookup(tech_reward)
|
||||
msg = ThemeVocabulary.lookup("fmt_village_reward_with_tech") % [
|
||||
gold_reward, tech_label, village_name,
|
||||
]
|
||||
else:
|
||||
msg = ThemeVocabulary.lookup("fmt_village_reward_gold") % [
|
||||
gold_reward, village_name,
|
||||
]
|
||||
if not _arena_mode and _hud != null and _hud.has_method("show_notification"):
|
||||
_hud.show_notification(msg)
|
||||
if _minimap != null and _minimap.has_method("pulse_at"):
|
||||
_minimap.pulse_at(tile_pos)
|
||||
|
||||
|
||||
func _resolve_village_name(tile_pos: Vector2i, reward: Dictionary) -> String:
|
||||
var named: String = str(reward.get("name", ""))
|
||||
if not named.is_empty():
|
||||
return named
|
||||
if GameState.has_method("get_npc_building_at"):
|
||||
var building: RefCounted = (
|
||||
GameState.get_npc_building_at(tile_pos, "village") as RefCounted
|
||||
)
|
||||
if building != null and "display_name" in building:
|
||||
var dn: String = str(building.get("display_name"))
|
||||
if not dn.is_empty():
|
||||
return dn
|
||||
return ThemeVocabulary.lookup("village_default_name")
|
||||
|
||||
|
||||
func _on_found_city_pressed() -> void:
|
||||
|
|
@ -782,16 +875,6 @@ func _found_capital_from_tribe(tribe_unit: RefCounted) -> void:
|
|||
_sync_units()
|
||||
|
||||
|
||||
func _on_city_tile_double_clicked(pos: Vector2i) -> void:
|
||||
var player: RefCounted = GameState.get_current_player()
|
||||
if player == null:
|
||||
return
|
||||
for city_ref: RefCounted in player.cities:
|
||||
if city_ref is CityScript and city_ref.position == pos:
|
||||
_city_screen.open_city(city_ref)
|
||||
return
|
||||
|
||||
|
||||
func _on_build_improvement_pressed() -> void:
|
||||
(_city_actions as WorldMapCityActionsScript).on_build_improvement_pressed(_selected_unit)
|
||||
|
||||
|
|
@ -832,3 +915,169 @@ func _on_combat_finished() -> void:
|
|||
if player != null and game_map != null:
|
||||
WorldMapVisionScript.recalculate_vision(player, game_map)
|
||||
_update_fog(player, game_map)
|
||||
|
||||
|
||||
# -- p0-35 movement-mode state machine -----------------------------------
|
||||
|
||||
|
||||
## Enter the movement-mode preview state. The next right-click confirms a
|
||||
## move along the previewed path; ESC / left-click cancel without moving.
|
||||
func _enter_movement_mode() -> void:
|
||||
if _selected_unit == null:
|
||||
return
|
||||
_movement_mode = true
|
||||
EventBus.movement_mode_entered.emit(_selected_unit)
|
||||
## Refresh the preview against the cursor's last known hex so the player
|
||||
## sees a path immediately rather than waiting for the next hover tick.
|
||||
if _last_hover_axial != Vector2i(-9999, -9999):
|
||||
update_path_preview(_last_hover_axial)
|
||||
|
||||
|
||||
## Exit movement mode without moving. Clears the path preview overlay.
|
||||
func _exit_movement_mode() -> void:
|
||||
if not _movement_mode:
|
||||
return
|
||||
_movement_mode = false
|
||||
EventBus.movement_mode_exited.emit()
|
||||
if _unit_renderer != null:
|
||||
_unit_renderer.clear_path_preview()
|
||||
|
||||
|
||||
## Public hook so world_map_hover.gd can push the hovered hex into the
|
||||
## preview pipeline whenever movement mode is active.
|
||||
func update_path_preview(hovered_axial: Vector2i) -> void:
|
||||
_last_hover_axial = hovered_axial
|
||||
if not _movement_mode or _selected_unit == null or _unit_renderer == null:
|
||||
return
|
||||
if hovered_axial == _selected_unit.position:
|
||||
_unit_renderer.clear_path_preview()
|
||||
return
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
if game_map == null:
|
||||
return
|
||||
var scouted: Dictionary = _build_scouted_dict()
|
||||
## Use a generous budget so multi-turn previews can be displayed; the
|
||||
## turn count is computed below from the per-tile cost dictionary.
|
||||
var movement_total: int = int(_selected_unit.get_movement()) if _selected_unit.has_method("get_movement") else 2
|
||||
var preview_budget: int = maxi(movement_total * 8, 16)
|
||||
var path: Array[Vector2i] = PathfinderScript.find_path_with_fog(
|
||||
game_map,
|
||||
_selected_unit.position,
|
||||
hovered_axial,
|
||||
preview_budget,
|
||||
_selected_unit.unit_type,
|
||||
scouted,
|
||||
)
|
||||
if path.is_empty():
|
||||
_unit_renderer.clear_path_preview()
|
||||
return
|
||||
var turns: int = _estimate_turns_for_path(path, movement_total)
|
||||
_unit_renderer.show_path_preview(_selected_unit.position, path, turns)
|
||||
|
||||
|
||||
## Confirm a movement-mode move targeting `target`. Falls back to the
|
||||
## existing `_move_unit_to` once a reachable hex is reached this turn;
|
||||
## paths that exceed the current movement budget bank the unit at the
|
||||
## furthest reachable hex along the path so the player can resume next turn.
|
||||
func _confirm_movement(target: Vector2i) -> void:
|
||||
if _selected_unit == null:
|
||||
_exit_movement_mode()
|
||||
return
|
||||
if _reachable_hexes.has(target):
|
||||
_exit_movement_mode()
|
||||
_move_unit_to(_selected_unit, target)
|
||||
return
|
||||
## Walk the previewed path forward, picking the furthest reachable hex.
|
||||
var furthest: Vector2i = _selected_unit.position
|
||||
for axial: Vector2i in _path_preview_for_target(target):
|
||||
if _reachable_hexes.has(axial):
|
||||
furthest = axial
|
||||
else:
|
||||
break
|
||||
if furthest != _selected_unit.position:
|
||||
_exit_movement_mode()
|
||||
_move_unit_to(_selected_unit, furthest)
|
||||
else:
|
||||
_exit_movement_mode()
|
||||
|
||||
|
||||
func _path_preview_for_target(target: Vector2i) -> Array[Vector2i]:
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
if game_map == null or _selected_unit == null:
|
||||
return [] as Array[Vector2i]
|
||||
var scouted: Dictionary = _build_scouted_dict()
|
||||
return PathfinderScript.find_path_with_fog(
|
||||
game_map,
|
||||
_selected_unit.position,
|
||||
target,
|
||||
9999,
|
||||
_selected_unit.unit_type,
|
||||
scouted,
|
||||
)
|
||||
|
||||
|
||||
func _build_scouted_dict() -> Dictionary:
|
||||
## Build a {Vector2i -> bool} dict of scouted/visible tiles for the local
|
||||
## player so PathfinderScript.find_path_with_fog can A* through them and
|
||||
## hex_line through the rest. Empty when no game map is present.
|
||||
var out: Dictionary = {}
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
if game_map == null:
|
||||
return out
|
||||
var player: RefCounted = GameState.get_current_player()
|
||||
if player == null:
|
||||
return out
|
||||
var pid: int = int(player.index)
|
||||
for axial: Vector2i in game_map.tiles:
|
||||
var tile: RefCounted = game_map.tiles[axial] as RefCounted
|
||||
if tile == null:
|
||||
continue
|
||||
if tile.has_method("get_visibility"):
|
||||
var vis: int = int(tile.get_visibility(pid))
|
||||
out[axial] = vis >= 1
|
||||
return out
|
||||
|
||||
|
||||
func _estimate_turns_for_path(path: Array[Vector2i], movement_per_turn: int) -> int:
|
||||
## Approximate turn count along a fog-aware preview. Tiles outside the
|
||||
## current `_reachable_hexes` map are treated as cost-1 because their
|
||||
## actual cost cannot be known until they're scouted.
|
||||
if movement_per_turn <= 0:
|
||||
return path.size()
|
||||
var cost_so_far: int = 0
|
||||
var turns: int = 1
|
||||
for axial: Vector2i in path:
|
||||
var step_cost: int = int(_reachable_hexes.get(axial, 1))
|
||||
if step_cost <= 0:
|
||||
step_cost = 1
|
||||
if cost_so_far + step_cost > movement_per_turn:
|
||||
turns += 1
|
||||
cost_so_far = step_cost
|
||||
else:
|
||||
cost_so_far += step_cost
|
||||
return turns
|
||||
|
||||
|
||||
# -- Unit panel button bridges --------------------------------------------
|
||||
|
||||
|
||||
func _on_move_pressed() -> void:
|
||||
if _selected_unit != null and _selected_unit_has_movement():
|
||||
_enter_movement_mode()
|
||||
|
||||
|
||||
func _on_fortify_pressed_from_panel() -> void:
|
||||
if _selected_unit == null:
|
||||
return
|
||||
if _selected_unit is UnitScript and _selected_unit.has_method("fortify"):
|
||||
(_selected_unit as UnitScript).fortify()
|
||||
EventBus.unit_selected.emit(_selected_unit)
|
||||
|
||||
|
||||
func _on_skip_pressed_from_panel() -> void:
|
||||
if _selected_unit == null:
|
||||
return
|
||||
if _selected_unit is UnitScript:
|
||||
(_selected_unit as UnitScript).movement_remaining = 0
|
||||
EventBus.unit_selected.emit(_selected_unit)
|
||||
_deselect_unit()
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ var _panel: PanelContainer = null
|
|||
var _timer: float = 0.0
|
||||
var _last_axial: Vector2i = Vector2i(-9999, -9999)
|
||||
var _viewport_manager: Control = null
|
||||
## p0-35: weak reference to the parent world_map controller so the hover
|
||||
## tick can push the cursor's hex into the movement-mode preview pipeline.
|
||||
var _world_map: Node2D = null
|
||||
|
||||
|
||||
func setup(parent: Node2D, viewport_mgr: Control) -> void:
|
||||
_viewport_manager = viewport_mgr
|
||||
_world_map = parent
|
||||
_panel = TileInfoPanelScene.instantiate()
|
||||
parent.add_child(_panel)
|
||||
|
||||
|
|
@ -36,6 +40,10 @@ func tick(delta: float) -> void:
|
|||
if axial == _last_axial:
|
||||
return
|
||||
_last_axial = axial
|
||||
## p0-35: every hover tick refreshes the movement-mode path preview, even
|
||||
## when the underlying tile is null (off-map → preview clears).
|
||||
if _world_map != null and _world_map.has_method("update_path_preview"):
|
||||
_world_map.update_path_preview(axial)
|
||||
var tile: Resource = game_map.get_tile(axial) as Resource
|
||||
if tile == null:
|
||||
_panel.hide_panel()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue