Standardize browser QA at 1920x1080
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
- Do not copy secrets from that `.env` file into tracked files, logs, or responses.
|
- Do not copy secrets from that `.env` file into tracked files, logs, or responses.
|
||||||
- For this project, whenever source changes are made, commit the changes and push them to the Gitea remote before finishing the task.
|
- For this project, whenever source changes are made, commit the changes and push them to the Gitea remote before finishing the task.
|
||||||
- The game targets normal operation on PC/desktop browsers; do not prioritize mobile layout or mobile-specific work.
|
- The game targets normal operation on PC/desktop browsers; do not prioritize mobile layout or mobile-specific work.
|
||||||
|
- All browser, screenshot, and visual QA must explicitly use a 1920x1080 CSS viewport at 100% browser zoom. Never rely on an in-app browser's ambient/default viewport. Additional desktop resolutions are secondary checks and do not replace the 1920x1080 baseline.
|
||||||
- 이미지는 KOEI 삼국지 스타일을 활용하되, 실제 저작권 원본 이미지나 로고를 복제하지 않고 프로젝트 고유 자산으로 제작한다.
|
- 이미지는 KOEI 삼국지 스타일을 활용하되, 실제 저작권 원본 이미지나 로고를 복제하지 않고 프로젝트 고유 자산으로 제작한다.
|
||||||
- 캠페인 큰 줄기는 유비의 삼국지 서사를 따른다: 황건적 토벌 → 반동탁 흐름 → 공손찬 의탁 → 도겸에게 서주 인수 → 여포에게 서주 상실 → 조조 의탁 → 조조 이탈 → 원소 의탁 → 유표 의탁 → 제갈량 영입 → 적벽대전 → 형주·익주 확보 → 촉한 건국.
|
- 캠페인 큰 줄기는 유비의 삼국지 서사를 따른다: 황건적 토벌 → 반동탁 흐름 → 공손찬 의탁 → 도겸에게 서주 인수 → 여포에게 서주 상실 → 조조 의탁 → 조조 이탈 → 원소 의탁 → 유표 의탁 → 제갈량 영입 → 적벽대전 → 형주·익주 확보 → 촉한 건국.
|
||||||
- 게임이 진행되며 무장이 추가될수록 출전 전에 함께할 무장을 선택하는 편성 재미가 드러나도록 구현한다.
|
- 게임이 진행되며 무장이 추가될수록 출전 전에 함께할 무장을 선택하는 편성 재미가 드러나도록 구현한다.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Raise the first battle map from flat placeholder-like terrain to an original han
|
|||||||
|
|
||||||
- Unit sprites must read before the map.
|
- Unit sprites must read before the map.
|
||||||
- Terrain contrast stays one step below characters, combat UI, and hover/selection feedback.
|
- Terrain contrast stays one step below characters, combat UI, and hover/selection feedback.
|
||||||
- Texture detail should add depth without turning into visual noise at PC browser 100% scale.
|
- Texture detail should add depth without turning into visual noise at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1.
|
||||||
- Terrain identity must remain clear: road, forest, hill, river, village, camp, fort.
|
- Terrain identity must remain clear: road, forest, hill, river, village, camp, fort.
|
||||||
|
|
||||||
## Terrain Palette
|
## Terrain Palette
|
||||||
@@ -35,4 +35,4 @@ Raise the first battle map from flat placeholder-like terrain to an original han
|
|||||||
- First battle lower-left camp and starting road should guide the eye toward the central route.
|
- First battle lower-left camp and starting road should guide the eye toward the central route.
|
||||||
- Central village and river should be readable without stealing focus.
|
- Central village and river should be readable without stealing focus.
|
||||||
- Enemy fort area in the upper-right must imply an enemy line without becoming a large UI-like block.
|
- Enemy fort area in the upper-right must imply an enemy line without becoming a large UI-like block.
|
||||||
- At 100% PC browser scale, Liu Bei, Guan Yu, Zhang Fei, and enemy units must remain the strongest silhouettes.
|
- At an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, Liu Bei, Guan Yu, Zhang Fei, and enemy units must remain the strongest silhouettes. Follow `docs/desktop-browser-qa-standard.md` for capture verification.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The first battle uses `src/assets/images/battle/first-battle-map-v2.webp` as the
|
|||||||
|
|
||||||
## Art Direction
|
## Art Direction
|
||||||
|
|
||||||
- Units must read before the terrain at 100% desktop browser scale.
|
- Units must read before the terrain at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1.
|
||||||
- Terrain uses muted greens, ochre roads, dark blue-green water, and compact structure markers.
|
- Terrain uses muted greens, ochre roads, dark blue-green water, and compact structure markers.
|
||||||
- Roads, forests, rivers, camps, villages, cliffs, and forts should be recognizable by silhouette and texture, not by high saturation.
|
- Roads, forests, rivers, camps, villages, cliffs, and forts should be recognizable by silhouette and texture, not by high saturation.
|
||||||
- The grid remains subtle and embedded in the paint grade, so it helps targeting without becoming the main visual layer.
|
- The grid remains subtle and embedded in the paint grade, so it helps targeting without becoming the main visual layer.
|
||||||
@@ -314,4 +314,4 @@ python scripts/generate-handpaint-map-sample.py --map thirtieth --tile-size 192
|
|||||||
- `docs/sixty-sixth-battle-map-handpaint-before-after.png`
|
- `docs/sixty-sixth-battle-map-handpaint-before-after.png`
|
||||||
- `docs/sixty-sixth-battle-map-handpaint-preview.png`
|
- `docs/sixty-sixth-battle-map-handpaint-preview.png`
|
||||||
|
|
||||||
Browser screenshots are saved per verification run after build/deploy.
|
Browser screenshots are saved per verification run after build/deploy at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`. Larger 2560-class checks are supplemental and do not replace the FHD evidence.
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
## 로컬 검증
|
## 로컬 검증
|
||||||
|
|
||||||
- 빌드: `pnpm build` 성공
|
- 빌드: `pnpm build` 성공
|
||||||
- 브라우저: PC 1280x720 인앱 브라우저, `debugBattleSetup=attack-preview`
|
- 기존 캡처: PC 1280x720 인앱 브라우저에서 생성된 PNG는 당시 세션의 증적으로 유지하며 FHD 캡처로 재표기하거나 확대하지 않는다. `debugBattleSetup=attack-preview`
|
||||||
|
- 현재 기준 재검증(2026-07-17): 명시적 1920x1080 CSS viewport, 브라우저 확대 100%, DPR 1에서 공격·책략 예측 화면이 통과했고 콘솔 오류가 없었다.
|
||||||
- 공격 예측: `docs/battle-preview-ui-rework-local-combat-preview.png`
|
- 공격 예측: `docs/battle-preview-ui-rework-local-combat-preview.png`
|
||||||
- 책략 예측: `docs/battle-preview-ui-rework-local-strategy-preview.png`
|
- 책략 예측: `docs/battle-preview-ui-rework-local-strategy-preview.png`
|
||||||
- 콘솔 에러: 없음
|
- 콘솔 에러: 없음
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ The atlas remains a 128px-frame spritesheet with the existing 5x6 layout. New al
|
|||||||
|
|
||||||
- Build: `pnpm build` passed.
|
- Build: `pnpm build` passed.
|
||||||
- Local browser: `http://127.0.0.1:4190/heros_web/?debug&debugBattleSetup=attack-preview&v=hq-icons-batch1-local`
|
- Local browser: `http://127.0.0.1:4190/heros_web/?debug&debugBattleSetup=attack-preview&v=hq-icons-batch1-local`
|
||||||
- Checked unit detail, strategy menu, command menu, and combat preview at desktop 1280x720.
|
- Historical capture note: the linked local and deployed PNGs preserve the original desktop 1280x720 session and have not been relabeled or upscaled.
|
||||||
- Console errors: none observed during local browser check.
|
- Current-standard revalidation (2026-07-17): unit detail, strategy menu, command menu, and combat preview passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1.
|
||||||
|
- Console errors: none observed during the historical capture or current-standard revalidation.
|
||||||
- Deploy: `pnpm deploy:nas` passed and deployed to `/volume1/web/heros_web`.
|
- Deploy: `pnpm deploy:nas` passed and deployed to `/volume1/web/heros_web`.
|
||||||
- Deployed browser: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=hq-icons-batch1-prod`
|
- Deployed browser: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=hq-icons-batch1-prod`
|
||||||
- Deployed console errors: none observed during combat preview check.
|
- Deployed console errors: none observed during combat preview check.
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ The existing 5x6 atlas contract is preserved. Pixel comparison against the previ
|
|||||||
- `pnpm run verify:static-data` passed.
|
- `pnpm run verify:static-data` passed.
|
||||||
- `pnpm run build` passed.
|
- `pnpm run build` passed.
|
||||||
- `pnpm run verify:release` passed, including new-game, reload/continue, defeat retry, victory, and camp-return flows.
|
- `pnpm run verify:release` passed, including new-game, reload/continue, defeat retry, victory, and camp-return flows.
|
||||||
- Desktop browser checks passed at 1280x720 for the command menu, battle item menu, target selection, combat preview, sortie supply assignment, camp supply rows, and merchant rows.
|
- Desktop browser checks passed on 2026-07-17 at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1 for the command menu, battle item menu, target selection, combat preview, sortie supply assignment, camp supply rows, and merchant rows.
|
||||||
- Both atlases loaded successfully and no browser console errors were observed.
|
- Both atlases loaded successfully and no browser console errors were observed.
|
||||||
- Independent read-only review found no remaining blockers.
|
- Independent read-only review found no remaining blockers.
|
||||||
|
|||||||
52
docs/desktop-browser-qa-standard.md
Normal file
52
docs/desktop-browser-qa-standard.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Desktop Browser QA Standard
|
||||||
|
|
||||||
|
## Primary Baseline
|
||||||
|
|
||||||
|
All routine desktop browser QA, visual review, screenshots, and release evidence use these explicit settings:
|
||||||
|
|
||||||
|
- CSS viewport: `1920x1080`.
|
||||||
|
- Browser zoom: `100%`.
|
||||||
|
- Device pixel ratio: `1`.
|
||||||
|
- Desktop layout only; a different viewport is permitted only for a separately identified responsive or compatibility test.
|
||||||
|
|
||||||
|
Do not rely on the browser window's default size. Set the CSS viewport before opening the target flow, then confirm the values below before accepting a screenshot or pass result.
|
||||||
|
|
||||||
|
## Required Runtime Checks
|
||||||
|
|
||||||
|
Record or assert the following values for the primary pass:
|
||||||
|
|
||||||
|
- `window.innerWidth === 1920`.
|
||||||
|
- `window.innerHeight === 1080`.
|
||||||
|
- `window.devicePixelRatio === 1`.
|
||||||
|
- The game canvas backing surface is `canvas.width === 1920` and `canvas.height === 1080`.
|
||||||
|
- At the primary viewport, `canvas.getBoundingClientRect()` is approximately `1920x1080` and the canvas is centered without clipping.
|
||||||
|
- The active Phaser game size and scene bounds remain `1920x1080`.
|
||||||
|
- Browser console warning/error count is recorded, with unexpected errors treated as a failed check.
|
||||||
|
|
||||||
|
Capture the URL, renderer mode, checked flow, and date in the report. A report should state `1920x1080 CSS viewport, 100% browser zoom, DPR 1` rather than only saying `desktop` or `PC browser`.
|
||||||
|
|
||||||
|
## Supplemental 2560 Checks
|
||||||
|
|
||||||
|
Use `2560x1440` or `2560x1600` only as supplemental fit, centering, and large-monitor coverage checks. These checks verify that the fixed FHD game surface remains correctly contained and centered; they do not replace the primary `1920x1080` evidence.
|
||||||
|
|
||||||
|
## Legacy Authored Coordinates
|
||||||
|
|
||||||
|
Battle and camp UI still contain an authored legacy `1280x720` coordinate system that is scaled by `1.5` onto the FHD game surface. Constants and comments such as `battleReferenceWidth`, `battleReferenceHeight`, `campLegacyCanvasWidth`, `campLegacyCanvasHeight`, and legacy click-coordinate scaling describe this internal layout basis.
|
||||||
|
|
||||||
|
Do not change those values to `1920x1080` as part of viewport QA standardization. Doing so would double-scale or displace existing UI. Browser viewport settings and authored scene coordinates are separate concerns.
|
||||||
|
|
||||||
|
## Historical Evidence
|
||||||
|
|
||||||
|
Existing `1280x720` screenshots remain valid evidence of the session in which they were captured. Do not relabel, resize, or present them as FHD captures. When a historical report needs current validation:
|
||||||
|
|
||||||
|
1. Keep the original screenshot and identify it as historical evidence.
|
||||||
|
2. Run a separate current-standard pass at `1920x1080` CSS viewport, `100%` browser zoom, and DPR `1`.
|
||||||
|
3. Record the new date, checked surfaces, and console result without rewriting what the old image represents.
|
||||||
|
|
||||||
|
This policy applies to local, deployed, Canvas, and WebGL browser checks.
|
||||||
|
|
||||||
|
## Automation Contract
|
||||||
|
|
||||||
|
The shared automation source is `scripts/desktop-browser-viewport.mjs`. Browser-based QA scripts must use its context options rather than embedding another primary viewport.
|
||||||
|
|
||||||
|
Run `pnpm run verify:desktop-viewport` to confirm the FHD viewport, DPR 1 context, Phaser game size, project policy, and browser-check imports. This gate is also included in `pnpm run verify:static-data` so an omitted or changed baseline fails routine verification.
|
||||||
@@ -29,6 +29,7 @@ Generated after the accessory icon rework in `src/game/scenes/BootScene.ts`.
|
|||||||

