feat: make sortie feedback actionable
This commit is contained in:
@@ -5361,15 +5361,110 @@ try {
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-formation-evaluation-updated.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'mid camp formation evaluation update');
|
||||
|
||||
const closeMidBattleEvaluationPoint = await readBattleResultEvaluationControlPoint(page, 'close');
|
||||
await page.mouse.click(closeMidBattleEvaluationPoint.x, closeMidBattleEvaluationPoint.y);
|
||||
const midResultImprovementAction = updatedMobileResultState?.sortieEvaluation?.improvementAction;
|
||||
const midImprovementTargetBattleId = midResultImprovementAction?.targetBattleId;
|
||||
assert(
|
||||
midResultImprovementAction?.available === true &&
|
||||
midResultImprovementAction.label === '다음 출전 보완' &&
|
||||
midResultImprovementAction.sourceBattleId === midCampNextBattleId &&
|
||||
typeof midImprovementTargetBattleId === 'string' &&
|
||||
midImprovementTargetBattleId !== midCampNextBattleId &&
|
||||
midResultImprovementAction.planId === midRecommendationLaunchFixture.selection.planId &&
|
||||
isFiniteBounds(midResultImprovementAction.buttonBounds) &&
|
||||
boundsInside(midResultImprovementAction.buttonBounds, fhdViewportBounds),
|
||||
`Expected the victory evaluation to expose an in-FHD next-sortie improvement action: ${JSON.stringify(midResultImprovementAction)}`
|
||||
);
|
||||
const midImprovementPoint = await readBattleResultEvaluationControlPoint(page, 'improvement');
|
||||
await page.mouse.click(midImprovementPoint.x, midImprovementPoint.y);
|
||||
await waitForSortiePrep(page);
|
||||
|
||||
const guidedPreviewState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const guidedPreviewSave = await readCampaignSave(page);
|
||||
const guidedPreview = guidedPreviewState?.sortieGuidedImprovement;
|
||||
assert(
|
||||
guidedPreviewState?.sortiePrepStep === 'formation' &&
|
||||
guidedPreviewState.nextSortieBattleId === midImprovementTargetBattleId &&
|
||||
guidedPreview?.stage === 'preview' &&
|
||||
guidedPreview.sourceBattleId === midCampNextBattleId &&
|
||||
guidedPreview.targetBattleId === midImprovementTargetBattleId &&
|
||||
guidedPreview.proposal?.planId === midResultImprovementAction.planId &&
|
||||
guidedImprovementHasOneAction(guidedPreview.proposal) &&
|
||||
guidedPreviewState.sortieComparisonPreview?.source === 'guided-improvement' &&
|
||||
guidedPreviewState.sortieComparisonAction?.kind === 'confirm-improvement' &&
|
||||
isFiniteBounds(guidedPreviewState.sortieComparisonAction?.buttonBounds) &&
|
||||
boundsInside(guidedPreviewState.sortieComparisonAction.buttonBounds, fhdViewportBounds) &&
|
||||
sameJsonValue(
|
||||
campaignSaveWithoutCampRecruitmentEffects(guidedPreviewSave),
|
||||
campaignSaveWithoutCampRecruitmentEffects(updatedMobileResultSave)
|
||||
),
|
||||
`Expected a non-destructive one-officer improvement preview for the next battle: ${JSON.stringify({
|
||||
prepStep: guidedPreviewState?.sortiePrepStep,
|
||||
nextBattleId: guidedPreviewState?.nextSortieBattleId,
|
||||
guidedImprovement: guidedPreview,
|
||||
comparisonPreviewSource: guidedPreviewState?.sortieComparisonPreview?.source,
|
||||
comparisonAction: guidedPreviewState?.sortieComparisonAction,
|
||||
stableSaveUnchanged: sameJsonValue(
|
||||
campaignSaveWithoutCampRecruitmentEffects(guidedPreviewSave),
|
||||
campaignSaveWithoutCampRecruitmentEffects(updatedMobileResultSave)
|
||||
)
|
||||
})}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-sortie-guided-improvement-preview.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'mid sortie guided improvement preview');
|
||||
|
||||
const guidedApplyPoint = await readSortieComparisonActionPoint(page);
|
||||
await page.mouse.click(guidedApplyPoint.x, guidedApplyPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const camp = window.__HEROS_DEBUG__?.camp?.();
|
||||
return camp?.sortieGuidedImprovement?.stage === 'applied' &&
|
||||
camp?.sortieComparisonAction?.kind === 'undo-improvement';
|
||||
}, undefined, { timeout: 30000 });
|
||||
const guidedAppliedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const guidedAppliedSave = await readCampaignSave(page);
|
||||
const guidedProposal = guidedAppliedState?.sortieGuidedImprovement?.proposal;
|
||||
assert(
|
||||
sameJsonValue(guidedAppliedState?.selectedSortieUnitIds, guidedProposal?.projectedSelectedUnitIds) &&
|
||||
selectedAssignmentsMatch(guidedAppliedState?.sortieFormationAssignments, guidedProposal?.projectedFormationAssignments, guidedProposal?.projectedSelectedUnitIds) &&
|
||||
sameJsonValue(guidedAppliedSave.current?.selectedSortieUnitIds, guidedProposal?.projectedSelectedUnitIds) &&
|
||||
selectedAssignmentsMatch(guidedAppliedSave.current?.sortieFormationAssignments, guidedProposal?.projectedFormationAssignments, guidedProposal?.projectedSelectedUnitIds) &&
|
||||
guidedAppliedSave.current?.sortieRecommendationSelection?.battleId === midImprovementTargetBattleId &&
|
||||
guidedAppliedSave.current?.sortieRecommendationSelection?.planId === midResultImprovementAction.planId &&
|
||||
guidedAppliedState?.sortieGuidedImprovementUndo?.available === true,
|
||||
`Expected confirmation to persist exactly the projected roster/role change and revised recommendation provenance: ${JSON.stringify({
|
||||
state: guidedAppliedState,
|
||||
save: guidedAppliedSave
|
||||
})}`
|
||||
);
|
||||
|
||||
const guidedUndoPoint = await readSortieComparisonActionPoint(page);
|
||||
await page.mouse.click(guidedUndoPoint.x, guidedUndoPoint.y);
|
||||
await page.waitForFunction((baselineIds) => {
|
||||
const camp = window.__HEROS_DEBUG__?.camp?.();
|
||||
return camp?.sortieGuidedImprovement === null &&
|
||||
camp?.sortieGuidedImprovementUndo === null &&
|
||||
JSON.stringify(camp?.selectedSortieUnitIds) === JSON.stringify(baselineIds);
|
||||
}, guidedProposal.baselineSelectedUnitIds, { timeout: 30000 });
|
||||
const guidedUndoState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const guidedUndoSave = await readCampaignSave(page);
|
||||
assert(
|
||||
sameJsonValue(guidedUndoState?.selectedSortieUnitIds, guidedProposal.baselineSelectedUnitIds) &&
|
||||
selectedAssignmentsMatch(guidedUndoState?.sortieFormationAssignments, guidedProposal.baselineFormationAssignments, guidedProposal.baselineSelectedUnitIds) &&
|
||||
sameJsonValue(guidedUndoSave.current?.selectedSortieUnitIds, guidedProposal.baselineSelectedUnitIds) &&
|
||||
selectedAssignmentsMatch(guidedUndoSave.current?.sortieFormationAssignments, guidedProposal.baselineFormationAssignments, guidedProposal.baselineSelectedUnitIds) &&
|
||||
guidedUndoSave.current?.sortieRecommendationSelection === undefined,
|
||||
`Expected one-click undo to restore the exact pre-improvement formation: ${JSON.stringify({
|
||||
state: guidedUndoState,
|
||||
save: guidedUndoSave
|
||||
})}`
|
||||
);
|
||||
|
||||
const returnFromGuidedImprovementPoint = await readSortieTextControlPoint(page, '군영으로');
|
||||
await page.mouse.click(returnFromGuidedImprovementPoint.x, returnFromGuidedImprovementPoint.y);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle()?.sortieEvaluation?.open === false,
|
||||
() => window.__HEROS_DEBUG__?.camp?.()?.sortieVisible === false,
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await clickLegacyUi(page, 738, 642);
|
||||
await waitForCampAfterBattleResult(page);
|
||||
|
||||
const midCampReviewState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const midCampReviewSaveBefore = await readCampaignSave(page);
|
||||
@@ -5387,6 +5482,11 @@ try {
|
||||
recommendationFeedbackSemantic(midCampFormationEvaluation.recommendationFeedback),
|
||||
recommendationFeedbackSemantic(updatedMobileResultState?.sortieEvaluation?.recommendationFeedback)
|
||||
) &&
|
||||
midCampFormationEvaluation.improvementAction?.available === true &&
|
||||
midCampFormationEvaluation.improvementAction.label === '다음 출전 보완' &&
|
||||
midCampFormationEvaluation.improvementAction.sourceBattleId === midCampNextBattleId &&
|
||||
midCampFormationEvaluation.improvementAction.targetBattleId === midImprovementTargetBattleId &&
|
||||
midCampFormationEvaluation.improvementAction.planId === midResultImprovementAction.planId &&
|
||||
sameJsonValue(midCampFormationEvaluation.sourcePresetIds, midResultSortieReviewBeforePresetUpdate.sourcePresetIds) &&
|
||||
midCampMobileCard?.saved === true &&
|
||||
midCampMobileCard.matching === true &&
|
||||
@@ -5425,6 +5525,9 @@ try {
|
||||
openedMidCampReviewProbe.state?.reportFormationEvaluation?.open === true &&
|
||||
openedMidCampReviewProbe.texts.includes('최근 전투 편성 평가') &&
|
||||
openedMidCampReviewProbe.texts.some((text) => text.includes('추천안 검증') && text.includes(midRecommendationLaunchFixture.selection.label)) &&
|
||||
openedMidCampReviewProbe.texts.includes('다음 출전 보완') &&
|
||||
isFiniteBounds(openedMidCampReviewProbe.state?.reportFormationEvaluation?.improvementAction?.buttonBounds) &&
|
||||
boundsInside(openedMidCampReviewProbe.state.reportFormationEvaluation.improvementAction.buttonBounds, fhdViewportBounds) &&
|
||||
isFiniteBounds(openedCampRecommendationFeedback?.panelBounds) &&
|
||||
boundsInside(openedCampRecommendationFeedback.panelBounds, fhdViewportBounds) &&
|
||||
sameJsonValue(openedCampRecommendationFeedback.displayedUnitIds, midRecommendationLaunchFixture.selection.selectedUnitIds) &&
|
||||
@@ -8122,7 +8225,9 @@ async function readBattleResultEvaluationControlPoint(page, control, presetId) {
|
||||
? evaluation?.toggleButtonBounds
|
||||
: control === 'close'
|
||||
? evaluation?.closeButtonBounds
|
||||
: presetCard?.actionButtonBounds;
|
||||
: control === 'improvement'
|
||||
? evaluation?.improvementAction?.buttonBounds
|
||||
: presetCard?.actionButtonBounds;
|
||||
if (!scene || !canvas || !bounds) {
|
||||
return {
|
||||
ready: false,
|
||||
@@ -8190,7 +8295,9 @@ async function readCampReportFormationEvaluationControlPoint(page, control, pres
|
||||
? evaluation?.toggleButtonBounds
|
||||
: control === 'close'
|
||||
? evaluation?.closeButtonBounds
|
||||
: presetCard?.actionButtonBounds;
|
||||
: control === 'improvement'
|
||||
? evaluation?.improvementAction?.buttonBounds
|
||||
: presetCard?.actionButtonBounds;
|
||||
if (!scene || !canvas || !bounds) {
|
||||
return {
|
||||
ready: false,
|
||||
@@ -8962,10 +9069,86 @@ function assertSameMembers(values, expected, message) {
|
||||
assert(expected.every((value) => valueSet.has(value)), message);
|
||||
}
|
||||
|
||||
function selectedAssignmentsMatch(actual, expected, selectedUnitIds) {
|
||||
return Boolean(
|
||||
actual && expected && Array.isArray(selectedUnitIds) &&
|
||||
selectedUnitIds.every((unitId) => actual[unitId] === expected[unitId])
|
||||
);
|
||||
}
|
||||
|
||||
function guidedImprovementHasOneAction(proposal) {
|
||||
if (!proposal) {
|
||||
return false;
|
||||
}
|
||||
const baselineIds = proposal.baselineSelectedUnitIds ?? [];
|
||||
const projectedIds = proposal.projectedSelectedUnitIds ?? [];
|
||||
const idSet = new Set([...baselineIds, ...projectedIds]);
|
||||
const rosterDelta = [...idSet].filter(
|
||||
(unitId) => baselineIds.includes(unitId) !== projectedIds.includes(unitId)
|
||||
);
|
||||
const roleDelta = [...idSet].filter(
|
||||
(unitId) => proposal.baselineFormationAssignments?.[unitId] !== proposal.projectedFormationAssignments?.[unitId]
|
||||
);
|
||||
if (proposal.kind === 'swap') {
|
||||
return baselineIds.length === projectedIds.length &&
|
||||
rosterDelta.length === 2 &&
|
||||
!projectedIds.includes(proposal.outgoingUnitId) &&
|
||||
projectedIds.includes(proposal.incomingUnitId);
|
||||
}
|
||||
if (proposal.kind === 'add') {
|
||||
return projectedIds.length === baselineIds.length + 1 &&
|
||||
rosterDelta.length === 1 &&
|
||||
rosterDelta[0] === proposal.incomingUnitId &&
|
||||
projectedIds.includes(proposal.incomingUnitId);
|
||||
}
|
||||
if (proposal.kind === 'role') {
|
||||
return sameJsonValue(baselineIds, projectedIds) &&
|
||||
roleDelta.length === 1 &&
|
||||
roleDelta[0] === proposal.unitId;
|
||||
}
|
||||
return proposal.kind === 'tactic' &&
|
||||
sameJsonValue(baselineIds, projectedIds) &&
|
||||
roleDelta.length === 0 &&
|
||||
typeof proposal.instruction === 'string' &&
|
||||
proposal.instruction.length > 0;
|
||||
}
|
||||
|
||||
function sameJsonValue(value, expected) {
|
||||
return stableJson(value) === stableJson(expected);
|
||||
}
|
||||
|
||||
function campaignSaveWithoutCampRecruitmentEffects(save) {
|
||||
const stableState = (state) => {
|
||||
if (!state) {
|
||||
return state;
|
||||
}
|
||||
const {
|
||||
updatedAt: _updatedAt,
|
||||
roster: _roster,
|
||||
bonds: _bonds,
|
||||
firstBattleReport,
|
||||
...stableFields
|
||||
} = state;
|
||||
if (!firstBattleReport) {
|
||||
return stableFields;
|
||||
}
|
||||
const {
|
||||
units: _reportUnits,
|
||||
bonds: _reportBonds,
|
||||
...stableReportFields
|
||||
} = firstBattleReport;
|
||||
return {
|
||||
...stableFields,
|
||||
firstBattleReport: stableReportFields
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
current: stableState(save?.current),
|
||||
slot1: stableState(save?.slot1)
|
||||
};
|
||||
}
|
||||
|
||||
function recommendationFeedbackSemantic(feedback) {
|
||||
if (!feedback) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user