feat: confirm and undo sortie swaps
This commit is contained in:
@@ -387,6 +387,33 @@ try {
|
||||
);
|
||||
|
||||
await advanceSortiePrepStep(page, 'formation');
|
||||
const midFormationSwapFixture = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
const preservedUnitId = state?.selectedSortieUnitIds?.find((unitId) => unitId !== 'ma-dai');
|
||||
if (!scene || !preservedUnitId || typeof scene.persistSortieSelection !== 'function' || typeof scene.showSortiePrep !== 'function') {
|
||||
return { ready: false, preservedUnitId: preservedUnitId ?? null };
|
||||
}
|
||||
|
||||
scene.sortieFormationAssignments = {
|
||||
...scene.sortieFormationAssignments,
|
||||
'ma-dai': 'flank'
|
||||
};
|
||||
scene.sortieItemAssignments = {
|
||||
...scene.sortieItemAssignments,
|
||||
'ma-dai': { ...(scene.sortieItemAssignments?.['ma-dai'] ?? {}), bean: 1 },
|
||||
[preservedUnitId]: { ...(scene.sortieItemAssignments?.[preservedUnitId] ?? {}), salve: 1 }
|
||||
};
|
||||
scene.persistSortieSelection();
|
||||
scene.showSortiePrep();
|
||||
scene.persistSortieSelection();
|
||||
scene.showSortiePrep();
|
||||
return { ready: true, preservedUnitId };
|
||||
});
|
||||
assert(
|
||||
midFormationSwapFixture.ready === true && midFormationSwapFixture.preservedUnitId,
|
||||
`Expected a saved role and supply fixture for the swap regression: ${JSON.stringify(midFormationSwapFixture)}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-formation.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'mid camp sortie formation');
|
||||
|
||||
@@ -401,6 +428,19 @@ try {
|
||||
|
||||
const midFormationSelectedBefore = [...(midFormationPortraitState?.selectedSortieUnitIds ?? [])];
|
||||
const midFormationCampaignSelectedBefore = [...(midFormationPortraitState?.campaign?.selectedSortieUnitIds ?? [])];
|
||||
const midFormationAssignmentsBefore = { ...(midFormationPortraitState?.sortieFormationAssignments ?? {}) };
|
||||
const midFormationItemAssignmentsBefore = structuredClone(midFormationPortraitState?.sortieItemAssignments ?? {});
|
||||
const midFormationSaveBefore = await readCampaignSave(page);
|
||||
assert(
|
||||
midFormationAssignmentsBefore['ma-dai'] === 'flank' &&
|
||||
midFormationItemAssignmentsBefore['ma-dai']?.bean === 1 &&
|
||||
midFormationItemAssignmentsBefore[midFormationSwapFixture.preservedUnitId]?.salve === 1,
|
||||
`Expected the swap fixture to retain Ma Dai and another officer's assignments: ${JSON.stringify({
|
||||
formation: midFormationAssignmentsBefore,
|
||||
items: midFormationItemAssignmentsBefore,
|
||||
preservedUnitId: midFormationSwapFixture.preservedUnitId
|
||||
})}`
|
||||
);
|
||||
await page.mouse.click(210, 447);
|
||||
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortieFocusedUnitId === 'ma-dai', undefined, { timeout: 30000 });
|
||||
const swapFocusState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
@@ -430,7 +470,17 @@ try {
|
||||
});
|
||||
const midFormationSwapPreviewState = midFormationSwapPreviewProbe.state;
|
||||
const swapPreview = midFormationSwapPreviewState?.sortieComparisonPreview;
|
||||
const expectedProjectedSwapIds = midFormationSelectedBefore.map((unitId) => unitId === 'ma-dai' ? 'guan-yu' : unitId);
|
||||
const projectedSwapIdSet = new Set(
|
||||
midFormationSelectedBefore.map((unitId) => unitId === 'ma-dai' ? 'guan-yu' : unitId)
|
||||
);
|
||||
const expectedProjectedSwapIds = [
|
||||
...(midFormationSwapPreviewState?.sortieRoster ?? [])
|
||||
.filter((unit) => unit.required && projectedSwapIdSet.has(unit.id))
|
||||
.map((unit) => unit.id),
|
||||
...(midFormationSwapPreviewState?.sortieRoster ?? [])
|
||||
.filter((unit) => !unit.required && projectedSwapIdSet.has(unit.id))
|
||||
.map((unit) => unit.id)
|
||||
].slice(0, midFormationSelectedBefore.length);
|
||||
assert(
|
||||
midFormationPortraitState.sortiePortraitRoster.some(
|
||||
(unit) => unit.id === swapPreview?.incomingUnitId && unit.portraitReady && unit.portraitTextureKey?.startsWith('portrait-card-')
|
||||
@@ -504,6 +554,214 @@ try {
|
||||
`Expected pointerout to restore the visible Ma Dai focus preview: ${JSON.stringify(afterSwapPreviewProbe)}`
|
||||
);
|
||||
|
||||
await page.mouse.move(530, 544);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.sortieHoveredUnitId === 'guan-yu' && state?.sortieComparisonPreview?.source === 'hover-swap';
|
||||
}, undefined, { timeout: 30000 });
|
||||
await page.mouse.click(530, 544);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.sortiePinnedSwapCandidateUnitId === 'guan-yu' &&
|
||||
state?.sortieComparisonPreview?.source === 'pinned-swap' &&
|
||||
state?.sortieComparisonAction?.kind === 'confirm-swap';
|
||||
}, undefined, { timeout: 30000 });
|
||||
const pinnedSwapProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
const view = scene?.sortieComparisonPanelView;
|
||||
return {
|
||||
state,
|
||||
action: {
|
||||
buttonVisible: view?.actionButton?.visible ?? false,
|
||||
labelVisible: view?.actionLabel?.visible ?? false,
|
||||
label: view?.actionLabel?.text ?? null
|
||||
}
|
||||
};
|
||||
});
|
||||
const pinnedSwapState = pinnedSwapProbe.state;
|
||||
const pinnedSwapPreview = pinnedSwapState?.sortieComparisonPreview;
|
||||
const pinnedSwapSave = await readCampaignSave(page);
|
||||
const previewIncomingRole = pinnedSwapState?.sortieRoster?.find((unit) => unit.id === 'guan-yu')?.formationRole;
|
||||
assert(
|
||||
pinnedSwapPreview?.source === 'pinned-swap' &&
|
||||
pinnedSwapPreview?.mode === 'swap' &&
|
||||
pinnedSwapPreview?.outgoingUnitId === 'ma-dai' &&
|
||||
pinnedSwapPreview?.incomingUnitId === 'guan-yu' &&
|
||||
JSON.stringify(pinnedSwapPreview?.projectedSelectedUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
|
||||
previewIncomingRole === 'front' &&
|
||||
pinnedSwapState?.sortieComparisonAction?.label === '교체 확정' &&
|
||||
pinnedSwapState?.sortieComparisonAction?.outgoingUnitId === 'ma-dai' &&
|
||||
pinnedSwapState?.sortieComparisonAction?.incomingUnitId === 'guan-yu' &&
|
||||
pinnedSwapProbe.action.buttonVisible === true &&
|
||||
pinnedSwapProbe.action.labelVisible === true &&
|
||||
pinnedSwapProbe.action.label === '교체 확정',
|
||||
`Expected clicking Guan Yu's card body to pin the canonical Ma Dai swap with a visible confirm action: ${JSON.stringify(pinnedSwapProbe)}`
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(pinnedSwapState?.selectedSortieUnitIds) === JSON.stringify(midFormationSelectedBefore) &&
|
||||
JSON.stringify(pinnedSwapState?.campaign?.selectedSortieUnitIds) === JSON.stringify(midFormationCampaignSelectedBefore) &&
|
||||
sameJsonValue(pinnedSwapState?.sortieFormationAssignments, midFormationAssignmentsBefore) &&
|
||||
sameJsonValue(pinnedSwapState?.sortieItemAssignments, midFormationItemAssignmentsBefore) &&
|
||||
JSON.stringify(pinnedSwapSave.current?.selectedSortieUnitIds) === JSON.stringify(midFormationSaveBefore.current?.selectedSortieUnitIds) &&
|
||||
JSON.stringify(pinnedSwapSave.slot1?.selectedSortieUnitIds) === JSON.stringify(midFormationSaveBefore.slot1?.selectedSortieUnitIds) &&
|
||||
sameJsonValue(pinnedSwapSave.current?.sortieFormationAssignments, midFormationSaveBefore.current?.sortieFormationAssignments) &&
|
||||
sameJsonValue(pinnedSwapSave.slot1?.sortieFormationAssignments, midFormationSaveBefore.slot1?.sortieFormationAssignments) &&
|
||||
sameJsonValue(pinnedSwapSave.current?.sortieItemAssignments, midFormationSaveBefore.current?.sortieItemAssignments) &&
|
||||
sameJsonValue(pinnedSwapSave.slot1?.sortieItemAssignments, midFormationSaveBefore.slot1?.sortieItemAssignments),
|
||||
`Expected pinning the swap to preserve runtime, campaign, current-save, and slot-1 assignments: ${JSON.stringify({
|
||||
state: pinnedSwapState,
|
||||
save: pinnedSwapSave
|
||||
})}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-swap-pinned.png`, fullPage: true });
|
||||
|
||||
const confirmSwapActionPoint = await readSortieComparisonActionPoint(page);
|
||||
assert(
|
||||
confirmSwapActionPoint.label === '교체 확정',
|
||||
`Expected the scaled Phaser action button to expose the confirm label: ${JSON.stringify(confirmSwapActionPoint)}`
|
||||
);
|
||||
await page.mouse.click(confirmSwapActionPoint.x, confirmSwapActionPoint.y);
|
||||
await page.waitForFunction((projectedIds) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return JSON.stringify(state?.selectedSortieUnitIds) === JSON.stringify(projectedIds) &&
|
||||
state?.sortieFormationAssignments?.['ma-dai'] === undefined &&
|
||||
state?.sortieFormationAssignments?.['guan-yu'] === 'front' &&
|
||||
state?.sortieItemAssignments?.['ma-dai'] === undefined &&
|
||||
state?.sortieItemAssignments?.['guan-yu'] === undefined &&
|
||||
state?.sortieSwapUndo?.available === true &&
|
||||
state?.sortieComparisonAction?.kind === 'undo-swap';
|
||||
}, expectedProjectedSwapIds, { timeout: 30000 });
|
||||
const confirmedSwapState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const confirmedSwapSave = await readCampaignSave(page);
|
||||
const expectedConfirmedAssignments = { ...midFormationAssignmentsBefore };
|
||||
delete expectedConfirmedAssignments['ma-dai'];
|
||||
expectedConfirmedAssignments['guan-yu'] = previewIncomingRole;
|
||||
const expectedConfirmedItemAssignments = structuredClone(midFormationItemAssignmentsBefore);
|
||||
delete expectedConfirmedItemAssignments['ma-dai'];
|
||||
delete expectedConfirmedItemAssignments['guan-yu'];
|
||||
assert(
|
||||
JSON.stringify(confirmedSwapState?.selectedSortieUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
|
||||
JSON.stringify(confirmedSwapState?.campaign?.selectedSortieUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
|
||||
JSON.stringify(confirmedSwapSave.current?.selectedSortieUnitIds) === JSON.stringify(expectedProjectedSwapIds) &&
|
||||
JSON.stringify(confirmedSwapSave.slot1?.selectedSortieUnitIds) === JSON.stringify(expectedProjectedSwapIds),
|
||||
`Expected swap confirmation to persist the canonical projected roster everywhere: ${JSON.stringify({
|
||||
expectedProjectedSwapIds,
|
||||
runtime: confirmedSwapState?.selectedSortieUnitIds,
|
||||
campaign: confirmedSwapState?.campaign?.selectedSortieUnitIds,
|
||||
current: confirmedSwapSave.current?.selectedSortieUnitIds,
|
||||
slot1: confirmedSwapSave.slot1?.selectedSortieUnitIds
|
||||
})}`
|
||||
);
|
||||
assert(
|
||||
sameJsonValue(confirmedSwapState?.sortieFormationAssignments, expectedConfirmedAssignments) &&
|
||||
sameJsonValue(confirmedSwapState?.campaign?.sortieFormationAssignments, expectedConfirmedAssignments) &&
|
||||
sameJsonValue(confirmedSwapSave.current?.sortieFormationAssignments, expectedConfirmedAssignments) &&
|
||||
sameJsonValue(confirmedSwapSave.slot1?.sortieFormationAssignments, expectedConfirmedAssignments) &&
|
||||
sameJsonValue(confirmedSwapState?.sortieItemAssignments, expectedConfirmedItemAssignments) &&
|
||||
sameJsonValue(confirmedSwapState?.campaign?.sortieItemAssignments, expectedConfirmedItemAssignments) &&
|
||||
sameJsonValue(confirmedSwapSave.current?.sortieItemAssignments, expectedConfirmedItemAssignments) &&
|
||||
sameJsonValue(confirmedSwapSave.slot1?.sortieItemAssignments, expectedConfirmedItemAssignments),
|
||||
`Expected confirmation to remove Ma Dai's role and supply, assign Guan Yu's preview role without transferring supply, and preserve every other assignment: ${JSON.stringify({
|
||||
expectedFormation: expectedConfirmedAssignments,
|
||||
expectedItems: expectedConfirmedItemAssignments,
|
||||
state: confirmedSwapState,
|
||||
save: confirmedSwapSave
|
||||
})}`
|
||||
);
|
||||
assert(
|
||||
confirmedSwapState?.sortieFocusedUnitId === 'guan-yu' &&
|
||||
confirmedSwapState?.sortiePinnedSwapCandidateUnitId === null &&
|
||||
confirmedSwapState?.sortieSwapUndo?.available === true &&
|
||||
confirmedSwapState?.sortieSwapUndo?.outgoingUnitId === 'ma-dai' &&
|
||||
confirmedSwapState?.sortieSwapUndo?.incomingUnitId === 'guan-yu' &&
|
||||
confirmedSwapState?.sortieComparisonAction?.kind === 'undo-swap' &&
|
||||
confirmedSwapState?.sortieComparisonAction?.label === '되돌리기' &&
|
||||
confirmedSwapState?.sortiePlanFeedback?.includes('장비·보급'),
|
||||
`Expected confirmed swap feedback and one-use undo action: ${JSON.stringify(confirmedSwapState)}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-swap-confirmed.png`, fullPage: true });
|
||||
|
||||
const undoSwapActionPoint = await readSortieComparisonActionPoint(page);
|
||||
assert(
|
||||
undoSwapActionPoint.label === '되돌리기',
|
||||
`Expected the scaled Phaser action button to switch to undo: ${JSON.stringify(undoSwapActionPoint)}`
|
||||
);
|
||||
await page.mouse.click(undoSwapActionPoint.x, undoSwapActionPoint.y);
|
||||
await page.waitForFunction((selectedBefore) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return JSON.stringify(state?.selectedSortieUnitIds) === JSON.stringify(selectedBefore) &&
|
||||
state?.sortieFocusedUnitId === 'ma-dai' &&
|
||||
state?.sortieComparisonAction === null &&
|
||||
!state?.sortieSwapUndo?.available;
|
||||
}, midFormationSelectedBefore, { timeout: 30000 });
|
||||
const undoneSwapProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
const view = scene?.sortieComparisonPanelView;
|
||||
return {
|
||||
state,
|
||||
action: {
|
||||
buttonVisible: view?.actionButton?.visible ?? false,
|
||||
labelVisible: view?.actionLabel?.visible ?? false,
|
||||
label: view?.actionLabel?.text ?? null
|
||||
}
|
||||
};
|
||||
});
|
||||
const undoneSwapState = undoneSwapProbe.state;
|
||||
const undoneSwapSave = await readCampaignSave(page);
|
||||
const undoRestoreChecks = {
|
||||
runtimeSelection: JSON.stringify(undoneSwapState?.selectedSortieUnitIds) === JSON.stringify(midFormationSelectedBefore),
|
||||
campaignSelection: JSON.stringify(undoneSwapState?.campaign?.selectedSortieUnitIds) === JSON.stringify(midFormationCampaignSelectedBefore),
|
||||
currentSaveSelection: JSON.stringify(undoneSwapSave.current?.selectedSortieUnitIds) === JSON.stringify(midFormationSaveBefore.current?.selectedSortieUnitIds),
|
||||
slotSelection: JSON.stringify(undoneSwapSave.slot1?.selectedSortieUnitIds) === JSON.stringify(midFormationSaveBefore.slot1?.selectedSortieUnitIds),
|
||||
runtimeFormation: sameJsonValue(undoneSwapState?.sortieFormationAssignments, midFormationAssignmentsBefore),
|
||||
campaignFormation: sameJsonValue(undoneSwapState?.campaign?.sortieFormationAssignments, midFormationAssignmentsBefore),
|
||||
currentSaveFormation: sameJsonValue(undoneSwapSave.current?.sortieFormationAssignments, midFormationSaveBefore.current?.sortieFormationAssignments),
|
||||
slotFormation: sameJsonValue(undoneSwapSave.slot1?.sortieFormationAssignments, midFormationSaveBefore.slot1?.sortieFormationAssignments),
|
||||
runtimeItems: sameJsonValue(undoneSwapState?.sortieItemAssignments, midFormationItemAssignmentsBefore),
|
||||
campaignItems: sameJsonValue(undoneSwapState?.campaign?.sortieItemAssignments, midFormationItemAssignmentsBefore),
|
||||
currentSaveItems: sameJsonValue(undoneSwapSave.current?.sortieItemAssignments, midFormationSaveBefore.current?.sortieItemAssignments),
|
||||
slotItems: sameJsonValue(undoneSwapSave.slot1?.sortieItemAssignments, midFormationSaveBefore.slot1?.sortieItemAssignments)
|
||||
};
|
||||
assert(
|
||||
Object.values(undoRestoreChecks).every(Boolean),
|
||||
`Expected one-time undo to restore the exact roster, role map, and supply map in runtime and both saves: ${JSON.stringify({
|
||||
checks: undoRestoreChecks,
|
||||
expected: {
|
||||
runtimeSelected: midFormationSelectedBefore,
|
||||
campaignSelected: midFormationCampaignSelectedBefore,
|
||||
currentSaveSelected: midFormationSaveBefore.current?.selectedSortieUnitIds,
|
||||
slotSelected: midFormationSaveBefore.slot1?.selectedSortieUnitIds,
|
||||
formation: midFormationAssignmentsBefore,
|
||||
items: midFormationItemAssignmentsBefore
|
||||
},
|
||||
actual: {
|
||||
runtimeSelected: undoneSwapState?.selectedSortieUnitIds,
|
||||
campaignSelected: undoneSwapState?.campaign?.selectedSortieUnitIds,
|
||||
currentSaveSelected: undoneSwapSave.current?.selectedSortieUnitIds,
|
||||
slotSelected: undoneSwapSave.slot1?.selectedSortieUnitIds,
|
||||
runtimeFormation: undoneSwapState?.sortieFormationAssignments,
|
||||
campaignFormation: undoneSwapState?.campaign?.sortieFormationAssignments,
|
||||
currentSaveFormation: undoneSwapSave.current?.sortieFormationAssignments,
|
||||
slotFormation: undoneSwapSave.slot1?.sortieFormationAssignments,
|
||||
runtimeItems: undoneSwapState?.sortieItemAssignments,
|
||||
campaignItems: undoneSwapState?.campaign?.sortieItemAssignments,
|
||||
currentSaveItems: undoneSwapSave.current?.sortieItemAssignments,
|
||||
slotItems: undoneSwapSave.slot1?.sortieItemAssignments
|
||||
}
|
||||
})}`
|
||||
);
|
||||
assert(
|
||||
undoneSwapState?.sortieFocusedUnitId === 'ma-dai' &&
|
||||
undoneSwapState?.sortiePinnedSwapCandidateUnitId === null &&
|
||||
undoneSwapState?.sortieComparisonAction === null &&
|
||||
!undoneSwapState?.sortieSwapUndo?.available &&
|
||||
undoneSwapProbe.action.buttonVisible === false &&
|
||||
undoneSwapProbe.action.labelVisible === false,
|
||||
`Expected undo to restore Ma Dai focus and consume the panel action exactly once: ${JSON.stringify(undoneSwapProbe)}`
|
||||
);
|
||||
|
||||
const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||
await page.mouse.click(658, 196);
|
||||
await page.waitForFunction((previousScroll) => {
|
||||
@@ -979,6 +1237,49 @@ async function readCampaignSave(page) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readSortieComparisonActionPoint(page) {
|
||||
const probe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const actionButton = scene?.sortieComparisonPanelView?.actionButton;
|
||||
const actionLabel = scene?.sortieComparisonPanelView?.actionLabel;
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (!scene || !actionButton?.visible || !actionLabel?.visible || !canvas) {
|
||||
return {
|
||||
ready: false,
|
||||
buttonVisible: actionButton?.visible ?? false,
|
||||
labelVisible: actionLabel?.visible ?? false,
|
||||
label: actionLabel?.text ?? null
|
||||
};
|
||||
}
|
||||
|
||||
const bounds = actionButton.getBounds();
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
const logicalWidth = scene.scale.width;
|
||||
const logicalHeight = scene.scale.height;
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
ready: true,
|
||||
label: actionLabel.text,
|
||||
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
|
||||
canvasBounds: {
|
||||
x: canvasBounds.x,
|
||||
y: canvasBounds.y,
|
||||
width: canvasBounds.width,
|
||||
height: canvasBounds.height
|
||||
},
|
||||
logicalSize: { width: logicalWidth, height: logicalHeight },
|
||||
x: canvasBounds.left + centerX * canvasBounds.width / logicalWidth,
|
||||
y: canvasBounds.top + centerY * canvasBounds.height / logicalHeight
|
||||
};
|
||||
});
|
||||
assert(
|
||||
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
|
||||
`Expected a visible Phaser comparison action with a canvas-scaled click point: ${JSON.stringify(probe)}`
|
||||
);
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readBattleDoctrineProbe(page) {
|
||||
return page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
@@ -1146,6 +1447,28 @@ function assertSameMembers(values, expected, message) {
|
||||
assert(expected.every((value) => valueSet.has(value)), message);
|
||||
}
|
||||
|
||||
function sameJsonValue(value, expected) {
|
||||
return stableJson(value) === stableJson(expected);
|
||||
}
|
||||
|
||||
function stableJson(value) {
|
||||
return JSON.stringify(normalize(value));
|
||||
|
||||
function normalize(entry) {
|
||||
if (Array.isArray(entry)) {
|
||||
return entry.map(normalize);
|
||||
}
|
||||
if (entry && typeof entry === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.keys(entry)
|
||||
.sort()
|
||||
.map((key) => [key, normalize(entry[key])])
|
||||
);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeploymentMatchesPreview(positions, preview, message) {
|
||||
assert(Array.isArray(positions) && Array.isArray(preview), message);
|
||||
assert(positions.length === preview.length, message);
|
||||
|
||||
Reference in New Issue
Block a user