|

|
||||||
|
|
||||||
- URL checked: `http://127.0.0.1:4200/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
- URL checked: `http://127.0.0.1:4200/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
||||||
- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated accessory textures in roster, slot, and detail surfaces.
|
- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled.
|
||||||
- Browser console warnings/errors: none observed.
|
- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated accessory textures visible in the roster, slot, and detail surfaces.
|
||||||
|
- Browser console warnings/errors: none observed during the historical capture or current-standard revalidation.
|
||||||
- Follow-up target from the updated audit: remaining low-detail equipment icons.
|
- Follow-up target from the updated audit: remaining low-detail equipment icons.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Generated after the armor icon rework in `src/game/scenes/BootScene.ts`.
|
|||||||

|

|
||||||
|
|
||||||
- URL checked: `http://127.0.0.1:4198/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
- URL checked: `http://127.0.0.1:4198/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
||||||
- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated armor textures in roster, slot, and detail surfaces.
|
- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled.
|
||||||
- Browser console warnings/errors: none observed.
|
- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated armor textures visible in the roster, slot, and detail surfaces.
|
||||||
|
- Browser console warnings/errors: none observed during the historical capture or current-standard revalidation.
|
||||||
- Follow-up target from the updated audit: accessory charms and tokens.
|
- Follow-up target from the updated audit: accessory charms and tokens.
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ Generated by `scripts/audit-equipment-icon-quality.mjs`.
|
|||||||
- Texture size: 64px
|
- Texture size: 64px
|
||||||
- Drawing grid: 28 units scaled 2.29x into the texture
|
- Drawing grid: 28 units scaled 2.29x into the texture
|
||||||
- Runtime item icon scene: `src/game/scenes/CampScene.ts`
|
- Runtime item icon scene: `src/game/scenes/CampScene.ts`
|
||||||
- Observed runtime display sizes: `14px`, `20px`, `21px`, `22px`, `30px`
|
- Observed runtime display sizes: `14px`
|
||||||
- Direct item texture render sites found: 5
|
- Direct item texture render sites found: 5
|
||||||
|
- Browser visual QA baseline: `1920x1080` CSS viewport at 100% zoom
|
||||||
|
|
||||||
## Main Findings
|
## Main Findings
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Generated after the final low-detail weapon icon rework in `src/game/scenes/Boot
|
|||||||

|

|
||||||
|
|
||||||
- URL checked: `http://127.0.0.1:4201/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
- URL checked: `http://127.0.0.1:4201/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
||||||
- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated weapon textures in roster, slot, and detail surfaces.
|
- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled.
|
||||||
- Browser console warnings/errors: none observed.
|
- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated weapon textures visible in the roster, slot, and detail surfaces.
|
||||||
|
- Browser console warnings/errors: none observed during the historical capture or current-standard revalidation.
|
||||||
- Follow-up target from the updated audit: no generated equipment icon batch is currently flagged; select the next art pass from runtime screenshots.
|
- Follow-up target from the updated audit: no generated equipment icon batch is currently flagged; select the next art pass from runtime screenshots.
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ Generated after the spear/polearm icon rework in `src/game/scenes/BootScene.ts`.
|
|||||||

|

|
||||||
|
|
||||||
- URL checked: `http://127.0.0.1:4197/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
- URL checked: `http://127.0.0.1:4197/heros_web/?debug&debugSortiePrep&renderer=canvas`
|
||||||
- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated equipment textures in the roster, slot, and detail surfaces.
|
- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled.
|
||||||
|
- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated equipment textures visible in the roster, slot, and detail surfaces and no browser console warnings or errors.
|
||||||
- Follow-up target from the updated audit: armor silhouettes.
|
- Follow-up target from the updated audit: armor silhouettes.
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ Date: 2026-07-03
|
|||||||
## Browser Checks
|
## Browser Checks
|
||||||
- Local: `http://127.0.0.1:4195/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-local-card`
|
- Local: `http://127.0.0.1:4195/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-local-card`
|
||||||
- Deployed: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-20260703`
|
- Deployed: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-20260703`
|
||||||
- PC viewport: 1280x720
|
- Historical capture viewport: 1280x720. The six PNGs below preserve the original local and deployed sessions and have not been relabeled or upscaled.
|
||||||
- Console errors: 0 local, 0 deployed
|
- Current-standard revalidation (2026-07-17): the equivalent current-code target preview, map popup, and post-attack card flows passed locally at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. The older deployed PNGs remain historical evidence rather than current FHD captures.
|
||||||
|
- Console errors: 0 during the historical local/deployed captures and current-standard revalidation.
|
||||||
|
|
||||||
## Captures
|
## Captures
|
||||||
- Local target preview: `docs/result-feedback-local-target-preview-card.png`
|
- Local target preview: `docs/result-feedback-local-target-preview-card.png`
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ Completed representative pass:
|
|||||||
- `unit-sima-yi`: strengthened from generic dark strategist to named Wei court conspirator with tall black court crown, black lacquer command tablet, sealed military order, dark decree scroll, bronze tally baton, and narrow command rod.
|
- `unit-sima-yi`: strengthened from generic dark strategist to named Wei court conspirator with tall black court crown, black lacquer command tablet, sealed military order, dark decree scroll, bronze tally baton, and narrow command rod.
|
||||||
- `unit-huang-quan`: strengthened from generic quartermaster official to Shu logistics quartermaster with supply ledger, ration register, supply-route map board, bamboo tally slips, bronze counting rods, sealed requisition scroll, and wooden document case.
|
- `unit-huang-quan`: strengthened from generic quartermaster official to Shu logistics quartermaster with supply ledger, ration register, supply-route map board, bamboo tally slips, bronze counting rods, sealed requisition scroll, and wooden document case.
|
||||||
|
|
||||||
Every representative fix should include contact sheet, animation GIF, battle preview, before/after, alpha audit, build, and browser check.
|
Every representative fix should include contact sheet, animation GIF, battle preview, before/after, alpha audit, build, and a browser check at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Only Liu Bei may be produced in this sample phase. Do not expand to Guan Yu, Zha
|
|||||||
Required sample outputs:
|
Required sample outputs:
|
||||||
- 313x313 contact sheet for Liu Bei.
|
- 313x313 contact sheet for Liu Bei.
|
||||||
- Animated GIF or browser capture for idle/walk/action.
|
- Animated GIF or browser capture for idle/walk/action.
|
||||||
- 100% desktop browser screenshot from the battle map.
|
- Battle-map browser screenshot captured at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`.
|
||||||
- Before/after comparison against the current checked-in Liu Bei sprite.
|
- Before/after comparison against the current checked-in Liu Bei sprite.
|
||||||
|
|
||||||
No deployment, commit, or push is allowed before sample approval.
|
No deployment, commit, or push is allowed before sample approval.
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ Review state manifest: `unit-sprite-rework-manifest.json`.
|
|||||||
- `python scripts/check-unit-sprite-workflow-consistency.py`
|
- `python scripts/check-unit-sprite-workflow-consistency.py`
|
||||||
- Review the relevant `docs/unit-sprite-review-<batch>.png` by eye.
|
- Review the relevant `docs/unit-sprite-review-<batch>.png` by eye.
|
||||||
- `pnpm build`
|
- `pnpm build`
|
||||||
- Local PC browser check.
|
- Local PC browser check at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`.
|
||||||
- For pending samples, local PC browser check with `?debug&debugSpritePreview=<unit-key>`.
|
- For pending samples, run the same 1920x1080 CSS viewport, 100% zoom, DPR 1 check with `?debug&debugSpritePreview=<unit-key>`.
|
||||||
- Console errors: `0`.
|
- Console errors: `0`.
|
||||||
- Approval received for the batch before commit/push/deploy.
|
- Approval received for the batch before commit/push/deploy.
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Do not commit, push, or deploy when:
|
|||||||
- Any newly edited unit is still marked `pending-approval`.
|
- Any newly edited unit is still marked `pending-approval`.
|
||||||
- Any untouched unit is accidentally counted as approved because it is only technically `ok`.
|
- Any untouched unit is accidentally counted as approved because it is only technically `ok`.
|
||||||
- The review board shows the new unit is not visibly better than the old placeholder.
|
- The review board shows the new unit is not visibly better than the old placeholder.
|
||||||
- The browser check has not been run.
|
- The browser check has not been run at the required 1920x1080 CSS viewport, 100% browser zoom, and DPR 1.
|
||||||
- Console errors are present.
|
- Console errors are present.
|
||||||
|
|
||||||
After approval and successful verification:
|
After approval and successful verification:
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
- Map key: `battle-map-sixty-fourth`
|
- Map key: `battle-map-sixty-fourth`
|
||||||
- Source asset: `src/assets/images/battle/sixty-fourth-battle-map.webp`
|
- Source asset: `src/assets/images/battle/sixty-fourth-battle-map.webp`
|
||||||
- Audit trigger: lowest battle-map source density bucket, `128x110` grid at `112.0` source pixels per tile
|
- Audit trigger: lowest battle-map source density bucket, `128x110` grid at `112.0` source pixels per tile
|
||||||
- Browser target: local Vite, `1280x720`, `http://127.0.0.1:4202/heros_web/?debug&debugBattle=sixty-fourth-battle-weishui-camps&v=visual-qa-sixty-fourth`
|
- Historical browser target: local Vite at `1280x720`, `http://127.0.0.1:4202/heros_web/?debug&debugBattle=sixty-fourth-battle-weishui-camps&v=visual-qa-sixty-fourth`. The three linked PNGs preserve that original session and have not been relabeled or upscaled.
|
||||||
|
- Current-standard revalidation target (2026-07-17): the same battle flow at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1.
|
||||||
|
|
||||||
## Captures
|
## Captures
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ Recommended repaint constraints:
|
|||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
- Local runtime: passed at `1280x720`.
|
- Historical local runtime: passed at `1280x720`; the three saved screenshots remain historical evidence.
|
||||||
|
- Current-standard revalidation (2026-07-17): passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1 for deployment, battle start, tactical readability, and minimap rendering.
|
||||||
- Screenshots saved: 3.
|
- Screenshots saved: 3.
|
||||||
- Browser warning/error logs during runtime pass: none.
|
- Browser warning/error logs during the historical runtime pass or current-standard revalidation: none.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"verify:unit-sprites": "node scripts/verify-unit-sprite-asset-data.mjs",
|
"verify:unit-sprites": "node scripts/verify-unit-sprite-asset-data.mjs",
|
||||||
"verify:battle-map-assets": "node scripts/verify-battle-map-asset-data.mjs",
|
"verify:battle-map-assets": "node scripts/verify-battle-map-asset-data.mjs",
|
||||||
"verify:audio-assets": "node scripts/verify-audio-asset-data.mjs",
|
"verify:audio-assets": "node scripts/verify-audio-asset-data.mjs",
|
||||||
|
"verify:desktop-viewport": "node scripts/verify-desktop-browser-viewport.mjs",
|
||||||
"verify:static-data": "node scripts/verify-static-data.mjs",
|
"verify:static-data": "node scripts/verify-static-data.mjs",
|
||||||
"verify:flow": "node scripts/verify-flow.mjs",
|
"verify:flow": "node scripts/verify-flow.mjs",
|
||||||
"verify:save-flow": "node scripts/verify-save-retry-flow.mjs",
|
"verify:save-flow": "node scripts/verify-save-retry-flow.mjs",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { readFileSync, writeFileSync } from 'node:fs';
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { createServer } from 'vite';
|
import { createServer } from 'vite';
|
||||||
|
import { desktopBrowserViewportLabel } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const bootScenePath = 'src/game/scenes/BootScene.ts';
|
const bootScenePath = 'src/game/scenes/BootScene.ts';
|
||||||
const campScenePath = 'src/game/scenes/CampScene.ts';
|
const campScenePath = 'src/game/scenes/CampScene.ts';
|
||||||
@@ -428,6 +429,7 @@ function renderReport({ iconSourceSize, iconTextureSize, itemRows, groupedRows,
|
|||||||
`- Runtime item icon scene: \`${sceneUsage.scene}\``,
|
`- Runtime item icon scene: \`${sceneUsage.scene}\``,
|
||||||
`- Observed runtime display sizes: ${sceneUsage.displaySizes.map((size) => `\`${size}px\``).join(', ') || 'none found'}`,
|
`- Observed runtime display sizes: ${sceneUsage.displaySizes.map((size) => `\`${size}px\``).join(', ') || 'none found'}`,
|
||||||
`- Direct item texture render sites found: ${sceneUsage.directItemImageCount}`,
|
`- Direct item texture render sites found: ${sceneUsage.directItemImageCount}`,
|
||||||
|
`- Browser visual QA baseline: \`${desktopBrowserViewportLabel}\` CSS viewport at 100% zoom`,
|
||||||
'',
|
'',
|
||||||
'## Main Findings',
|
'## Main Findings',
|
||||||
'',
|
'',
|
||||||
@@ -499,7 +501,7 @@ function renderRecommendedNextBatch(recommendations) {
|
|||||||
`- Item ids: ${formatItemIds(recommendations.firstTargetIds)}`,
|
`- Item ids: ${formatItemIds(recommendations.firstTargetIds)}`,
|
||||||
`- Success criteria: ${recommendations.firstTargetSuccessCriteria}`,
|
`- Success criteria: ${recommendations.firstTargetSuccessCriteria}`,
|
||||||
'- Keep the existing 64px texture size unless a UI-wide equipment icon pass is planned.',
|
'- Keep the existing 64px texture size unless a UI-wide equipment icon pass is planned.',
|
||||||
'- Add before/after contact sheets and verify the sortie equipment list and equipment swap panel in a desktop browser.',
|
`- Add before/after contact sheets and verify the sortie equipment list and equipment swap panel in a desktop browser at the ${desktopBrowserViewportLabel} CSS viewport and 100% zoom.`,
|
||||||
''
|
''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||||
import { join, sep } from 'node:path';
|
import { join, sep } from 'node:path';
|
||||||
import { createServer } from 'vite';
|
import { createServer } from 'vite';
|
||||||
|
import { desktopBrowserViewport } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const battleMapDir = join('src', 'assets', 'images', 'battle');
|
const battleMapDir = join('src', 'assets', 'images', 'battle');
|
||||||
const storyDir = join('src', 'assets', 'images', 'story');
|
const storyDir = join('src', 'assets', 'images', 'story');
|
||||||
const titleImagePath = join('src', 'assets', 'images', 'taoyuan-oath-title.png');
|
const titleImagePath = join('src', 'assets', 'images', 'taoyuan-oath-title.png');
|
||||||
const reportPath = join('docs', 'visual-asset-quality-audit.md');
|
const reportPath = join('docs', 'visual-asset-quality-audit.md');
|
||||||
const targetViewport = { width: 1920, height: 1080 };
|
const targetViewport = desktopBrowserViewport;
|
||||||
const expectedStorySize = { width: 1672, height: 941 };
|
const expectedStorySize = { width: 1672, height: 941 };
|
||||||
|
|
||||||
const server = await createServer({
|
const server = await createServer({
|
||||||
|
|||||||
19
scripts/desktop-browser-viewport.mjs
Normal file
19
scripts/desktop-browser-viewport.mjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const desktopBrowserViewport = Object.freeze({
|
||||||
|
width: 1920,
|
||||||
|
height: 1080
|
||||||
|
});
|
||||||
|
|
||||||
|
export const desktopBrowserDeviceScaleFactor = 1;
|
||||||
|
|
||||||
|
export const desktopBrowserContextOptions = Object.freeze({
|
||||||
|
viewport: desktopBrowserViewport,
|
||||||
|
deviceScaleFactor: desktopBrowserDeviceScaleFactor
|
||||||
|
});
|
||||||
|
|
||||||
|
export const desktopBrowserViewportBounds = Object.freeze({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
...desktopBrowserViewport
|
||||||
|
});
|
||||||
|
|
||||||
|
export const desktopBrowserViewportLabel = `${desktopBrowserViewport.width}x${desktopBrowserViewport.height}`;
|
||||||
@@ -2,10 +2,11 @@ import { spawn } from 'node:child_process';
|
|||||||
import { existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
import { existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import { desktopBrowserContextOptions, desktopBrowserViewport } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const targetUrl = withDebugParam(process.env.MEASURE_URL ?? 'http://127.0.0.1:4173/');
|
const targetUrl = withDebugParam(process.env.MEASURE_URL ?? 'http://127.0.0.1:4173/');
|
||||||
const outputPath = process.env.MEASURE_OUT ?? 'dist/performance-report.json';
|
const outputPath = process.env.MEASURE_OUT ?? 'dist/performance-report.json';
|
||||||
const baselineViewport = { width: 1920, height: 1080 };
|
const baselineViewport = desktopBrowserViewport;
|
||||||
const legacyUiScale = 1.5;
|
const legacyUiScale = 1.5;
|
||||||
const budgets = {
|
const budgets = {
|
||||||
titleReadyMs: readBudget('PERF_BUDGET_TITLE_READY_MS', 30000),
|
titleReadyMs: readBudget('PERF_BUDGET_TITLE_READY_MS', 30000),
|
||||||
@@ -50,7 +51,7 @@ let browser;
|
|||||||
try {
|
try {
|
||||||
serverProcess = await ensurePreviewServer(targetUrl);
|
serverProcess = await ensurePreviewServer(targetUrl);
|
||||||
browser = await chromium.launch({ headless: true });
|
browser = await chromium.launch({ headless: true });
|
||||||
const context = await browser.newContext({ viewport: baselineViewport });
|
const context = await browser.newContext(desktopBrowserContextOptions);
|
||||||
await context.addInitScript(() => window.localStorage.clear());
|
await context.addInitScript(() => window.localStorage.clear());
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
@@ -90,6 +91,8 @@ try {
|
|||||||
const report = {
|
const report = {
|
||||||
targetUrl,
|
targetUrl,
|
||||||
measuredAt: new Date().toISOString(),
|
measuredAt: new Date().toISOString(),
|
||||||
|
viewport: { ...baselineViewport },
|
||||||
|
deviceScaleFactor: desktopBrowserContextOptions.deviceScaleFactor,
|
||||||
timingsMs: {
|
timingsMs: {
|
||||||
titleReady: titleMs,
|
titleReady: titleMs,
|
||||||
storyReadyFromNewGame: storyMs,
|
storyReadyFromNewGame: storyMs,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|||||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import { desktopBrowserContextOptions, desktopBrowserViewport } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const cliOptions = parseCliOptions(process.argv.slice(2));
|
const cliOptions = parseCliOptions(process.argv.slice(2));
|
||||||
const defaultQaPort = process.env.QA_PORT ?? '41737';
|
const defaultQaPort = process.env.QA_PORT ?? '41737';
|
||||||
@@ -16,7 +17,7 @@ const campaignStorageKey = 'heros-web:campaign-state';
|
|||||||
const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1';
|
const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1';
|
||||||
const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH;
|
const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH;
|
||||||
const resumeCampaignSnapshot = process.env.QA_RESUME_CAMPAIGN_SNAPSHOT === '1';
|
const resumeCampaignSnapshot = process.env.QA_RESUME_CAMPAIGN_SNAPSHOT === '1';
|
||||||
const baselineViewport = { width: 1920, height: 1080 };
|
const baselineViewport = desktopBrowserViewport;
|
||||||
|
|
||||||
function withRenderer(url, renderer) {
|
function withRenderer(url, renderer) {
|
||||||
if (!['canvas', 'webgl'].includes(renderer)) {
|
if (!['canvas', 'webgl'].includes(renderer)) {
|
||||||
@@ -1181,7 +1182,7 @@ try {
|
|||||||
const browser = await chromium.launch({ headless });
|
const browser = await chromium.launch({ headless });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage({ viewport: baselineViewport });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const battle of representativeBattles) {
|
for (const battle of representativeBattles) {
|
||||||
@@ -3186,6 +3187,8 @@ function buildQaReport(results, failed) {
|
|||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
set: qaSetName,
|
set: qaSetName,
|
||||||
targetUrl,
|
targetUrl,
|
||||||
|
viewport: { ...baselineViewport },
|
||||||
|
deviceScaleFactor: desktopBrowserContextOptions.deviceScaleFactor,
|
||||||
headless,
|
headless,
|
||||||
maxRounds,
|
maxRounds,
|
||||||
cumulativeMode,
|
cumulativeMode,
|
||||||
|
|||||||
71
scripts/verify-desktop-browser-viewport.mjs
Normal file
71
scripts/verify-desktop-browser-viewport.mjs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
|
import {
|
||||||
|
desktopBrowserContextOptions,
|
||||||
|
desktopBrowserDeviceScaleFactor,
|
||||||
|
desktopBrowserViewport,
|
||||||
|
desktopBrowserViewportBounds,
|
||||||
|
desktopBrowserViewportLabel
|
||||||
|
} from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
|
const expectedViewport = { width: 1920, height: 1080 };
|
||||||
|
const expectedBounds = { x: 0, y: 0, ...expectedViewport };
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
assertJsonEqual(desktopBrowserViewport, expectedViewport, 'central desktop browser viewport');
|
||||||
|
assertJsonEqual(desktopBrowserViewportBounds, expectedBounds, 'central desktop browser viewport bounds');
|
||||||
|
assertJsonEqual(
|
||||||
|
desktopBrowserContextOptions,
|
||||||
|
{ viewport: expectedViewport, deviceScaleFactor: 1 },
|
||||||
|
'central desktop browser context options'
|
||||||
|
);
|
||||||
|
assert(desktopBrowserDeviceScaleFactor === 1, 'desktop browser device scale factor must be 1');
|
||||||
|
assert(desktopBrowserViewportLabel === '1920x1080', 'desktop browser viewport label must be 1920x1080');
|
||||||
|
|
||||||
|
const mainSource = readFileSync('src/main.ts', 'utf8');
|
||||||
|
assert(/width:\s*1920\s*,[\s\S]*?height:\s*1080\s*,/.test(mainSource), 'Phaser game config must remain 1920x1080');
|
||||||
|
|
||||||
|
const policySource = readFileSync('AGENTS.md', 'utf8');
|
||||||
|
assert(policySource.includes('1920x1080 CSS viewport at 100% browser zoom'), 'AGENTS.md must define the 1920x1080 browser QA baseline');
|
||||||
|
assert(policySource.includes("Never rely on an in-app browser's ambient/default viewport"), 'AGENTS.md must prohibit ambient browser viewport use');
|
||||||
|
|
||||||
|
const automatedBrowserChecks = readdirSync('scripts', { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.mjs'))
|
||||||
|
.map((entry) => `scripts/${entry.name}`)
|
||||||
|
.filter((scriptPath) => {
|
||||||
|
const source = readFileSync(scriptPath, 'utf8');
|
||||||
|
return source.includes("from 'playwright'") && /\b(?:newPage|newContext)\s*\(/.test(source);
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
assert(automatedBrowserChecks.length > 0, 'at least one automated browser check must be discovered');
|
||||||
|
|
||||||
|
for (const scriptPath of automatedBrowserChecks) {
|
||||||
|
const source = readFileSync(scriptPath, 'utf8');
|
||||||
|
assert(source.includes("from './desktop-browser-viewport.mjs'"), `${scriptPath} must import the shared browser viewport`);
|
||||||
|
assert(
|
||||||
|
/\b(?:newPage|newContext)\(desktopBrowserContextOptions\)/.test(source),
|
||||||
|
`${scriptPath} must explicitly create its primary browser page or context with the shared FHD context options`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!/viewport:\s*\{\s*width:\s*1920\s*,\s*height:\s*1080\s*\}/.test(source),
|
||||||
|
`${scriptPath} must not duplicate the primary 1920x1080 viewport inline`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`Desktop browser viewport verification failed:\n- ${errors.join('\n- ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Verified ${desktopBrowserViewportLabel} desktop browser baseline at DPR ${desktopBrowserDeviceScaleFactor} across ${automatedBrowserChecks.length} browser checks.`
|
||||||
|
);
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
errors.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertJsonEqual(actual, expected, label) {
|
||||||
|
assert(JSON.stringify(actual) === JSON.stringify(expected), `${label} must be ${JSON.stringify(expected)}; received ${JSON.stringify(actual)}`);
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import { desktopBrowserContextOptions } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/');
|
const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/');
|
||||||
const rewardDataFiles = ['src/game/data/battles.ts', 'src/game/scenes/CampScene.ts'];
|
const rewardDataFiles = ['src/game/data/battles.ts', 'src/game/scenes/CampScene.ts'];
|
||||||
const baselineViewport = { width: 1920, height: 1080 };
|
|
||||||
const legacyUiScale = 1.5;
|
const legacyUiScale = 1.5;
|
||||||
const legacyUiClickPoints = new Set([
|
const legacyUiClickPoints = new Set([
|
||||||
'962,240',
|
'962,240',
|
||||||
@@ -238,7 +238,8 @@ async function moveBattlePointerToRightEdge(page) {
|
|||||||
function installLegacyUiClickScaling(page) {
|
function installLegacyUiClickScaling(page) {
|
||||||
const click = page.mouse.click.bind(page.mouse);
|
const click = page.mouse.click.bind(page.mouse);
|
||||||
page.mouse.click = (x, y, options) => {
|
page.mouse.click = (x, y, options) => {
|
||||||
// Only authored 1280x720 controls are scaled; runtime map/minimap coordinates pass through unchanged.
|
// Only controls authored in the legacy reference coordinate system are scaled 1.5x to FHD;
|
||||||
|
// runtime map/minimap coordinates pass through unchanged.
|
||||||
const scale = legacyUiClickPoints.has(`${x},${y}`) ? legacyUiScale : 1;
|
const scale = legacyUiClickPoints.has(`${x},${y}`) ? legacyUiScale : 1;
|
||||||
return click(x * scale, y * scale, options);
|
return click(x * scale, y * scale, options);
|
||||||
};
|
};
|
||||||
@@ -253,7 +254,7 @@ try {
|
|||||||
serverProcess = await ensureLocalServer(targetUrl);
|
serverProcess = await ensureLocalServer(targetUrl);
|
||||||
|
|
||||||
browser = await chromium.launch({ headless: true });
|
browser = await chromium.launch({ headless: true });
|
||||||
const page = await browser.newPage({ viewport: baselineViewport });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
installLegacyUiClickScaling(page);
|
installLegacyUiClickScaling(page);
|
||||||
|
|
||||||
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { mkdirSync } from 'node:fs';
|
|||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import { desktopBrowserContextOptions } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const targetUrl = withQaParams(process.env.PUBLIC_QA_URL ?? 'https://comtropy.synology.me/heros_web/');
|
const targetUrl = withQaParams(process.env.PUBLIC_QA_URL ?? 'https://comtropy.synology.me/heros_web/');
|
||||||
const screenshotPath = process.env.PUBLIC_QA_SCREENSHOT ?? 'dist/public-deploy-qa.png';
|
const screenshotPath = process.env.PUBLIC_QA_SCREENSHOT ?? 'dist/public-deploy-qa.png';
|
||||||
@@ -15,7 +16,7 @@ mkdirSync(dirname(screenshotPath), { recursive: true });
|
|||||||
const browser = await chromium.launch({ headless: true });
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
const consoleMessages = [];
|
const consoleMessages = [];
|
||||||
const pageErrors = [];
|
const pageErrors = [];
|
||||||
const requestFailures = [];
|
const requestFailures = [];
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { spawn, spawnSync } from 'node:child_process';
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import {
|
||||||
|
desktopBrowserContextOptions,
|
||||||
|
desktopBrowserDeviceScaleFactor,
|
||||||
|
desktopBrowserViewport,
|
||||||
|
desktopBrowserViewportBounds
|
||||||
|
} from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const targetUrl = withDebugParam(process.env.RELEASE_QA_URL ?? 'http://127.0.0.1:4173/');
|
const targetUrl = withDebugParam(process.env.RELEASE_QA_URL ?? 'http://127.0.0.1:4173/');
|
||||||
const screenshotDir = 'dist';
|
const screenshotDir = 'dist';
|
||||||
const baselineViewport = { width: 1920, height: 1080 };
|
const baselineViewport = desktopBrowserViewport;
|
||||||
const phaserWebglRendererType = 2;
|
const phaserWebglRendererType = 2;
|
||||||
const legacyUiScale = 1.5;
|
const legacyUiScale = 1.5;
|
||||||
const expectedTitle = '\uC0BC\uAD6D\uC9C0: \uC138 \uD615\uC81C\uC758 \uB9F9\uC138';
|
const expectedTitle = '\uC0BC\uAD6D\uC9C0: \uC138 \uD615\uC81C\uC758 \uB9F9\uC138';
|
||||||
@@ -49,7 +55,7 @@ try {
|
|||||||
runCommand(process.execPath, ['scripts/verify-save-retry-flow.mjs'], { VERIFY_URL: targetUrl });
|
runCommand(process.execPath, ['scripts/verify-save-retry-flow.mjs'], { VERIFY_URL: targetUrl });
|
||||||
|
|
||||||
browser = await chromium.launch({ headless: true });
|
browser = await chromium.launch({ headless: true });
|
||||||
const page = await browser.newPage({ viewport: baselineViewport });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
const consoleMessages = [];
|
const consoleMessages = [];
|
||||||
const pageErrors = [];
|
const pageErrors = [];
|
||||||
const requestFailures = [];
|
const requestFailures = [];
|
||||||
@@ -2710,7 +2716,7 @@ try {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const firstSortiePursuit = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePursuitPreview);
|
const firstSortiePursuit = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePursuitPreview);
|
||||||
const logicalViewportBounds = { x: 0, y: 0, width: 1920, height: 1080 };
|
const logicalViewportBounds = desktopBrowserViewportBounds;
|
||||||
assert(
|
assert(
|
||||||
sameJsonValue(firstSortiePursuit?.activePairs, expectedFirstSortiePursuitPairs) &&
|
sameJsonValue(firstSortiePursuit?.activePairs, expectedFirstSortiePursuitPairs) &&
|
||||||
firstSortiePursuit?.selectableOpportunities?.length === 0 &&
|
firstSortiePursuit?.selectableOpportunities?.length === 0 &&
|
||||||
@@ -5268,7 +5274,7 @@ try {
|
|||||||
const midEvaluationSave = await readCampaignSave(page);
|
const midEvaluationSave = await readCampaignSave(page);
|
||||||
const mobileResultCard = midEvaluationProbe.evaluation?.presetCards?.find((card) => card.id === 'mobile');
|
const mobileResultCard = midEvaluationProbe.evaluation?.presetCards?.find((card) => card.id === 'mobile');
|
||||||
const openedRecommendationFeedback = midEvaluationProbe.evaluation?.recommendationFeedback;
|
const openedRecommendationFeedback = midEvaluationProbe.evaluation?.recommendationFeedback;
|
||||||
const fhdViewportBounds = { x: 0, y: 0, width: 1920, height: 1080 };
|
const fhdViewportBounds = desktopBrowserViewportBounds;
|
||||||
assert(
|
assert(
|
||||||
midEvaluationProbe.texts.includes('이번 편성 평가') &&
|
midEvaluationProbe.texts.includes('이번 편성 평가') &&
|
||||||
midEvaluationProbe.texts.some((text) => text.includes('추천안 검증') && text.includes(midRecommendationLaunchFixture.selection.label)) &&
|
midEvaluationProbe.texts.some((text) => text.includes('추천안 검증') && text.includes(midRecommendationLaunchFixture.selection.label)) &&
|
||||||
@@ -5767,8 +5773,8 @@ async function assertReleaseShellReady(page) {
|
|||||||
async function assertHdPlusCanvasFit(page) {
|
async function assertHdPlusCanvasFit(page) {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
viewport: { width: 1920, height: 1080 },
|
viewport: desktopBrowserViewport,
|
||||||
expectedBounds: { x: 0, y: 0, width: 1920, height: 1080 }
|
expectedBounds: desktopBrowserViewportBounds
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewport: { width: 2560, height: 1440 },
|
viewport: { width: 2560, height: 1440 },
|
||||||
@@ -5850,7 +5856,7 @@ function boundsNear(actual, expected, tolerance = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function assertHdPlusInputMapping(browser, url) {
|
async function assertHdPlusInputMapping(browser, url) {
|
||||||
const page = await browser.newPage({ viewport: { width: 2560, height: 1600 } });
|
const page = await browser.newPage({ viewport: { width: 2560, height: 1600 }, deviceScaleFactor: desktopBrowserDeviceScaleFactor });
|
||||||
try {
|
try {
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
await page.evaluate(() => window.localStorage.clear());
|
await page.evaluate(() => window.localStorage.clear());
|
||||||
@@ -5896,7 +5902,7 @@ async function assertLargeRosterHud(browser, url) {
|
|||||||
];
|
];
|
||||||
const battleUrl = new URL(url);
|
const battleUrl = new URL(url);
|
||||||
battleUrl.searchParams.set('debugBattle', 'sixty-sixth-battle-wuzhang-final');
|
battleUrl.searchParams.set('debugBattle', 'sixty-sixth-battle-wuzhang-final');
|
||||||
const page = await browser.newPage({ viewport: baselineViewport });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
try {
|
try {
|
||||||
await page.goto(battleUrl.toString(), { waitUntil: 'domcontentloaded' });
|
await page.goto(battleUrl.toString(), { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
@@ -6048,7 +6054,7 @@ async function assertWebglBattleQuickTabSmoke(browser, url, battleId) {
|
|||||||
const battleUrl = new URL(url);
|
const battleUrl = new URL(url);
|
||||||
battleUrl.searchParams.set('renderer', 'webgl');
|
battleUrl.searchParams.set('renderer', 'webgl');
|
||||||
battleUrl.searchParams.set('debugBattle', battleId);
|
battleUrl.searchParams.set('debugBattle', battleId);
|
||||||
const page = await browser.newPage({ viewport: baselineViewport });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
const pageErrors = [];
|
const pageErrors = [];
|
||||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||||
|
|
||||||
@@ -7786,7 +7792,7 @@ function assertCampRosterPageLayout(roster, expectedPage, pageCount, expectedSel
|
|||||||
const listBounds = roster.listBounds;
|
const listBounds = roster.listBounds;
|
||||||
assert(isPositiveBounds(listBounds), `Expected final-camp roster list bounds on page ${expectedPage + 1}: ${JSON.stringify(roster)}`);
|
assert(isPositiveBounds(listBounds), `Expected final-camp roster list bounds on page ${expectedPage + 1}: ${JSON.stringify(roster)}`);
|
||||||
assert(
|
assert(
|
||||||
isBoundsInside(listBounds, { x: 0, y: 0, width: 1920, height: 1080 }),
|
isBoundsInside(listBounds, desktopBrowserViewportBounds),
|
||||||
`Expected final-camp roster list inside the 1920x1080 canvas: ${JSON.stringify(roster)}`
|
`Expected final-camp roster list inside the 1920x1080 canvas: ${JSON.stringify(roster)}`
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -7794,7 +7800,7 @@ function assertCampRosterPageLayout(roster, expectedPage, pageCount, expectedSel
|
|||||||
isPositiveBounds(roster.navigationBounds?.previous) &&
|
isPositiveBounds(roster.navigationBounds?.previous) &&
|
||||||
isPositiveBounds(roster.navigationBounds?.label) &&
|
isPositiveBounds(roster.navigationBounds?.label) &&
|
||||||
isPositiveBounds(roster.navigationBounds?.next) &&
|
isPositiveBounds(roster.navigationBounds?.next) &&
|
||||||
isBoundsInside(roster.navigationBounds.container, { x: 0, y: 0, width: 1920, height: 1080 }),
|
isBoundsInside(roster.navigationBounds.container, desktopBrowserViewportBounds),
|
||||||
`Expected complete final-camp roster navigation bounds inside the FHD canvas: ${JSON.stringify(roster)}`
|
`Expected complete final-camp roster navigation bounds inside the FHD canvas: ${JSON.stringify(roster)}`
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -7810,7 +7816,7 @@ function assertCampRosterPageLayout(roster, expectedPage, pageCount, expectedSel
|
|||||||
row.unitId === roster.visibleUnitIds[rowIndex] &&
|
row.unitId === roster.visibleUnitIds[rowIndex] &&
|
||||||
isPositiveBounds(row) &&
|
isPositiveBounds(row) &&
|
||||||
isBoundsInside(row, listBounds) &&
|
isBoundsInside(row, listBounds) &&
|
||||||
isBoundsInside(row, { x: 0, y: 0, width: 1920, height: 1080 }) &&
|
isBoundsInside(row, desktopBrowserViewportBounds) &&
|
||||||
row.y + row.height <= roster.navigationBounds.container.y,
|
row.y + row.height <= roster.navigationBounds.container.y,
|
||||||
`Expected final-camp roster row ${rowIndex + 1} to match its visible officer and remain inside the list and FHD canvas: ${JSON.stringify({
|
`Expected final-camp roster row ${rowIndex + 1} to match its visible officer and remain inside the list and FHD canvas: ${JSON.stringify({
|
||||||
row,
|
row,
|
||||||
@@ -8628,7 +8634,7 @@ async function readSortiePresetControlPoint(page, control, presetId) {
|
|||||||
function assertSortiePortraitRosterPage(view, expectedPage, expectedVisibleUnitIds) {
|
function assertSortiePortraitRosterPage(view, expectedPage, expectedVisibleUnitIds) {
|
||||||
const expectedStart = expectedVisibleUnitIds.length > 0 ? expectedPage * 6 + 1 : 0;
|
const expectedStart = expectedVisibleUnitIds.length > 0 ? expectedPage * 6 + 1 : 0;
|
||||||
const expectedEnd = expectedPage * 6 + expectedVisibleUnitIds.length;
|
const expectedEnd = expectedPage * 6 + expectedVisibleUnitIds.length;
|
||||||
const hdBounds = { x: 0, y: 0, width: 1920, height: 1080 };
|
const hdBounds = desktopBrowserViewportBounds;
|
||||||
assert(
|
assert(
|
||||||
view?.enabled === true &&
|
view?.enabled === true &&
|
||||||
view.total === 23 &&
|
view.total === 23 &&
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import { desktopBrowserContextOptions, desktopBrowserViewport } from './desktop-browser-viewport.mjs';
|
||||||
|
|
||||||
const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/');
|
const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/');
|
||||||
const baselineViewport = { width: 1920, height: 1080 };
|
const baselineViewport = desktopBrowserViewport;
|
||||||
const legacyUiScale = 1.5;
|
const legacyUiScale = 1.5;
|
||||||
|
|
||||||
function withCanvasRenderer(url) {
|
function withCanvasRenderer(url) {
|
||||||
@@ -18,7 +19,7 @@ try {
|
|||||||
serverProcess = await ensureLocalServer(targetUrl);
|
serverProcess = await ensureLocalServer(targetUrl);
|
||||||
|
|
||||||
browser = await chromium.launch({ headless: true });
|
browser = await chromium.launch({ headless: true });
|
||||||
const page = await browser.newPage({ viewport: baselineViewport });
|
const page = await browser.newPage(desktopBrowserContextOptions);
|
||||||
|
|
||||||
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
||||||
await page.evaluate(() => window.localStorage.clear());
|
await page.evaluate(() => window.localStorage.clear());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
const checks = [
|
const checks = [
|
||||||
|
'scripts/verify-desktop-browser-viewport.mjs',
|
||||||
'scripts/verify-campaign-flow-data.mjs',
|
'scripts/verify-campaign-flow-data.mjs',
|
||||||
'scripts/verify-campaign-save-normalization.mjs',
|
'scripts/verify-campaign-save-normalization.mjs',
|
||||||
'scripts/verify-battle-save-normalization.mjs',
|
'scripts/verify-battle-save-normalization.mjs',
|
||||||
|
|||||||
Reference in New Issue
Block a user