fix: harden campaign resume UX and flow verification

This commit is contained in:
2026-07-21 09:59:48 +09:00
parent be5fe3e880
commit c810f95d00
11 changed files with 1815 additions and 125 deletions

View File

@@ -6389,7 +6389,8 @@ try {
gold: 9800,
turnNumber: 18,
defeatedEnemies: 20,
selectedSortieUnitIds: ['zhuge-liang', 'jiang-wei', 'wang-ping', 'ma-dai', 'huang-quan', 'li-yan', 'wei-yan']
selectedSortieUnitIds: ['zhuge-liang', 'jiang-wei', 'wang-ping', 'ma-dai', 'huang-quan', 'li-yan', 'wei-yan'],
pendingVictoryRewardNotice: true
});
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
@@ -6397,6 +6398,11 @@ try {
await waitForCamp(page);
await waitForCampSkinTransition(page, 'northern');
await waitForCampSoundscapeTransition(page, 'camp-frontier', 'mountain-wind-ambience');
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.victoryRewardAcknowledgement?.visible === true,
undefined,
{ timeout: 30000 }
);
const finalCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(finalCampState?.campaign?.step === 'sixty-sixth-camp', `Expected final camp save to continue: ${JSON.stringify(finalCampState)}`);
assertCampSkinState(finalCampState, 'northern', 'final camp');
@@ -6420,29 +6426,61 @@ try {
Array.isArray(finalCampState.sortieDeploymentPreview) && finalCampState.sortieDeploymentPreview.length === 0,
`Expected final camp deployment preview to stay empty: ${JSON.stringify(finalCampState?.sortieDeploymentPreview)}`
);
if (finalCampState.victoryRewardAcknowledgement?.visible === true) {
const finalCampRewardSaveBeforeClose = await readCampaignSave(page);
const finalCampRewardClosePoint = await readCampVictoryRewardControlPoint(page, 'close');
await page.mouse.click(finalCampRewardClosePoint.x, finalCampRewardClosePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.victoryRewardAcknowledgement?.visible === false,
undefined,
{ timeout: 30000 }
);
const finalCampAfterRewardClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const finalCampRewardSaveAfterClose = await readCampaignSave(page);
assert(
sameJsonValue(finalCampRewardSaveAfterClose, finalCampRewardSaveBeforeClose) &&
finalCampAfterRewardClose?.campTabs?.find((tab) => tab.id === 'equipment')?.newBadgeVisible === true &&
finalCampAfterRewardClose?.campTabs?.find((tab) => tab.id === 'supplies')?.newBadgeVisible === true,
`Expected closing the final-camp reward notice to preserve its destination-specific NEW states: ${JSON.stringify({
saveUnchanged: sameJsonValue(finalCampRewardSaveAfterClose, finalCampRewardSaveBeforeClose),
tabs: finalCampAfterRewardClose?.campTabs
?.filter((tab) => ['equipment', 'supplies'].includes(tab.id))
.map((tab) => ({ id: tab.id, new: tab.newBadgeVisible }))
})}`
);
}
assert(
finalCampState.victoryRewardAcknowledgement?.visible === true &&
finalCampState.victoryRewardAcknowledgement?.battleId === 'sixty-sixth-battle-wuzhang-final',
`Expected the seeded final victory to expose its undismissed reward notice: ${JSON.stringify(finalCampState.victoryRewardAcknowledgement)}`
);
const finalCampRewardSaveBeforeClose = await readCampaignSave(page);
const finalCampRewardClosePoint = await readCampVictoryRewardControlPoint(page, 'close');
await page.mouse.click(finalCampRewardClosePoint.x, finalCampRewardClosePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.victoryRewardAcknowledgement?.visible === false,
undefined,
{ timeout: 30000 }
);
const finalCampAfterRewardClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const finalCampRewardSaveAfterClose = await readCampaignSave(page);
const finalBattleId = 'sixty-sixth-battle-wuzhang-final';
assert(
sameJsonValue(
campaignSaveWithoutVictoryRewardNoticeDismissals(finalCampRewardSaveAfterClose),
campaignSaveWithoutVictoryRewardNoticeDismissals(finalCampRewardSaveBeforeClose)
) &&
sameJsonValue(
campaignVictoryAcknowledgementSnapshot(finalCampRewardSaveAfterClose, finalBattleId),
campaignVictoryAcknowledgementSnapshot(finalCampRewardSaveBeforeClose, finalBattleId)
) &&
campaignVictoryNoticeDismissalSnapshot(finalCampRewardSaveBeforeClose, finalBattleId).current === false &&
campaignVictoryNoticeDismissalSnapshot(finalCampRewardSaveBeforeClose, finalBattleId).slot1 === false &&
campaignVictoryNoticeDismissalSnapshot(finalCampRewardSaveAfterClose, finalBattleId).current === true &&
campaignVictoryNoticeDismissalSnapshot(finalCampRewardSaveAfterClose, finalBattleId).slot1 === true &&
finalCampAfterRewardClose?.campTabs?.find((tab) => tab.id === 'equipment')?.newBadgeVisible === true &&
finalCampAfterRewardClose?.campTabs?.find((tab) => tab.id === 'supplies')?.newBadgeVisible === true,
`Expected closing the final-camp reward notice to persist only its dismissal while preserving destination-specific NEW states: ${JSON.stringify({
beforeDismissal: campaignVictoryNoticeDismissalSnapshot(finalCampRewardSaveBeforeClose, finalBattleId),
afterDismissal: campaignVictoryNoticeDismissalSnapshot(finalCampRewardSaveAfterClose, finalBattleId),
beforeAcknowledgement: campaignVictoryAcknowledgementSnapshot(finalCampRewardSaveBeforeClose, finalBattleId),
afterAcknowledgement: campaignVictoryAcknowledgementSnapshot(finalCampRewardSaveAfterClose, finalBattleId),
tabs: finalCampAfterRewardClose?.campTabs
?.filter((tab) => ['equipment', 'supplies'].includes(tab.id))
.map((tab) => ({ id: tab.id, new: tab.newBadgeVisible }))
})}`
);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await waitForCamp(page);
await waitForCampSkinTransition(page, 'northern');
const resumedFinalCampAfterRewardClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
resumedFinalCampAfterRewardClose?.campaign?.step === 'sixty-sixth-camp' &&
resumedFinalCampAfterRewardClose?.victoryRewardAcknowledgement?.visible === false &&
resumedFinalCampAfterRewardClose?.campTabs?.find((tab) => tab.id === 'equipment')?.newBadgeVisible === true &&
resumedFinalCampAfterRewardClose?.campTabs?.find((tab) => tab.id === 'supplies')?.newBadgeVisible === true,
`Expected title continue to keep the dismissed reward notice closed without consuming NEW states: ${JSON.stringify(resumedFinalCampAfterRewardClose)}`
);
await captureStableCampScreenshot(page, `${screenshotDir}/rc-final-camp.png`, 0, 'final camp');
await assertFinalCampRosterPagination(page);
@@ -9946,6 +9984,19 @@ async function seedCampaignSave(page, options) {
reserveTraining: [],
completedAt: now
};
const pendingVictoryRewardNotice = seed.pendingVictoryRewardNotice === true;
const acknowledgedVictoryRewardCategories = {
...(current.acknowledgedVictoryRewardCategories ?? {})
};
if (pendingVictoryRewardNotice) {
delete acknowledgedVictoryRewardCategories[seed.battleId];
}
const previouslyRecordedVictoryBattleIds = Object.entries(current.battleHistory ?? {})
.filter(([, settlement]) => settlement?.outcome === 'victory')
.map(([battleId]) => battleId);
if (current.firstBattleReport?.outcome === 'victory') {
previouslyRecordedVictoryBattleIds.push(current.firstBattleReport.battleId);
}
const next = {
...current,
updatedAt: now,
@@ -9956,9 +10007,18 @@ async function seedCampaignSave(page, options) {
selectedSortieUnitIds: seed.selectedSortieUnitIds,
latestBattleId: seed.battleId,
firstBattleReport: report,
acknowledgedVictoryRewardBattleIds: [
...new Set([...(current.acknowledgedVictoryRewardBattleIds ?? []), seed.battleId])
],
acknowledgedVictoryRewardBattleIds: pendingVictoryRewardNotice
? (current.acknowledgedVictoryRewardBattleIds ?? []).filter((battleId) => battleId !== seed.battleId)
: [...new Set([...(current.acknowledgedVictoryRewardBattleIds ?? []), seed.battleId])],
acknowledgedVictoryRewardCategories,
dismissedVictoryRewardNoticeBattleIds: pendingVictoryRewardNotice
? [
...new Set([
...(current.dismissedVictoryRewardNoticeBattleIds ?? []),
...previouslyRecordedVictoryBattleIds.filter((battleId) => battleId !== seed.battleId)
])
].filter((battleId) => battleId !== seed.battleId)
: [...(current.dismissedVictoryRewardNoticeBattleIds ?? [])],
battleHistory: {
...(current.battleHistory ?? {}),
[seed.battleId]: settlement
@@ -10259,6 +10319,32 @@ function campaignVictoryAcknowledgementSnapshot(save, battleId) {
};
}
function campaignVictoryNoticeDismissalSnapshot(save, battleId) {
const stateSnapshot = (state) => (state?.dismissedVictoryRewardNoticeBattleIds ?? []).includes(battleId);
return {
current: stateSnapshot(save?.current),
slot1: stateSnapshot(save?.slot1)
};
}
function campaignSaveWithoutVictoryRewardNoticeDismissals(save) {
const stableState = (state) => {
if (!state) {
return state;
}
const {
updatedAt: _updatedAt,
dismissedVictoryRewardNoticeBattleIds: _dismissedVictoryRewardNoticeBattleIds,
...stableFields
} = state;
return stableFields;
};
return {
current: stableState(save?.current),
slot1: stableState(save?.slot1)
};
}
function recommendationFeedbackSemantic(feedback) {
if (!feedback) {
return null;