fix: stabilize battle quick navigation
This commit is contained in:
@@ -5,6 +5,7 @@ import { chromium } from 'playwright';
|
||||
const targetUrl = withDebugParam(process.env.RELEASE_QA_URL ?? 'http://127.0.0.1:4173/');
|
||||
const screenshotDir = 'dist';
|
||||
const baselineViewport = { width: 1920, height: 1080 };
|
||||
const phaserWebglRendererType = 2;
|
||||
const legacyUiScale = 1.5;
|
||||
const expectedTitle = '\uC0BC\uAD6D\uC9C0: \uC138 \uD615\uC81C\uC758 \uB9F9\uC138';
|
||||
const firstBattleUnlockLabel = '\uD669\uAC74 \uC794\uB2F9 \uCD94\uACA9 \uAC1C\uBC29';
|
||||
@@ -74,6 +75,7 @@ try {
|
||||
await assertHdPlusCanvasFit(page);
|
||||
await assertHdPlusInputMapping(browser, targetUrl);
|
||||
await assertLargeRosterHud(browser, targetUrl);
|
||||
await assertWebglBattleQuickTabSmoke(browser, targetUrl, 'first-battle-zhuo-commandery');
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-title-first-run.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'title first run');
|
||||
|
||||
@@ -5599,6 +5601,157 @@ async function assertLargeRosterHud(browser, url) {
|
||||
}
|
||||
}
|
||||
|
||||
async function assertWebglBattleQuickTabSmoke(browser, url, battleId) {
|
||||
const battleUrl = new URL(url);
|
||||
battleUrl.searchParams.set('renderer', 'webgl');
|
||||
battleUrl.searchParams.set('debugBattle', battleId);
|
||||
const page = await browser.newPage({ viewport: baselineViewport });
|
||||
const pageErrors = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
|
||||
try {
|
||||
await page.addInitScript(() => window.localStorage.clear());
|
||||
await page.goto(battleUrl.toString(), { waitUntil: 'domcontentloaded' });
|
||||
await waitForBattleReady(page, battleId);
|
||||
await page.waitForFunction(() => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return battle?.phase === 'idle' && battle.battleHud?.quickTabs?.enabled === true;
|
||||
}, undefined, { timeout: 90000 });
|
||||
|
||||
const rendererProbe = await page.evaluate(() => {
|
||||
const game = window.__HEROS_GAME__;
|
||||
const scene = game?.scene.getScene('BattleScene');
|
||||
return {
|
||||
type: game?.renderer?.type ?? null,
|
||||
name: game?.renderer?.constructor?.name ?? null,
|
||||
sceneWidth: scene?.scale.width ?? null,
|
||||
sceneHeight: scene?.scale.height ?? null
|
||||
};
|
||||
});
|
||||
assert(
|
||||
rendererProbe.type === phaserWebglRendererType &&
|
||||
rendererProbe.sceneWidth === baselineViewport.width &&
|
||||
rendererProbe.sceneHeight === baselineViewport.height,
|
||||
`Expected the ${battleId} input smoke to use the FHD WebGL renderer: ${JSON.stringify(rendererProbe)}`
|
||||
);
|
||||
|
||||
let actionableUnitPoint = await readActionableBattleUnitPoint(page);
|
||||
await page.mouse.click(actionableUnitPoint.x, actionableUnitPoint.y);
|
||||
await page.waitForFunction(
|
||||
(unitId) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return (
|
||||
battle?.phase === 'moving' &&
|
||||
battle.selectedUnitId === unitId &&
|
||||
battle.pendingMove === null &&
|
||||
battle.battleHud?.quickTabs?.enabled === true
|
||||
);
|
||||
},
|
||||
actionableUnitPoint.unitId,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await page.keyboard.press('3');
|
||||
await waitForSideQuickTabView(page, 'bond', 'bond');
|
||||
const keyboardReturnState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
assert(
|
||||
keyboardReturnState?.phase === 'idle' &&
|
||||
keyboardReturnState.selectedUnitId === null &&
|
||||
keyboardReturnState.pendingMove === null &&
|
||||
keyboardReturnState.markerCount === 0,
|
||||
`Expected WebGL keyboard navigation to cancel an uncommitted selection: ${JSON.stringify(keyboardReturnState)}`
|
||||
);
|
||||
|
||||
actionableUnitPoint = await readActionableBattleUnitPoint(page);
|
||||
await page.mouse.click(actionableUnitPoint.x, actionableUnitPoint.y);
|
||||
await page.waitForFunction(
|
||||
(unitId) => window.__HEROS_DEBUG__?.battle()?.phase === 'moving' && window.__HEROS_DEBUG__?.battle()?.selectedUnitId === unitId,
|
||||
actionableUnitPoint.unitId,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await page.mouse.click(actionableUnitPoint.x, actionableUnitPoint.y);
|
||||
await page.waitForFunction(
|
||||
(unitId) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return battle?.phase === 'command' && battle.selectedUnitId === unitId && battle.commandMenuVisible === true;
|
||||
},
|
||||
actionableUnitPoint.unitId,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const commandLockState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
assert(
|
||||
commandLockState?.pendingMove?.unitId === actionableUnitPoint.unitId &&
|
||||
commandLockState.battleHud?.quickTabs?.enabled === false,
|
||||
`Expected committed movement and its command menu to keep quick tabs locked: ${JSON.stringify(commandLockState)}`
|
||||
);
|
||||
const waitCommandPoint = await readBattleCommandControlPoint(page, 'wait');
|
||||
await page.mouse.click(waitCommandPoint.x, waitCommandPoint.y);
|
||||
await page.waitForFunction(
|
||||
(completedUnitId) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return (
|
||||
battle?.phase === 'moving' &&
|
||||
battle.selectedUnitId &&
|
||||
battle.selectedUnitId !== completedUnitId &&
|
||||
battle.actedUnitIds?.includes(completedUnitId) &&
|
||||
battle.battleHud?.quickTabs?.enabled === true
|
||||
);
|
||||
},
|
||||
actionableUnitPoint.unitId,
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
|
||||
const rosterTabPoint = await readSideQuickTabControlPoint(page, 'roster');
|
||||
await page.mouse.click(rosterTabPoint.x, rosterTabPoint.y);
|
||||
await waitForSideQuickTabView(page, 'roster', 'roster');
|
||||
const autoFocusReturnState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
assert(
|
||||
autoFocusReturnState?.phase === 'idle' &&
|
||||
autoFocusReturnState.selectedUnitId === null &&
|
||||
autoFocusReturnState.pendingMove === null &&
|
||||
autoFocusReturnState.markerCount === 0 &&
|
||||
autoFocusReturnState.actedUnitIds?.includes(actionableUnitPoint.unitId),
|
||||
`Expected a WebGL mouse tab click to leave auto-focused movement safely: ${JSON.stringify(autoFocusReturnState)}`
|
||||
);
|
||||
|
||||
const emptyTilePoint = await readEmptyBattleTilePoint(page);
|
||||
const transitionRevision = autoFocusReturnState.battleHud?.panelTransition?.revision;
|
||||
await page.mouse.move(emptyTilePoint.x, emptyTilePoint.y);
|
||||
await page.waitForFunction(
|
||||
({ tileX, tileY }) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return (
|
||||
battle?.pointerFeedbackVisible === true &&
|
||||
battle.pointerFeedbackTile?.x === tileX &&
|
||||
battle.pointerFeedbackTile?.y === tileY &&
|
||||
battle.battleHud?.quickTabs?.activeTab === 'roster' &&
|
||||
battle.battleHud.quickTabs.currentView === 'roster'
|
||||
);
|
||||
},
|
||||
{ tileX: emptyTilePoint.tile.x, tileY: emptyTilePoint.tile.y },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const hoverState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
assert(
|
||||
hoverState?.battleHud?.terrainPanel === null &&
|
||||
hoverState.battleHud.panelTransition?.revision === transitionRevision,
|
||||
`Expected WebGL terrain hover to preserve the roster panel: ${JSON.stringify(hoverState)}`
|
||||
);
|
||||
|
||||
await page.mouse.click(emptyTilePoint.x, emptyTilePoint.y);
|
||||
await waitForSideQuickTabView(page, 'situation', 'terrain-detail');
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForSideQuickTabView(page, 'situation', 'situation');
|
||||
const screenshot = await page.screenshot({ path: `${screenshotDir}/rc-webgl-${battleId}-quick-tabs.png`, fullPage: true });
|
||||
assert(
|
||||
screenshot.byteLength >= 100_000,
|
||||
`Expected a visibly painted ${battleId} WebGL frame: ${JSON.stringify({ byteLength: screenshot.byteLength })}`
|
||||
);
|
||||
assert(pageErrors.length === 0, `Unexpected ${battleId} WebGL runtime errors: ${JSON.stringify(pageErrors)}`);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function assertFinalBattleQuickTabs(page) {
|
||||
const initialProbe = await readLargeRosterHudProbe(page);
|
||||
assertSideQuickTabsLayout(initialProbe, 'situation', 'situation');
|
||||
@@ -5732,6 +5885,136 @@ async function waitForSideQuickTabView(page, activeTab, currentView) {
|
||||
);
|
||||
}
|
||||
|
||||
async function readSideQuickTabControlPoint(page, tabId) {
|
||||
const point = await page.evaluate((expectedTabId) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const tab = window.__HEROS_DEBUG__?.battle()?.battleHud?.quickTabs?.tabs?.find((candidate) => candidate.id === expectedTabId);
|
||||
const canvas = document.querySelector('canvas');
|
||||
const canvasBounds = canvas?.getBoundingClientRect();
|
||||
if (!scene || !tab?.bounds || !canvas || !canvasBounds) {
|
||||
return null;
|
||||
}
|
||||
const logicalX = tab.bounds.x + tab.bounds.width / 2;
|
||||
const logicalY = tab.bounds.y + tab.bounds.height / 2;
|
||||
return {
|
||||
tabId: expectedTabId,
|
||||
x: canvasBounds.left + logicalX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + logicalY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
}, tabId);
|
||||
assert(point && Number.isFinite(point.x) && Number.isFinite(point.y), `Expected a visible ${tabId} quick-tab control: ${JSON.stringify(point)}`);
|
||||
return point;
|
||||
}
|
||||
|
||||
async function readBattleCommandControlPoint(page, command) {
|
||||
const point = await page.evaluate((expectedCommand) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const entry = scene?.commandButtons?.find((candidate) => candidate.command === expectedCommand);
|
||||
const canvas = document.querySelector('canvas');
|
||||
const canvasBounds = canvas?.getBoundingClientRect();
|
||||
const bounds = entry?.background?.getBounds?.();
|
||||
if (!scene || !bounds || !canvas || !canvasBounds) {
|
||||
return null;
|
||||
}
|
||||
const logicalX = bounds.x + bounds.width / 2;
|
||||
const logicalY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
command: expectedCommand,
|
||||
x: canvasBounds.left + logicalX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + logicalY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
}, command);
|
||||
assert(point && Number.isFinite(point.x) && Number.isFinite(point.y), `Expected a visible ${command} command control: ${JSON.stringify(point)}`);
|
||||
return point;
|
||||
}
|
||||
|
||||
async function readActionableBattleUnitPoint(page) {
|
||||
const point = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
const canvas = document.querySelector('canvas');
|
||||
const canvasBounds = canvas?.getBoundingClientRect();
|
||||
if (!scene || !battle || !canvas || !canvasBounds) {
|
||||
return null;
|
||||
}
|
||||
const unit = battle.units?.find((candidate) => {
|
||||
const view = scene.unitViews?.get(candidate.id);
|
||||
return candidate.faction === 'ally' && candidate.hp > 0 && !candidate.acted && view?.sprite?.visible && view.sprite.active;
|
||||
});
|
||||
const sprite = unit ? scene.unitViews?.get(unit.id)?.sprite : null;
|
||||
const bounds = sprite?.getBounds?.();
|
||||
if (!unit || !bounds) {
|
||||
return null;
|
||||
}
|
||||
const logicalX = bounds.x + bounds.width / 2;
|
||||
const logicalY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
unitId: unit.id,
|
||||
tile: { x: unit.x, y: unit.y },
|
||||
x: canvasBounds.left + logicalX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + logicalY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
});
|
||||
assert(
|
||||
point?.unitId && Number.isFinite(point.x) && Number.isFinite(point.y),
|
||||
`Expected a visible actionable ally for real-input QA: ${JSON.stringify(point)}`
|
||||
);
|
||||
return point;
|
||||
}
|
||||
|
||||
async function readEmptyBattleTilePoint(page) {
|
||||
const point = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
const canvas = document.querySelector('canvas');
|
||||
const canvasBounds = canvas?.getBoundingClientRect();
|
||||
const camera = battle?.camera;
|
||||
if (!scene || !battle || !camera || !canvas || !canvasBounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const occupiedTiles = new Set(
|
||||
(battle.units ?? []).filter((unit) => unit.hp > 0).map((unit) => `${unit.x},${unit.y}`)
|
||||
);
|
||||
const markerTiles = new Set(
|
||||
(scene.markers ?? [])
|
||||
.filter((marker) => marker?.active && marker?.visible)
|
||||
.map((marker) => `${marker.getData('tileX')},${marker.getData('tileY')}`)
|
||||
);
|
||||
const candidates = [];
|
||||
const maxX = Math.min(camera.mapWidth, camera.x + camera.visibleColumns);
|
||||
const maxY = Math.min(camera.mapHeight, camera.y + camera.visibleRows);
|
||||
const centerX = camera.x + camera.visibleColumns / 2;
|
||||
const centerY = camera.y + camera.visibleRows / 2;
|
||||
for (let y = camera.y; y < maxY; y += 1) {
|
||||
for (let x = camera.x; x < maxX; x += 1) {
|
||||
const key = `${x},${y}`;
|
||||
if (occupiedTiles.has(key) || markerTiles.has(key)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({ x, y, distance: Math.abs(x - centerX) + Math.abs(y - centerY) });
|
||||
}
|
||||
}
|
||||
candidates.sort((left, right) => left.distance - right.distance || left.y - right.y || left.x - right.x);
|
||||
const tile = candidates[0];
|
||||
if (!tile) {
|
||||
return null;
|
||||
}
|
||||
const logicalX = scene.tileCenterX(tile.x);
|
||||
const logicalY = scene.tileCenterY(tile.y);
|
||||
return {
|
||||
tile: { x: tile.x, y: tile.y },
|
||||
x: canvasBounds.left + logicalX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + logicalY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
});
|
||||
assert(
|
||||
point?.tile && Number.isFinite(point.x) && Number.isFinite(point.y),
|
||||
`Expected an empty visible battle tile for pointer QA: ${JSON.stringify(point)}`
|
||||
);
|
||||
return point;
|
||||
}
|
||||
|
||||
async function assertFinalBattleFhdDetailPanels(page) {
|
||||
const allyFixture = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
|
||||
Reference in New Issue
Block a user