fix: sequence battle feedback and autosaves
This commit is contained in:
@@ -47,6 +47,11 @@ const battleBaseKey = `heros-web:battle:${battleId}`;
|
||||
const battleSlotKey = `${battleBaseKey}:slot-1`;
|
||||
const legacyBattleKey = 'heros-web:first-battle-state';
|
||||
const battleStorageKeys = [battleSlotKey, battleBaseKey, legacyBattleKey];
|
||||
const otherBattleBaseKey =
|
||||
'heros-web:battle:second-battle-yellow-turban-pursuit';
|
||||
const otherBattleSlotKey = `${otherBattleBaseKey}:slot-1`;
|
||||
const otherBattleCheckpoint =
|
||||
'{"sentinel":"preserve-other-battle-checkpoint"}';
|
||||
const baseUrl =
|
||||
process.env.VERIFY_BATTLE_SAVE_GENERATION_URL ??
|
||||
`http://127.0.0.1:${renderer === 'canvas' ? 41825 : 41826}/heros_web/`;
|
||||
@@ -222,7 +227,10 @@ try {
|
||||
staleMarkerHp,
|
||||
validMarkerHp,
|
||||
valid,
|
||||
removalStats
|
||||
removalStats,
|
||||
restoreRemovalInterceptor() {
|
||||
storagePrototype.removeItem = originalRemoveItem;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -491,6 +499,606 @@ try {
|
||||
assert.equal(restored.slotGeneration, campaignGeneration);
|
||||
assert.equal(restored.campaignStep, campaignStep);
|
||||
assert.equal(restored.slotStep, campaignStep);
|
||||
|
||||
const tutorialPendingEventProbe = await page.evaluate(() => {
|
||||
const scene =
|
||||
window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
if (
|
||||
!scene ||
|
||||
typeof scene.scheduleFirstBattleTutorial !==
|
||||
'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'The first-battle tutorial resume probe is unavailable.'
|
||||
);
|
||||
}
|
||||
const originalStart =
|
||||
scene.startFirstBattleTutorial;
|
||||
const originalDelayedCall =
|
||||
scene.time.delayedCall;
|
||||
const originalState = {
|
||||
phase: scene.phase,
|
||||
activeFaction: scene.activeFaction,
|
||||
turnNumber: scene.turnNumber,
|
||||
firstBattleTutorialStep:
|
||||
scene.firstBattleTutorialStep,
|
||||
activeBattleEvent: scene.activeBattleEvent,
|
||||
battleEventQueue: scene.battleEventQueue,
|
||||
battleEventObjects: scene.battleEventObjects
|
||||
};
|
||||
let tutorialStartCalls = 0;
|
||||
let retryCalls = 0;
|
||||
try {
|
||||
scene.phase = 'idle';
|
||||
scene.activeFaction = 'ally';
|
||||
scene.turnNumber = 1;
|
||||
scene.firstBattleTutorialStep = undefined;
|
||||
scene.activeBattleEvent = undefined;
|
||||
scene.battleEventObjects = [];
|
||||
scene.battleEventQueue = [{
|
||||
key: 'qa-restored-pending-event',
|
||||
title: 'Restored pending event',
|
||||
lines: ['This event must display before tutorial input.'],
|
||||
priority: 'normal',
|
||||
playCue: false
|
||||
}];
|
||||
scene.startFirstBattleTutorial = () => {
|
||||
tutorialStartCalls += 1;
|
||||
return true;
|
||||
};
|
||||
scene.time.delayedCall = () => {
|
||||
retryCalls += 1;
|
||||
return { remove() {} };
|
||||
};
|
||||
scene.scheduleFirstBattleTutorial();
|
||||
return {
|
||||
tutorialStartCalls,
|
||||
retryCalls,
|
||||
pendingEventCount:
|
||||
scene.battleEventQueue.length
|
||||
};
|
||||
} finally {
|
||||
scene.startFirstBattleTutorial = originalStart;
|
||||
scene.time.delayedCall = originalDelayedCall;
|
||||
scene.phase = originalState.phase;
|
||||
scene.activeFaction = originalState.activeFaction;
|
||||
scene.turnNumber = originalState.turnNumber;
|
||||
scene.firstBattleTutorialStep =
|
||||
originalState.firstBattleTutorialStep;
|
||||
scene.activeBattleEvent =
|
||||
originalState.activeBattleEvent;
|
||||
scene.battleEventQueue =
|
||||
originalState.battleEventQueue;
|
||||
scene.battleEventObjects =
|
||||
originalState.battleEventObjects;
|
||||
}
|
||||
});
|
||||
assert.deepEqual(
|
||||
tutorialPendingEventProbe,
|
||||
{
|
||||
tutorialStartCalls: 0,
|
||||
retryCalls: 1,
|
||||
pendingEventCount: 1
|
||||
},
|
||||
`${renderer}: a restored pending battle event was not allowed to finish before the first-battle tutorial resumed.`
|
||||
);
|
||||
|
||||
const autosaveContract = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
const battle = window.__HEROS_DEBUG__?.battle?.();
|
||||
const fixtureState =
|
||||
window.__HEROS_BATTLE_GENERATION_FIXTURE__;
|
||||
fixtureState?.restoreRemovalInterceptor?.();
|
||||
return {
|
||||
sceneMethod:
|
||||
typeof scene?.persistBattleAutosave === 'function',
|
||||
debugState:
|
||||
battle?.autosave ?? null
|
||||
};
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
autosaveContract.sceneMethod,
|
||||
true,
|
||||
`${renderer}: BattleScene.persistBattleAutosave(reason) is unavailable.`
|
||||
);
|
||||
assertAutosaveDebugState(
|
||||
autosaveContract.debugState,
|
||||
`${renderer}: initial autosave debug state`
|
||||
);
|
||||
|
||||
const actionBaseline = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
const actionTrigger = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
const unit = scene?.debugUnitById?.('liu-bei');
|
||||
if (
|
||||
!scene ||
|
||||
!unit ||
|
||||
typeof scene.completeUnitAction !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'The ally-action autosave trigger is unavailable.'
|
||||
);
|
||||
}
|
||||
scene.phase = 'command';
|
||||
scene.activeFaction = 'ally';
|
||||
scene.selectedUnit = unit;
|
||||
scene.pendingMove = undefined;
|
||||
scene.targetingAction = undefined;
|
||||
scene.selectedUsable = undefined;
|
||||
scene.completeUnitAction(unit, 'wait');
|
||||
return {
|
||||
phase:
|
||||
window.__HEROS_DEBUG__?.battle?.()?.phase ?? null,
|
||||
actedUnitIds: [
|
||||
...(window.__HEROS_DEBUG__?.battle?.()?.actedUnitIds ?? [])
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
assert(
|
||||
actionTrigger.actedUnitIds.includes('liu-bei'),
|
||||
`${renderer}: the ally action did not complete.`
|
||||
);
|
||||
|
||||
await page.waitForFunction(
|
||||
({
|
||||
requestedBattleSlotKey,
|
||||
previousSavedAt,
|
||||
previousWriteCount
|
||||
}) => {
|
||||
const persisted = JSON.parse(
|
||||
window.localStorage.getItem(requestedBattleSlotKey) ??
|
||||
'null'
|
||||
);
|
||||
const autosave =
|
||||
window.__HEROS_DEBUG__?.battle?.()?.autosave;
|
||||
return (
|
||||
persisted?.savedAt &&
|
||||
persisted.savedAt !== previousSavedAt &&
|
||||
persisted.actedUnitIds?.includes('liu-bei') &&
|
||||
autosave?.writeCount === previousWriteCount + 1 &&
|
||||
autosave?.lastResult === 'saved' &&
|
||||
autosave?.lastReason === 'ally-action'
|
||||
);
|
||||
},
|
||||
{
|
||||
requestedBattleSlotKey: battleSlotKey,
|
||||
previousSavedAt: actionBaseline.state.savedAt,
|
||||
previousWriteCount:
|
||||
actionBaseline.autosave.writeCount
|
||||
}
|
||||
);
|
||||
|
||||
const actionAutosave = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
assertAutosaveWrite(
|
||||
actionBaseline,
|
||||
actionAutosave,
|
||||
{
|
||||
reason: 'ally-action',
|
||||
expectedTurnNumber: fixture.validTurnNumber,
|
||||
expectedActedUnitId: 'liu-bei'
|
||||
},
|
||||
`${renderer}: ally action autosave`
|
||||
);
|
||||
|
||||
const unsafeAutosaves = [];
|
||||
for (const [
|
||||
unsafeIndex,
|
||||
unsafePhase
|
||||
] of ['command', 'animating'].entries()) {
|
||||
const beforeUnsafe = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
const unsafeTurnNumber = 41 + unsafeIndex;
|
||||
const eventProbe = await page.evaluate(
|
||||
({
|
||||
requestedPhase,
|
||||
requestedTurnNumber
|
||||
}) => {
|
||||
const scene =
|
||||
window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
const unit = scene?.debugUnitById?.('liu-bei');
|
||||
if (
|
||||
!scene ||
|
||||
!unit ||
|
||||
typeof scene.persistBattleAutosave !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'The unsafe pagehide autosave probe is unavailable.'
|
||||
);
|
||||
}
|
||||
scene.turnNumber = requestedTurnNumber;
|
||||
scene.activeFaction = 'ally';
|
||||
scene.phase = requestedPhase;
|
||||
scene.selectedUnit =
|
||||
requestedPhase === 'command' ? unit : undefined;
|
||||
scene.pendingMove = undefined;
|
||||
scene.targetingAction = undefined;
|
||||
scene.selectedUsable = undefined;
|
||||
window.dispatchEvent(new Event('pagehide'));
|
||||
const battle =
|
||||
window.__HEROS_DEBUG__?.battle?.();
|
||||
return {
|
||||
phase: battle?.phase ?? null,
|
||||
turnNumber: battle?.turnNumber ?? null,
|
||||
autosave: battle?.autosave ?? null
|
||||
};
|
||||
},
|
||||
{
|
||||
requestedPhase: unsafePhase,
|
||||
requestedTurnNumber: unsafeTurnNumber
|
||||
}
|
||||
);
|
||||
const afterUnsafe = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
|
||||
assert.equal(eventProbe.phase, unsafePhase);
|
||||
assert.equal(
|
||||
eventProbe.turnNumber,
|
||||
unsafeTurnNumber
|
||||
);
|
||||
assertAutosaveDebugState(
|
||||
eventProbe.autosave,
|
||||
`${renderer}: ${unsafePhase} pagehide debug state`
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.raw,
|
||||
beforeUnsafe.raw,
|
||||
`${renderer}: ${unsafePhase} pagehide overwrote the last safe checkpoint.`
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.autosave.writeCount,
|
||||
beforeUnsafe.autosave.writeCount,
|
||||
`${renderer}: ${unsafePhase} pagehide incremented the autosave write count.`
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.autosave.skipCount,
|
||||
beforeUnsafe.autosave.skipCount + 1,
|
||||
`${renderer}: ${unsafePhase} pagehide did not record one safe-state skip.`
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.autosave.failureCount,
|
||||
beforeUnsafe.autosave.failureCount,
|
||||
`${renderer}: ${unsafePhase} pagehide was reported as a failure instead of a skip.`
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.autosave.lastResult,
|
||||
'skipped'
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.autosave.lastReason,
|
||||
'pagehide'
|
||||
);
|
||||
assert.equal(
|
||||
afterUnsafe.autosave.lastSavedAt,
|
||||
beforeUnsafe.state.savedAt
|
||||
);
|
||||
unsafeAutosaves.push({
|
||||
phase: unsafePhase,
|
||||
turnNumber: unsafeTurnNumber,
|
||||
writeCount: afterUnsafe.autosave.writeCount,
|
||||
skipCount: afterUnsafe.autosave.skipCount,
|
||||
checkpointSavedAt: afterUnsafe.state.savedAt
|
||||
});
|
||||
}
|
||||
|
||||
const readinessBaseline = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
const readinessTurnNumber = 42;
|
||||
const readinessProbe = await page.evaluate(
|
||||
(requestedTurnNumber) => {
|
||||
const scene =
|
||||
window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
if (!scene) {
|
||||
throw new Error(
|
||||
'The autosave readiness probe is unavailable.'
|
||||
);
|
||||
}
|
||||
scene.turnNumber = requestedTurnNumber;
|
||||
scene.activeFaction = 'ally';
|
||||
scene.phase = 'idle';
|
||||
scene.selectedUnit = undefined;
|
||||
scene.pendingMove = undefined;
|
||||
scene.targetingAction = undefined;
|
||||
scene.selectedUsable = undefined;
|
||||
scene.battleAutosaveReady = false;
|
||||
window.dispatchEvent(new Event('pagehide'));
|
||||
const blocked =
|
||||
window.__HEROS_DEBUG__?.battle?.()?.autosave ??
|
||||
null;
|
||||
scene.battleAutosaveReady = true;
|
||||
return blocked;
|
||||
},
|
||||
readinessTurnNumber
|
||||
);
|
||||
const readinessAfter = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
assertAutosaveDebugState(
|
||||
readinessProbe,
|
||||
`${renderer}: pre-initialization pagehide debug state`
|
||||
);
|
||||
assert.equal(
|
||||
readinessAfter.raw,
|
||||
readinessBaseline.raw,
|
||||
`${renderer}: a pagehide before battle initialization replaced the last safe checkpoint.`
|
||||
);
|
||||
assert.equal(
|
||||
readinessAfter.autosave.writeCount,
|
||||
readinessBaseline.autosave.writeCount
|
||||
);
|
||||
assert.equal(
|
||||
readinessAfter.autosave.skipCount,
|
||||
readinessBaseline.autosave.skipCount + 1
|
||||
);
|
||||
assert.equal(
|
||||
readinessAfter.autosave.lastResult,
|
||||
'skipped'
|
||||
);
|
||||
assert.equal(
|
||||
readinessAfter.autosave.lastReason,
|
||||
'pagehide'
|
||||
);
|
||||
|
||||
const idleBaseline = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
const idleTurnNumber = 43;
|
||||
const idleMarkerHp = Math.max(
|
||||
1,
|
||||
fixture.validMarkerHp - 1
|
||||
);
|
||||
await page.evaluate(
|
||||
({
|
||||
requestedTurnNumber,
|
||||
requestedMarkerHp
|
||||
}) => {
|
||||
const scene =
|
||||
window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
const unit = scene?.debugUnitById?.('liu-bei');
|
||||
if (
|
||||
!scene ||
|
||||
!unit ||
|
||||
typeof scene.persistBattleAutosave !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'The idle pagehide autosave probe is unavailable.'
|
||||
);
|
||||
}
|
||||
scene.turnNumber = requestedTurnNumber;
|
||||
scene.activeFaction = 'ally';
|
||||
scene.phase = 'idle';
|
||||
scene.selectedUnit = undefined;
|
||||
scene.pendingMove = undefined;
|
||||
scene.targetingAction = undefined;
|
||||
scene.selectedUsable = undefined;
|
||||
unit.hp = requestedMarkerHp;
|
||||
window.dispatchEvent(new Event('pagehide'));
|
||||
},
|
||||
{
|
||||
requestedTurnNumber: idleTurnNumber,
|
||||
requestedMarkerHp: idleMarkerHp
|
||||
}
|
||||
);
|
||||
|
||||
await page.waitForFunction(
|
||||
({
|
||||
requestedBattleSlotKey,
|
||||
previousSavedAt,
|
||||
previousWriteCount,
|
||||
expectedTurnNumber,
|
||||
expectedMarkerHp
|
||||
}) => {
|
||||
const persisted = JSON.parse(
|
||||
window.localStorage.getItem(requestedBattleSlotKey) ??
|
||||
'null'
|
||||
);
|
||||
const autosave =
|
||||
window.__HEROS_DEBUG__?.battle?.()?.autosave;
|
||||
return (
|
||||
persisted?.savedAt &&
|
||||
persisted.savedAt !== previousSavedAt &&
|
||||
persisted.turnNumber === expectedTurnNumber &&
|
||||
persisted.units?.find(
|
||||
(unit) => unit.id === 'liu-bei'
|
||||
)?.hp === expectedMarkerHp &&
|
||||
autosave?.writeCount === previousWriteCount + 1 &&
|
||||
autosave?.lastResult === 'saved' &&
|
||||
autosave?.lastReason === 'pagehide'
|
||||
);
|
||||
},
|
||||
{
|
||||
requestedBattleSlotKey: battleSlotKey,
|
||||
previousSavedAt: idleBaseline.state.savedAt,
|
||||
previousWriteCount:
|
||||
idleBaseline.autosave.writeCount,
|
||||
expectedTurnNumber: idleTurnNumber,
|
||||
expectedMarkerHp: idleMarkerHp
|
||||
}
|
||||
);
|
||||
|
||||
const idleAutosave = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
assertAutosaveWrite(
|
||||
idleBaseline,
|
||||
idleAutosave,
|
||||
{
|
||||
reason: 'pagehide',
|
||||
expectedTurnNumber: idleTurnNumber,
|
||||
expectedActedUnitId: 'liu-bei',
|
||||
expectedMarkerHp: idleMarkerHp
|
||||
},
|
||||
`${renderer}: ally idle pagehide autosave`
|
||||
);
|
||||
assert.equal(
|
||||
idleAutosave.autosave.skipCount,
|
||||
idleBaseline.autosave.skipCount,
|
||||
`${renderer}: a safe idle pagehide was counted as skipped.`
|
||||
);
|
||||
|
||||
const rollbackBaseline = await readAutosaveCheckpoint(
|
||||
page,
|
||||
battleSlotKey
|
||||
);
|
||||
const rollbackBaseRaw = await page.evaluate(
|
||||
(requestedBattleBaseKey) =>
|
||||
window.localStorage.getItem(requestedBattleBaseKey),
|
||||
battleBaseKey
|
||||
);
|
||||
const rollbackProbe = await page.evaluate(
|
||||
({
|
||||
requestedBattleSlotKey,
|
||||
requestedBattleBaseKey,
|
||||
requestedOtherBattleSlotKey,
|
||||
requestedOtherBattleBaseKey,
|
||||
requestedOtherBattleCheckpoint
|
||||
}) => {
|
||||
const scene =
|
||||
window.__HEROS_DEBUG__?.scene('BattleScene');
|
||||
if (
|
||||
!scene ||
|
||||
typeof scene.persistBattleAutosave !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'The autosave rollback probe is unavailable.'
|
||||
);
|
||||
}
|
||||
scene.battleAutosaveReady = true;
|
||||
scene.activeFaction = 'ally';
|
||||
scene.phase = 'idle';
|
||||
scene.selectedUnit = undefined;
|
||||
scene.pendingMove = undefined;
|
||||
scene.targetingAction = undefined;
|
||||
scene.selectedUsable = undefined;
|
||||
scene.turnNumber = 44;
|
||||
window.localStorage.setItem(
|
||||
requestedOtherBattleSlotKey,
|
||||
requestedOtherBattleCheckpoint
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
requestedOtherBattleBaseKey,
|
||||
requestedOtherBattleCheckpoint
|
||||
);
|
||||
|
||||
const storagePrototype =
|
||||
Object.getPrototypeOf(window.localStorage);
|
||||
const originalSetItem = storagePrototype.setItem;
|
||||
let injected = false;
|
||||
storagePrototype.setItem = function setItem(
|
||||
key,
|
||||
value
|
||||
) {
|
||||
if (
|
||||
this === window.localStorage &&
|
||||
String(key) === requestedBattleSlotKey &&
|
||||
!injected
|
||||
) {
|
||||
injected = true;
|
||||
throw new DOMException(
|
||||
'Injected autosave write refusal',
|
||||
'QuotaExceededError'
|
||||
);
|
||||
}
|
||||
return originalSetItem.call(this, key, value);
|
||||
};
|
||||
|
||||
let saved;
|
||||
try {
|
||||
saved = scene.persistBattleAutosave('debug');
|
||||
} finally {
|
||||
storagePrototype.setItem = originalSetItem;
|
||||
}
|
||||
return {
|
||||
saved,
|
||||
injected,
|
||||
slotRaw: window.localStorage.getItem(
|
||||
requestedBattleSlotKey
|
||||
),
|
||||
baseRaw: window.localStorage.getItem(
|
||||
requestedBattleBaseKey
|
||||
),
|
||||
otherBattleSlotRaw: window.localStorage.getItem(
|
||||
requestedOtherBattleSlotKey
|
||||
),
|
||||
otherBattleBaseRaw: window.localStorage.getItem(
|
||||
requestedOtherBattleBaseKey
|
||||
),
|
||||
autosave:
|
||||
window.__HEROS_DEBUG__?.battle?.()?.autosave ??
|
||||
null
|
||||
};
|
||||
},
|
||||
{
|
||||
requestedBattleSlotKey: battleSlotKey,
|
||||
requestedBattleBaseKey: battleBaseKey,
|
||||
requestedOtherBattleSlotKey: otherBattleSlotKey,
|
||||
requestedOtherBattleBaseKey: otherBattleBaseKey,
|
||||
requestedOtherBattleCheckpoint:
|
||||
otherBattleCheckpoint
|
||||
}
|
||||
);
|
||||
assert.equal(rollbackProbe.injected, true);
|
||||
assert.equal(rollbackProbe.saved, false);
|
||||
assert.equal(
|
||||
rollbackProbe.slotRaw,
|
||||
rollbackBaseline.raw,
|
||||
`${renderer}: a failed autosave did not restore the canonical slot checkpoint.`
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.baseRaw,
|
||||
rollbackBaseRaw,
|
||||
`${renderer}: a failed autosave did not restore the slot-one compatibility checkpoint.`
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.otherBattleSlotRaw,
|
||||
otherBattleCheckpoint,
|
||||
`${renderer}: a failed autosave discarded another battle's canonical checkpoint in the same campaign slot.`
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.otherBattleBaseRaw,
|
||||
otherBattleCheckpoint,
|
||||
`${renderer}: a failed autosave discarded another battle's compatibility checkpoint in the same campaign slot.`
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.autosave.writeCount,
|
||||
rollbackBaseline.autosave.writeCount
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.autosave.skipCount,
|
||||
rollbackBaseline.autosave.skipCount
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.autosave.failureCount,
|
||||
rollbackBaseline.autosave.failureCount + 1
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.autosave.lastResult,
|
||||
'failed'
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.autosave.lastReason,
|
||||
'debug'
|
||||
);
|
||||
assert.equal(
|
||||
rollbackProbe.autosave.lastSavedAt,
|
||||
rollbackBaseline.state.savedAt
|
||||
);
|
||||
assert.equal(pageErrors.length, 0, pageErrors.join('\n'));
|
||||
assert.equal(consoleErrors.length, 0, consoleErrors.join('\n'));
|
||||
|
||||
@@ -513,7 +1121,56 @@ try {
|
||||
campaignGeneration,
|
||||
saveGeneration: beforeControl.acceptedGeneration,
|
||||
restoredTurnNumber: restored.turnNumber,
|
||||
restoredMarkerHp: restored.markerHp
|
||||
restoredMarkerHp: restored.markerHp,
|
||||
pendingEventBeforeTutorial:
|
||||
tutorialPendingEventProbe
|
||||
},
|
||||
autosave: {
|
||||
allyAction: {
|
||||
phaseAfterAction:
|
||||
actionTrigger.phase,
|
||||
turnNumber:
|
||||
actionAutosave.state.turnNumber,
|
||||
actedUnitIds:
|
||||
actionAutosave.state.actedUnitIds,
|
||||
savedAt:
|
||||
actionAutosave.state.savedAt,
|
||||
debug:
|
||||
actionAutosave.autosave
|
||||
},
|
||||
unsafePagehide:
|
||||
unsafeAutosaves,
|
||||
initializationGuard: {
|
||||
turnNumber: readinessTurnNumber,
|
||||
debug: readinessAfter.autosave
|
||||
},
|
||||
idlePagehide: {
|
||||
turnNumber:
|
||||
idleAutosave.state.turnNumber,
|
||||
markerHp:
|
||||
idleAutosave.state.units.find(
|
||||
(unit) => unit.id === 'liu-bei'
|
||||
)?.hp ?? null,
|
||||
savedAt:
|
||||
idleAutosave.state.savedAt,
|
||||
debug:
|
||||
idleAutosave.autosave
|
||||
},
|
||||
failedWriteRollback: {
|
||||
slotPreserved:
|
||||
rollbackProbe.slotRaw ===
|
||||
rollbackBaseline.raw,
|
||||
basePreserved:
|
||||
rollbackProbe.baseRaw ===
|
||||
rollbackBaseRaw,
|
||||
otherBattleSlotPreserved:
|
||||
rollbackProbe.otherBattleSlotRaw ===
|
||||
otherBattleCheckpoint,
|
||||
otherBattleBasePreserved:
|
||||
rollbackProbe.otherBattleBaseRaw ===
|
||||
otherBattleCheckpoint,
|
||||
debug: rollbackProbe.autosave
|
||||
}
|
||||
},
|
||||
pageErrors: pageErrors.length,
|
||||
consoleErrors: consoleErrors.length
|
||||
@@ -576,6 +1233,159 @@ async function waitForBattleReady(page) {
|
||||
);
|
||||
}
|
||||
|
||||
async function readAutosaveCheckpoint(
|
||||
page,
|
||||
requestedBattleSlotKey
|
||||
) {
|
||||
const checkpoint = await page.evaluate(
|
||||
(storageKey) => {
|
||||
const raw = window.localStorage.getItem(storageKey);
|
||||
const battle =
|
||||
window.__HEROS_DEBUG__?.battle?.();
|
||||
return {
|
||||
raw,
|
||||
state: JSON.parse(raw ?? 'null'),
|
||||
autosave: battle?.autosave ?? null
|
||||
};
|
||||
},
|
||||
requestedBattleSlotKey
|
||||
);
|
||||
assert(
|
||||
checkpoint.raw,
|
||||
`Missing battle autosave checkpoint at ${requestedBattleSlotKey}.`
|
||||
);
|
||||
assert(
|
||||
checkpoint.state,
|
||||
`Invalid battle autosave checkpoint at ${requestedBattleSlotKey}.`
|
||||
);
|
||||
assertAutosaveDebugState(
|
||||
checkpoint.autosave,
|
||||
`Autosave debug state for ${requestedBattleSlotKey}`
|
||||
);
|
||||
return checkpoint;
|
||||
}
|
||||
|
||||
function assertAutosaveDebugState(
|
||||
autosave,
|
||||
label
|
||||
) {
|
||||
assert(
|
||||
autosave && typeof autosave === 'object',
|
||||
`${label} is unavailable.`
|
||||
);
|
||||
for (const field of [
|
||||
'writeCount',
|
||||
'skipCount',
|
||||
'failureCount'
|
||||
]) {
|
||||
assert(
|
||||
Number.isInteger(autosave[field]) &&
|
||||
autosave[field] >= 0,
|
||||
`${label}.${field} must be a non-negative integer.`
|
||||
);
|
||||
}
|
||||
assert(
|
||||
[
|
||||
'saved',
|
||||
'skipped',
|
||||
'failed',
|
||||
null
|
||||
].includes(autosave.lastResult),
|
||||
`${label}.lastResult is invalid.`
|
||||
);
|
||||
assert(
|
||||
autosave.lastReason === null ||
|
||||
typeof autosave.lastReason === 'string',
|
||||
`${label}.lastReason is invalid.`
|
||||
);
|
||||
assert(
|
||||
autosave.lastSlot === null ||
|
||||
autosave.lastSlot === 1,
|
||||
`${label}.lastSlot is invalid.`
|
||||
);
|
||||
assert(
|
||||
autosave.lastSavedAt === null ||
|
||||
(
|
||||
typeof autosave.lastSavedAt === 'string' &&
|
||||
Number.isFinite(
|
||||
Date.parse(autosave.lastSavedAt)
|
||||
)
|
||||
),
|
||||
`${label}.lastSavedAt is invalid.`
|
||||
);
|
||||
}
|
||||
|
||||
function assertAutosaveWrite(
|
||||
before,
|
||||
after,
|
||||
{
|
||||
reason,
|
||||
expectedTurnNumber,
|
||||
expectedActedUnitId,
|
||||
expectedMarkerHp
|
||||
},
|
||||
label
|
||||
) {
|
||||
assert.notEqual(
|
||||
after.raw,
|
||||
before.raw,
|
||||
`${label} did not update the active battle slot.`
|
||||
);
|
||||
assert.notEqual(
|
||||
after.state.savedAt,
|
||||
before.state.savedAt,
|
||||
`${label} did not refresh savedAt.`
|
||||
);
|
||||
assert.equal(
|
||||
after.state.turnNumber,
|
||||
expectedTurnNumber,
|
||||
`${label} saved the wrong turn.`
|
||||
);
|
||||
assert(
|
||||
after.state.actedUnitIds.includes(
|
||||
expectedActedUnitId
|
||||
),
|
||||
`${label} omitted the completed ally action.`
|
||||
);
|
||||
if (expectedMarkerHp !== undefined) {
|
||||
assert.equal(
|
||||
after.state.units.find(
|
||||
(unit) => unit.id === 'liu-bei'
|
||||
)?.hp,
|
||||
expectedMarkerHp,
|
||||
`${label} did not persist the live ally state.`
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
after.autosave.writeCount,
|
||||
before.autosave.writeCount + 1,
|
||||
`${label} did not record exactly one write.`
|
||||
);
|
||||
assert.equal(
|
||||
after.autosave.skipCount,
|
||||
before.autosave.skipCount,
|
||||
`${label} unexpectedly recorded a skip.`
|
||||
);
|
||||
assert.equal(
|
||||
after.autosave.failureCount,
|
||||
before.autosave.failureCount,
|
||||
`${label} unexpectedly recorded a failure.`
|
||||
);
|
||||
assert.equal(
|
||||
after.autosave.lastResult,
|
||||
'saved'
|
||||
);
|
||||
assert.equal(
|
||||
after.autosave.lastReason,
|
||||
reason
|
||||
);
|
||||
assert.equal(after.autosave.lastSlot, 1);
|
||||
assert.equal(
|
||||
after.autosave.lastSavedAt,
|
||||
after.state.savedAt
|
||||
);
|
||||
}
|
||||
|
||||
async function assertDesktopRuntime(page, expectedRenderer) {
|
||||
const runtime = await page.evaluate(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
|
||||
Reference in New Issue
Block a user