fix: prevent camp save click-through
This commit is contained in:
@@ -42,9 +42,12 @@ const expectedBattleId = 'second-battle-yellow-turban-pursuit';
|
||||
|
||||
let serverProcess;
|
||||
let browser;
|
||||
const pageErrors = [];
|
||||
let delayedBattleModuleRequests = 0;
|
||||
|
||||
try {
|
||||
serverProcess = await ensureLocalServer(targetUrl);
|
||||
await prewarmBattleSceneModule(targetUrl);
|
||||
browser = await chromium.launch({
|
||||
headless:
|
||||
process.env.VERIFY_INTERACTION_UX_HEADLESS !== '0',
|
||||
@@ -60,10 +63,8 @@ try {
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(30000);
|
||||
|
||||
const pageErrors = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.stack ?? error.message));
|
||||
|
||||
let delayedBattleModuleRequests = 0;
|
||||
await page.route('**/src/game/scenes/BattleScene.ts*', async (route) => {
|
||||
delayedBattleModuleRequests += 1;
|
||||
await delay(1200);
|
||||
@@ -275,11 +276,49 @@ async function verifyCampModalAndNavigation(page) {
|
||||
})}`
|
||||
);
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
const scenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return scenes.includes('BattleScene') && battle?.scene === 'BattleScene';
|
||||
}, undefined, { timeout: 90000 });
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const scenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return scenes.includes('BattleScene') && battle?.scene === 'BattleScene';
|
||||
}, undefined, { timeout: 90000 });
|
||||
} catch (error) {
|
||||
const diagnostic = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const camp = window.__HEROS_DEBUG__?.camp();
|
||||
return {
|
||||
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
|
||||
camp: camp
|
||||
? {
|
||||
campaignStep: camp.campaign?.step ?? null,
|
||||
sortiePrepStep: camp.sortiePrepStep ?? null,
|
||||
sortiePrimaryAction: camp.sortiePrimaryAction?.kind ?? null,
|
||||
selectedSortieUnitIds: camp.selectedSortieUnitIds ?? [],
|
||||
nextSortieBattleId: camp.nextSortieBattleId ?? null,
|
||||
skipIntroStoryForSortie:
|
||||
camp.skipIntroStoryForSortie ?? null
|
||||
}
|
||||
: null,
|
||||
navigationPending: scene?.navigationPending ?? null,
|
||||
navigationBlockerCount: scene?.navigationBlockerObjects?.length ?? null,
|
||||
saveSlotObjectCount: scene?.saveSlotObjects?.length ?? null,
|
||||
saveConfirmObjectCount: scene?.saveSlotConfirmObjects?.length ?? null,
|
||||
sceneKeys: Object.keys(window.__HEROS_GAME__?.scene.keys ?? {}),
|
||||
resources: performance
|
||||
.getEntriesByType('resource')
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => name.includes('BattleScene'))
|
||||
};
|
||||
});
|
||||
throw new Error(
|
||||
`BattleScene did not become active after camp navigation: ${JSON.stringify({
|
||||
diagnostic,
|
||||
delayedBattleModuleRequests,
|
||||
pageErrors
|
||||
})}`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
const navigationCompleteState = await page.evaluate(() => ({
|
||||
battleSceneStarts: window.__HEROS_INTERACTION_UX__?.battleSceneStarts ?? 0,
|
||||
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? []
|
||||
@@ -2260,6 +2299,23 @@ async function canReach(url) {
|
||||
}
|
||||
}
|
||||
|
||||
async function prewarmBattleSceneModule(url) {
|
||||
const parsed = new URL(url);
|
||||
const moduleUrl = new URL(
|
||||
'/heros_web/src/game/scenes/BattleScene.ts',
|
||||
parsed.origin
|
||||
);
|
||||
const response = await fetch(moduleUrl, {
|
||||
signal: AbortSignal.timeout(30000)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Could not prewarm the BattleScene module (${response.status}) at ${moduleUrl}.`
|
||||
);
|
||||
}
|
||||
await response.arrayBuffer();
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -62,18 +62,25 @@ try {
|
||||
await waitForUrl(serverUrl, 30_000);
|
||||
}
|
||||
|
||||
browser = await chromium.launch({
|
||||
headless:
|
||||
process.env.VERIFY_MANUAL_SAVE_UX_HEADLESS !==
|
||||
'0',
|
||||
args: [
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader'
|
||||
]
|
||||
});
|
||||
|
||||
for (const renderer of renderers) {
|
||||
await verifyRenderer(browser, renderer);
|
||||
browser = await chromium.launch({
|
||||
headless:
|
||||
process.env.VERIFY_MANUAL_SAVE_UX_HEADLESS !==
|
||||
'0',
|
||||
args:
|
||||
renderer === 'webgl'
|
||||
? [
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader'
|
||||
]
|
||||
: []
|
||||
});
|
||||
try {
|
||||
await verifyRenderer(browser, renderer);
|
||||
} finally {
|
||||
await browser.close();
|
||||
browser = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(
|
||||
@@ -504,7 +511,8 @@ async function verifyCampManualSaveIsolation(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-camp-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
@@ -748,7 +756,8 @@ async function verifyTitleCampRestore(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-title-camp-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForTitleFocus(
|
||||
@@ -915,7 +924,8 @@ async function verifyTitleCampRestore(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-title-confirm-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForTitleFocus(
|
||||
@@ -1200,7 +1210,8 @@ async function verifyBattleManualSaveIsolation(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-battle-save-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForBattleSavePanel(page, null);
|
||||
@@ -1364,7 +1375,8 @@ async function verifyBattleManualSaveIsolation(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-battle-load-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForBattleSavePanel(page, null);
|
||||
@@ -1437,7 +1449,8 @@ async function verifyTitleBattleRestore(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-title-battle-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
|
||||
await page.keyboard.press('1');
|
||||
@@ -1571,7 +1584,8 @@ async function verifyTitleBattleRestore(
|
||||
await page.screenshot({
|
||||
path:
|
||||
`dist/qa-manual-save-battle-restored-${renderer}-1920x1080.png`,
|
||||
fullPage: true
|
||||
animations: 'disabled',
|
||||
timeout: 90_000
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11842,6 +11842,7 @@ export class CampScene extends Phaser.Scene {
|
||||
musicVolume: this.campSoundscape.music.volume,
|
||||
ambienceVolume: this.campSoundscape.ambience.volume
|
||||
});
|
||||
this.input.setTopOnly(true);
|
||||
this.input.keyboard?.on('keydown-ESC', (event: KeyboardEvent) => this.handleEscapeKey(event));
|
||||
this.input.keyboard?.on('keydown-ENTER', (event: KeyboardEvent) => this.handleSortieEnterKey(event));
|
||||
this.input.keyboard?.on('keydown', (event: KeyboardEvent) => {
|
||||
@@ -13388,9 +13389,13 @@ export class CampScene extends Phaser.Scene {
|
||||
_localY: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveSlotPanel();
|
||||
this.deferCampSavePointerAction(
|
||||
event,
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveSlotPanel();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
shade.on(
|
||||
@@ -13462,9 +13467,13 @@ export class CampScene extends Phaser.Scene {
|
||||
_localY: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveSlotPanel();
|
||||
this.deferCampSavePointerAction(
|
||||
event,
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveSlotPanel();
|
||||
}
|
||||
);
|
||||
});
|
||||
this.trackCampSaveSlot(close);
|
||||
}
|
||||
@@ -13537,8 +13546,10 @@ export class CampScene extends Phaser.Scene {
|
||||
_localY: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
this.requestSaveCampToSlot(summary.slot);
|
||||
this.deferCampSavePointerAction(
|
||||
event,
|
||||
() => this.requestSaveCampToSlot(summary.slot)
|
||||
);
|
||||
};
|
||||
[row, title, keycap, keyText, detail, meta, saveLabel].forEach((object) => {
|
||||
object.setInteractive({ useHandCursor: true });
|
||||
@@ -13614,9 +13625,13 @@ export class CampScene extends Phaser.Scene {
|
||||
_localY: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveConfirm();
|
||||
this.deferCampSavePointerAction(
|
||||
event,
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveConfirm();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
shade.on(
|
||||
@@ -13690,8 +13705,10 @@ export class CampScene extends Phaser.Scene {
|
||||
_localY: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
this.confirmCampSaveOverwrite();
|
||||
this.deferCampSavePointerAction(
|
||||
event,
|
||||
() => this.confirmCampSaveOverwrite()
|
||||
);
|
||||
});
|
||||
this.trackCampSaveConfirm(confirm);
|
||||
|
||||
@@ -13706,9 +13723,13 @@ export class CampScene extends Phaser.Scene {
|
||||
_localY: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveConfirm();
|
||||
this.deferCampSavePointerAction(
|
||||
event,
|
||||
() => {
|
||||
soundDirector.playSelect();
|
||||
this.hideCampSaveConfirm();
|
||||
}
|
||||
);
|
||||
});
|
||||
this.trackCampSaveConfirm(cancel);
|
||||
}
|
||||
@@ -13805,6 +13826,20 @@ export class CampScene extends Phaser.Scene {
|
||||
this.pendingSaveSlot = undefined;
|
||||
}
|
||||
|
||||
private deferCampSavePointerAction(
|
||||
event:
|
||||
| { stopPropagation: () => void }
|
||||
| undefined,
|
||||
action: () => void
|
||||
) {
|
||||
event?.stopPropagation();
|
||||
queueMicrotask(() => {
|
||||
if (this.sys.isActive()) {
|
||||
action();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private settleVictoryRewardArrival(report: CampaignVictoryRewardReport) {
|
||||
if (report.outcome !== 'victory') {
|
||||
this.pendingVictoryRewardReport = undefined;
|
||||
@@ -14911,6 +14946,12 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private setSortiePrepStep(step: SortiePrepStep) {
|
||||
if (
|
||||
this.saveSlotObjects.length > 0 ||
|
||||
this.saveSlotConfirmObjects.length > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (step !== 'briefing') {
|
||||
this.thirdCampPreparationBrowserOpen = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user