fix: resume campaign progress safely
This commit is contained in:
622
scripts/verify-battle-save-generation-browser.mjs
Normal file
622
scripts/verify-battle-save-generation-browser.mjs
Normal file
@@ -0,0 +1,622 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { chromium } from 'playwright';
|
||||
import {
|
||||
desktopBrowserContextOptions,
|
||||
desktopBrowserDeviceScaleFactor,
|
||||
desktopBrowserViewport
|
||||
} from './desktop-browser-viewport.mjs';
|
||||
import {
|
||||
startVerificationPreview,
|
||||
stopVerificationPreview
|
||||
} from './verification-preview-server.mjs';
|
||||
|
||||
const renderers = ['canvas', 'webgl'];
|
||||
const renderer = process.env.VERIFY_BATTLE_SAVE_GENERATION_RENDERER;
|
||||
|
||||
if (!renderer) {
|
||||
for (const requestedRenderer of renderers) {
|
||||
const result = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
VERIFY_BATTLE_SAVE_GENERATION_RENDERER: requestedRenderer
|
||||
},
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
assert(
|
||||
renderers.includes(renderer),
|
||||
`Unsupported battle-save generation renderer "${renderer}".`
|
||||
);
|
||||
|
||||
const battleId = 'first-battle-zhuo-commandery';
|
||||
const campaignStep = 'first-battle';
|
||||
const campaignGeneration = 4;
|
||||
const staleGeneration = 3;
|
||||
const campaignKey = 'heros-web:campaign-state';
|
||||
const campaignSlotKey = `${campaignKey}:slot-1`;
|
||||
const battleBaseKey = `heros-web:battle:${battleId}`;
|
||||
const battleSlotKey = `${battleBaseKey}:slot-1`;
|
||||
const legacyBattleKey = 'heros-web:first-battle-state';
|
||||
const battleStorageKeys = [battleSlotKey, battleBaseKey, legacyBattleKey];
|
||||
const baseUrl =
|
||||
process.env.VERIFY_BATTLE_SAVE_GENERATION_URL ??
|
||||
`http://127.0.0.1:${renderer === 'canvas' ? 41825 : 41826}/heros_web/`;
|
||||
const targetUrl = withDebugOptions(baseUrl, renderer);
|
||||
|
||||
let browser;
|
||||
let serverProcess;
|
||||
|
||||
try {
|
||||
serverProcess = await startVerificationPreview({
|
||||
targetUrl,
|
||||
explicitUrl: Boolean(
|
||||
process.env.VERIFY_BATTLE_SAVE_GENERATION_URL
|
||||
),
|
||||
label:
|
||||
`${renderer} battle save generation verification`
|
||||
});
|
||||
browser = await chromium.launch({
|
||||
headless: process.env.VERIFY_BATTLE_SAVE_GENERATION_HEADLESS !== '0',
|
||||
args:
|
||||
renderer === 'webgl'
|
||||
? ['--use-angle=swiftshader', '--enable-unsafe-swiftshader']
|
||||
: []
|
||||
});
|
||||
const context = await browser.newContext(desktopBrowserContextOptions);
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(30000);
|
||||
|
||||
const pageErrors = [];
|
||||
const consoleErrors = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.stack ?? error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(targetUrl, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 90000
|
||||
});
|
||||
await waitForDebugApi(page);
|
||||
await page.evaluate(() => window.localStorage.clear());
|
||||
await page.reload({
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 90000
|
||||
});
|
||||
await waitForTitle(page);
|
||||
await assertDesktopRuntime(page, renderer);
|
||||
|
||||
await page.evaluate(async (requestedBattleId) => {
|
||||
await window.__HEROS_DEBUG__?.goToBattle(requestedBattleId);
|
||||
}, battleId);
|
||||
await waitForBattleReady(page);
|
||||
|
||||
const fixture = await page.evaluate(
|
||||
({
|
||||
requestedBattleId,
|
||||
requestedCampaignStep,
|
||||
requestedCampaignGeneration,
|
||||
requestedStaleGeneration,
|
||||
requestedCampaignKey,
|
||||
requestedCampaignSlotKey,
|
||||
requestedBattleBaseKey,
|
||||
requestedBattleSlotKey,
|
||||
requestedLegacyBattleKey,
|
||||
requestedBattleStorageKeys
|
||||
}) => {
|
||||
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
if (
|
||||
!scene ||
|
||||
typeof scene.createBattleSaveState !== 'function' ||
|
||||
typeof scene.saveBattleState !== 'function'
|
||||
) {
|
||||
throw new Error('BattleScene save internals are unavailable.');
|
||||
}
|
||||
|
||||
// Persist a normalized campaign fixture and a fully valid battle-save
|
||||
// shape through the production writer before applying generation markers.
|
||||
scene.saveBattleState(1);
|
||||
const campaignRaw =
|
||||
window.localStorage.getItem(requestedCampaignSlotKey) ??
|
||||
window.localStorage.getItem(requestedCampaignKey);
|
||||
const battleRaw =
|
||||
window.localStorage.getItem(requestedBattleSlotKey) ??
|
||||
window.localStorage.getItem(requestedBattleBaseKey);
|
||||
if (!campaignRaw || !battleRaw) {
|
||||
throw new Error('The production save writer did not create the browser fixture.');
|
||||
}
|
||||
|
||||
const campaign = JSON.parse(campaignRaw);
|
||||
campaign.step = requestedCampaignStep;
|
||||
campaign.activeSaveSlot = 1;
|
||||
campaign.battleResumeGeneration = requestedCampaignGeneration;
|
||||
campaign.storyCheckpoint = undefined;
|
||||
campaign.explorationCheckpoint = undefined;
|
||||
campaign.updatedAt = new Date().toISOString();
|
||||
const serializedCampaign = JSON.stringify(campaign);
|
||||
window.localStorage.setItem(requestedCampaignSlotKey, serializedCampaign);
|
||||
window.localStorage.setItem(requestedCampaignKey, serializedCampaign);
|
||||
|
||||
const baseline = JSON.parse(battleRaw);
|
||||
const markerIndex = baseline.units.findIndex((unit) => unit.id === 'liu-bei');
|
||||
if (markerIndex < 0) {
|
||||
throw new Error('The first-battle Liu Bei save fixture is unavailable.');
|
||||
}
|
||||
const baselineMarkerHp = baseline.units[markerIndex].hp;
|
||||
const markerMaxHp = baseline.units[markerIndex].maxHp;
|
||||
const staleMarkerHp = Math.max(1, Math.min(markerMaxHp, baselineMarkerHp - 7));
|
||||
const validMarkerHp = Math.max(1, Math.min(markerMaxHp, baselineMarkerHp - 3));
|
||||
if (
|
||||
staleMarkerHp === baselineMarkerHp ||
|
||||
validMarkerHp === baselineMarkerHp ||
|
||||
staleMarkerHp === validMarkerHp
|
||||
) {
|
||||
throw new Error(
|
||||
`Battle marker HP values are not distinguishable: ${JSON.stringify({
|
||||
baselineMarkerHp,
|
||||
staleMarkerHp,
|
||||
validMarkerHp
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
const stale = structuredClone(baseline);
|
||||
stale.battleId = requestedBattleId;
|
||||
stale.campaignStep = requestedCampaignStep;
|
||||
stale.battleResumeGeneration = requestedStaleGeneration;
|
||||
stale.turnNumber = 8;
|
||||
stale.activeFaction = 'ally';
|
||||
stale.savedAt = new Date(Date.now() - 60_000).toISOString();
|
||||
stale.units[markerIndex].hp = staleMarkerHp;
|
||||
|
||||
const valid = structuredClone(baseline);
|
||||
valid.battleId = requestedBattleId;
|
||||
valid.campaignStep = requestedCampaignStep;
|
||||
valid.battleResumeGeneration = requestedCampaignGeneration;
|
||||
valid.turnNumber = 6;
|
||||
valid.activeFaction = 'ally';
|
||||
valid.savedAt = new Date().toISOString();
|
||||
valid.units[markerIndex].hp = validMarkerHp;
|
||||
|
||||
window.localStorage.removeItem(requestedBattleBaseKey);
|
||||
window.localStorage.removeItem(requestedLegacyBattleKey);
|
||||
window.localStorage.setItem(requestedBattleSlotKey, JSON.stringify(stale));
|
||||
|
||||
const storagePrototype = Object.getPrototypeOf(window.localStorage);
|
||||
const originalRemoveItem = storagePrototype.removeItem;
|
||||
const blockedKeys = new Set(requestedBattleStorageKeys);
|
||||
const removalStats = {
|
||||
attempts: 0,
|
||||
blocked: 0,
|
||||
keys: []
|
||||
};
|
||||
storagePrototype.removeItem = function removeItem(key) {
|
||||
const normalizedKey = String(key);
|
||||
if (this === window.localStorage && blockedKeys.has(normalizedKey)) {
|
||||
removalStats.attempts += 1;
|
||||
removalStats.blocked += 1;
|
||||
removalStats.keys.push(normalizedKey);
|
||||
throw new DOMException(
|
||||
'Injected battle-save removal refusal',
|
||||
'InvalidStateError'
|
||||
);
|
||||
}
|
||||
return originalRemoveItem.call(this, key);
|
||||
};
|
||||
|
||||
Object.defineProperty(window, '__HEROS_BATTLE_GENERATION_FIXTURE__', {
|
||||
configurable: true,
|
||||
value: {
|
||||
baselineMarkerHp,
|
||||
staleMarkerHp,
|
||||
validMarkerHp,
|
||||
valid,
|
||||
removalStats
|
||||
}
|
||||
});
|
||||
|
||||
scene.scene.restart({
|
||||
battleId: requestedBattleId,
|
||||
resumeBattleSaveSlot: 1
|
||||
});
|
||||
|
||||
return {
|
||||
baselineMarkerHp,
|
||||
staleMarkerHp,
|
||||
validMarkerHp,
|
||||
staleTurnNumber: stale.turnNumber,
|
||||
validTurnNumber: valid.turnNumber
|
||||
};
|
||||
},
|
||||
{
|
||||
requestedBattleId: battleId,
|
||||
requestedCampaignStep: campaignStep,
|
||||
requestedCampaignGeneration: campaignGeneration,
|
||||
requestedStaleGeneration: staleGeneration,
|
||||
requestedCampaignKey: campaignKey,
|
||||
requestedCampaignSlotKey: campaignSlotKey,
|
||||
requestedBattleBaseKey: battleBaseKey,
|
||||
requestedBattleSlotKey: battleSlotKey,
|
||||
requestedLegacyBattleKey: legacyBattleKey,
|
||||
requestedBattleStorageKeys: battleStorageKeys
|
||||
}
|
||||
);
|
||||
|
||||
await page.waitForFunction(
|
||||
({ expectedBattleId, expectedBaselineHp }) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle?.();
|
||||
const fixtureState = window.__HEROS_BATTLE_GENERATION_FIXTURE__;
|
||||
return (
|
||||
battle?.battleId === expectedBattleId &&
|
||||
battle?.mapBackgroundReady === true &&
|
||||
['deployment', 'idle'].includes(battle?.phase) &&
|
||||
battle?.turnNumber === 1 &&
|
||||
battle?.units?.find((unit) => unit.id === 'liu-bei')?.hp ===
|
||||
expectedBaselineHp &&
|
||||
fixtureState?.removalStats?.blocked >= 1
|
||||
);
|
||||
},
|
||||
{
|
||||
expectedBattleId: battleId,
|
||||
expectedBaselineHp: fixture.baselineMarkerHp
|
||||
},
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
|
||||
const staleProbe = await page.evaluate(
|
||||
({
|
||||
requestedBattleSlotKey,
|
||||
requestedStaleGeneration
|
||||
}) => {
|
||||
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
if (!scene) {
|
||||
throw new Error('BattleScene is unavailable after stale resume rejection.');
|
||||
}
|
||||
const rejected =
|
||||
scene.readBattleSaveState?.(1, true) === undefined;
|
||||
const loadDisabled =
|
||||
scene.isMapMenuActionDisabled?.('load') === true;
|
||||
scene.showSaveSlotPanel?.('load');
|
||||
const panelBeforeSelection =
|
||||
window.__HEROS_DEBUG__?.battle?.();
|
||||
scene.activateSaveSlotPanelSlot?.(1);
|
||||
const panelAfterSelection =
|
||||
window.__HEROS_DEBUG__?.battle?.();
|
||||
scene.hideSaveSlotPanel?.();
|
||||
|
||||
const persisted = JSON.parse(
|
||||
window.localStorage.getItem(requestedBattleSlotKey) ?? 'null'
|
||||
);
|
||||
const fixtureState =
|
||||
window.__HEROS_BATTLE_GENERATION_FIXTURE__;
|
||||
return {
|
||||
rejected,
|
||||
loadDisabled,
|
||||
panelMode:
|
||||
panelBeforeSelection?.saveSlotPanelMode ?? null,
|
||||
panelStayedOpen:
|
||||
panelAfterSelection?.saveSlotPanelVisible === true,
|
||||
promptMode:
|
||||
panelAfterSelection?.turnPromptMode ?? null,
|
||||
persistedGeneration:
|
||||
persisted?.battleResumeGeneration ?? null,
|
||||
removalStats: {
|
||||
attempts:
|
||||
fixtureState?.removalStats?.attempts ?? 0,
|
||||
blocked:
|
||||
fixtureState?.removalStats?.blocked ?? 0,
|
||||
keys: [
|
||||
...(fixtureState?.removalStats?.keys ?? [])
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
requestedBattleSlotKey: battleSlotKey,
|
||||
requestedStaleGeneration: staleGeneration
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
staleProbe.rejected,
|
||||
true,
|
||||
`${renderer}: BattleScene accepted a stale generation.`
|
||||
);
|
||||
assert.equal(
|
||||
staleProbe.loadDisabled,
|
||||
true,
|
||||
`${renderer}: manual Load remained enabled for a stale save.`
|
||||
);
|
||||
assert.equal(staleProbe.panelMode, 'load');
|
||||
assert.equal(
|
||||
staleProbe.panelStayedOpen,
|
||||
true,
|
||||
`${renderer}: selecting a stale Load row should be a no-op.`
|
||||
);
|
||||
assert.equal(
|
||||
staleProbe.promptMode,
|
||||
null,
|
||||
`${renderer}: a stale save reached the Load confirmation prompt.`
|
||||
);
|
||||
assert.equal(
|
||||
staleProbe.persistedGeneration,
|
||||
staleGeneration,
|
||||
`${renderer}: the removal-refusal fixture was not preserved.`
|
||||
);
|
||||
assert(
|
||||
staleProbe.removalStats.blocked >= 1,
|
||||
`${renderer}: no stale-save cleanup refusal was exercised.`
|
||||
);
|
||||
assert(
|
||||
staleProbe.removalStats.keys.every((key) =>
|
||||
battleStorageKeys.includes(key)
|
||||
),
|
||||
`${renderer}: removal refusal escaped the intended battle keys.`
|
||||
);
|
||||
|
||||
const beforeControl = await page.evaluate(
|
||||
({
|
||||
requestedBattleSlotKey,
|
||||
requestedCampaignGeneration
|
||||
}) => {
|
||||
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
const fixtureState =
|
||||
window.__HEROS_BATTLE_GENERATION_FIXTURE__;
|
||||
if (!scene || !fixtureState?.valid) {
|
||||
throw new Error('The current-generation control fixture is unavailable.');
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
requestedBattleSlotKey,
|
||||
JSON.stringify(fixtureState.valid)
|
||||
);
|
||||
const accepted = scene.readBattleSaveState?.(1, true);
|
||||
const loadDisabled =
|
||||
scene.isMapMenuActionDisabled?.('load') === true;
|
||||
scene.showSaveSlotPanel?.('load');
|
||||
const panel =
|
||||
window.__HEROS_DEBUG__?.battle?.();
|
||||
scene.activateSaveSlotPanelSlot?.(1);
|
||||
const confirmation =
|
||||
window.__HEROS_DEBUG__?.battle?.();
|
||||
|
||||
return {
|
||||
acceptedGeneration:
|
||||
accepted?.battleResumeGeneration ?? null,
|
||||
acceptedTurnNumber:
|
||||
accepted?.turnNumber ?? null,
|
||||
loadDisabled,
|
||||
panelMode: panel?.saveSlotPanelMode ?? null,
|
||||
confirmationMode:
|
||||
confirmation?.turnPromptMode ?? null,
|
||||
confirmationVisible:
|
||||
confirmation?.turnPromptVisible === true,
|
||||
expectedGeneration: requestedCampaignGeneration
|
||||
};
|
||||
},
|
||||
{
|
||||
requestedBattleSlotKey: battleSlotKey,
|
||||
requestedCampaignGeneration: campaignGeneration
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
beforeControl.acceptedGeneration,
|
||||
campaignGeneration,
|
||||
`${renderer}: the current-generation control was not accepted.`
|
||||
);
|
||||
assert.equal(
|
||||
beforeControl.acceptedTurnNumber,
|
||||
fixture.validTurnNumber
|
||||
);
|
||||
assert.equal(beforeControl.loadDisabled, false);
|
||||
assert.equal(beforeControl.panelMode, 'load');
|
||||
assert.equal(beforeControl.confirmationMode, 'load-confirm');
|
||||
assert.equal(beforeControl.confirmationVisible, true);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
scene?.activateTurnPromptPrimaryAction?.();
|
||||
});
|
||||
await page.waitForFunction(
|
||||
({
|
||||
expectedBattleId,
|
||||
expectedTurnNumber,
|
||||
expectedMarkerHp
|
||||
}) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle?.();
|
||||
return (
|
||||
battle?.battleId === expectedBattleId &&
|
||||
battle?.turnNumber === expectedTurnNumber &&
|
||||
battle?.activeFaction === 'ally' &&
|
||||
battle?.phase === 'idle' &&
|
||||
battle?.units?.find((unit) => unit.id === 'liu-bei')?.hp ===
|
||||
expectedMarkerHp
|
||||
);
|
||||
},
|
||||
{
|
||||
expectedBattleId: battleId,
|
||||
expectedTurnNumber: fixture.validTurnNumber,
|
||||
expectedMarkerHp: fixture.validMarkerHp
|
||||
},
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
|
||||
const restored = await page.evaluate(
|
||||
({
|
||||
requestedCampaignKey,
|
||||
requestedCampaignSlotKey
|
||||
}) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle?.();
|
||||
const current = JSON.parse(
|
||||
window.localStorage.getItem(requestedCampaignKey) ?? 'null'
|
||||
);
|
||||
const slot = JSON.parse(
|
||||
window.localStorage.getItem(requestedCampaignSlotKey) ?? 'null'
|
||||
);
|
||||
return {
|
||||
turnNumber: battle?.turnNumber ?? null,
|
||||
activeFaction: battle?.activeFaction ?? null,
|
||||
markerHp:
|
||||
battle?.units?.find((unit) => unit.id === 'liu-bei')?.hp ?? null,
|
||||
campaignGeneration:
|
||||
current?.battleResumeGeneration ?? null,
|
||||
slotGeneration:
|
||||
slot?.battleResumeGeneration ?? null,
|
||||
campaignStep: current?.step ?? null,
|
||||
slotStep: slot?.step ?? null
|
||||
};
|
||||
},
|
||||
{
|
||||
requestedCampaignKey: campaignKey,
|
||||
requestedCampaignSlotKey: campaignSlotKey
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(restored.turnNumber, fixture.validTurnNumber);
|
||||
assert.equal(restored.activeFaction, 'ally');
|
||||
assert.equal(restored.markerHp, fixture.validMarkerHp);
|
||||
assert.equal(restored.campaignGeneration, campaignGeneration);
|
||||
assert.equal(restored.slotGeneration, campaignGeneration);
|
||||
assert.equal(restored.campaignStep, campaignStep);
|
||||
assert.equal(restored.slotStep, campaignStep);
|
||||
assert.equal(pageErrors.length, 0, pageErrors.join('\n'));
|
||||
assert.equal(consoleErrors.length, 0, consoleErrors.join('\n'));
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
renderer,
|
||||
viewport: desktopBrowserViewport,
|
||||
deviceScaleFactor: desktopBrowserDeviceScaleFactor,
|
||||
cssZoom: 1,
|
||||
stale: {
|
||||
campaignGeneration,
|
||||
saveGeneration: staleGeneration,
|
||||
requestedTurnNumber: fixture.staleTurnNumber,
|
||||
liveTurnNumber: 1,
|
||||
loadDisabled: staleProbe.loadDisabled,
|
||||
removalRefusal: staleProbe.removalStats
|
||||
},
|
||||
control: {
|
||||
campaignGeneration,
|
||||
saveGeneration: beforeControl.acceptedGeneration,
|
||||
restoredTurnNumber: restored.turnNumber,
|
||||
restoredMarkerHp: restored.markerHp
|
||||
},
|
||||
pageErrors: pageErrors.length,
|
||||
consoleErrors: consoleErrors.length
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await stopVerificationPreview(serverProcess);
|
||||
}
|
||||
|
||||
function withDebugOptions(url, requestedRenderer) {
|
||||
const parsed = new URL(url);
|
||||
parsed.searchParams.set('debug', '1');
|
||||
parsed.searchParams.set('renderer', requestedRenderer);
|
||||
parsed.searchParams.set('motion', 'reduced');
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
async function waitForDebugApi(page) {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
Boolean(
|
||||
document.querySelector('canvas') &&
|
||||
window.__HEROS_GAME__ &&
|
||||
window.__HEROS_DEBUG__
|
||||
),
|
||||
undefined,
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForTitle(page) {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__HEROS_DEBUG__?.activeScenes?.().includes('TitleScene'),
|
||||
undefined,
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForBattleReady(page) {
|
||||
await page.waitForFunction(
|
||||
(expectedBattleId) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle?.();
|
||||
return (
|
||||
window.__HEROS_DEBUG__?.activeScenes?.().includes('BattleScene') &&
|
||||
battle?.scene === 'BattleScene' &&
|
||||
battle?.battleId === expectedBattleId &&
|
||||
battle?.battleOutcome === null &&
|
||||
['deployment', 'idle'].includes(battle?.phase) &&
|
||||
battle?.mapBackgroundReady === true &&
|
||||
battle?.resultVisible === false
|
||||
);
|
||||
},
|
||||
battleId,
|
||||
{ timeout: 90000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function assertDesktopRuntime(page, expectedRenderer) {
|
||||
const runtime = await page.evaluate(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
const bounds = canvas?.getBoundingClientRect();
|
||||
return {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
visualViewportScale: window.visualViewport?.scale ?? 1,
|
||||
rootCssZoom: getComputedStyle(document.documentElement).zoom,
|
||||
rendererType: window.__HEROS_GAME__?.renderer.type,
|
||||
canvas: canvas
|
||||
? {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
clientWidth: canvas.clientWidth,
|
||||
clientHeight: canvas.clientHeight,
|
||||
bounds: bounds
|
||||
? {
|
||||
width: bounds.width,
|
||||
height: bounds.height
|
||||
}
|
||||
: null
|
||||
}
|
||||
: null
|
||||
};
|
||||
});
|
||||
|
||||
assert.equal(runtime.innerWidth, desktopBrowserViewport.width);
|
||||
assert.equal(runtime.innerHeight, desktopBrowserViewport.height);
|
||||
assert.equal(runtime.devicePixelRatio, desktopBrowserDeviceScaleFactor);
|
||||
assert.equal(runtime.visualViewportScale, 1);
|
||||
assert(
|
||||
runtime.rootCssZoom === '1' || runtime.rootCssZoom === 'normal',
|
||||
`Unexpected CSS zoom ${runtime.rootCssZoom}.`
|
||||
);
|
||||
assert.equal(runtime.rendererType, expectedRenderer === 'canvas' ? 1 : 2);
|
||||
assert.equal(runtime.canvas?.width, desktopBrowserViewport.width);
|
||||
assert.equal(runtime.canvas?.height, desktopBrowserViewport.height);
|
||||
assert.equal(runtime.canvas?.clientWidth, desktopBrowserViewport.width);
|
||||
assert.equal(runtime.canvas?.clientHeight, desktopBrowserViewport.height);
|
||||
assert.equal(runtime.canvas?.bounds?.width, desktopBrowserViewport.width);
|
||||
assert.equal(runtime.canvas?.bounds?.height, desktopBrowserViewport.height);
|
||||
}
|
||||
Reference in New Issue
Block a user