feat: unify FHD battle detail panels
This commit is contained in:
@@ -5497,6 +5497,8 @@ async function assertLargeRosterHud(browser, url) {
|
||||
assertLateBattleHeaderLayout(situationProbe, 'situation');
|
||||
assertLateBattleSidebarLayout(situationProbe, 'situation', true);
|
||||
|
||||
await assertFinalBattleFhdDetailPanels(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
scene?.renderRosterPanel?.('ally', '출전 위치 확정. 행동할 장수를 선택하세요.');
|
||||
@@ -5593,6 +5595,160 @@ async function assertLargeRosterHud(browser, url) {
|
||||
}
|
||||
}
|
||||
|
||||
async function assertFinalBattleFhdDetailPanels(page) {
|
||||
const allyFixture = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const unit = scene?.debugUnitById?.('zhuge-liang');
|
||||
const message = '대상 선택을 취소했습니다.\n공격, 책략, 도구, 대기 중 하나를 다시 선택하세요.\n우클릭하면 이동 취소로 돌아갑니다.';
|
||||
if (!scene || !unit) {
|
||||
return null;
|
||||
}
|
||||
scene.renderUnitDetail?.(unit, message);
|
||||
return { unitId: unit.id, unitName: unit.name, messageLineCount: message.split('\n').length };
|
||||
});
|
||||
assert(
|
||||
allyFixture?.unitId === 'zhuge-liang' && allyFixture.messageLineCount === 3,
|
||||
`Expected the late-battle ally detail fixture to render Zhuge Liang with the real three-line command-cancel guidance: ${JSON.stringify(allyFixture)}`
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle()?.battleHud?.unitDetailPanel?.unitId === 'zhuge-liang',
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const allyDetailProbe = await readLargeRosterHudProbe(page);
|
||||
assertUnitDetailPanelLayout(allyDetailProbe, 'zhuge-liang', 'ally');
|
||||
|
||||
const enemyFixture = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const unit = scene?.debugUnitById?.('northern-twelfth-scout-e');
|
||||
if (!scene || !unit) {
|
||||
return null;
|
||||
}
|
||||
scene.renderUnitDetail?.(unit, scene.enemyDetailMessage?.(unit) ?? '적군 전력을 확인하세요.');
|
||||
return { unitId: unit.id, unitName: unit.name };
|
||||
});
|
||||
assert(
|
||||
enemyFixture?.unitId === 'northern-twelfth-scout-e' && enemyFixture.unitName?.length >= 8,
|
||||
`Expected the late-battle enemy detail fixture to exercise a long officer name: ${JSON.stringify(enemyFixture)}`
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle()?.battleHud?.unitDetailPanel?.unitId === 'northern-twelfth-scout-e',
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const enemyDetailProbe = await readLargeRosterHudProbe(page);
|
||||
assertUnitDetailPanelLayout(enemyDetailProbe, 'northern-twelfth-scout-e', 'long enemy');
|
||||
await waitForBattleHudPaint(page);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-unit-detail.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'final battle unit detail');
|
||||
|
||||
const bondFixture = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
if (!scene) {
|
||||
return null;
|
||||
}
|
||||
const expectedBondIds = scene.activeSortieBonds?.().map((bond) => bond.id) ?? [];
|
||||
scene.bondPage = 0;
|
||||
scene.renderBondPanel?.();
|
||||
return { expectedBondIds };
|
||||
});
|
||||
assert(
|
||||
bondFixture?.expectedBondIds?.length > 4,
|
||||
`Expected the exact eight-officer final sortie to exercise paged active bonds: ${JSON.stringify(bondFixture)}`
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle()?.battleHud?.bondPanel?.page === 0,
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
|
||||
const firstBondProbe = await readLargeRosterHudProbe(page);
|
||||
const bondPageCount = firstBondProbe.bondPanel?.pageCount ?? 0;
|
||||
assert(
|
||||
bondPageCount > 1 && firstBondProbe.bondPanel?.totalCount === bondFixture.expectedBondIds.length,
|
||||
`Expected paged final-battle bond debug state to match the active sortie bonds: ${JSON.stringify({ bondFixture, bondPanel: firstBondProbe.bondPanel })}`
|
||||
);
|
||||
const visibleBondIds = [];
|
||||
for (let pageIndex = 0; pageIndex < bondPageCount; pageIndex += 1) {
|
||||
if (pageIndex > 0) {
|
||||
await page.evaluate((targetPage) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
scene.bondPage = targetPage;
|
||||
scene.renderBondPanel?.();
|
||||
}, pageIndex);
|
||||
await page.waitForFunction(
|
||||
(targetPage) => window.__HEROS_DEBUG__?.battle()?.battleHud?.bondPanel?.page === targetPage,
|
||||
pageIndex,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
}
|
||||
const bondProbe = pageIndex === 0 ? firstBondProbe : await readLargeRosterHudProbe(page);
|
||||
assertBondPanelLayout(bondProbe, pageIndex, bondPageCount, bondFixture.expectedBondIds.length);
|
||||
visibleBondIds.push(...bondProbe.bondPanel.visibleBondIds);
|
||||
}
|
||||
assert(
|
||||
visibleBondIds.length === bondFixture.expectedBondIds.length &&
|
||||
new Set(visibleBondIds).size === visibleBondIds.length &&
|
||||
sameJsonValue([...visibleBondIds].sort(), [...bondFixture.expectedBondIds].sort()),
|
||||
`Expected every active final-battle bond exactly once across all pages: ${JSON.stringify({ expected: bondFixture.expectedBondIds, actual: visibleBondIds })}`
|
||||
);
|
||||
await waitForBattleHudPaint(page);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-bond-panel.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'final battle bond panel');
|
||||
|
||||
const expectedThreatIds = [
|
||||
'northern-twelfth-scout-e',
|
||||
'northern-twelfth-camp-guard-e',
|
||||
'northern-twelfth-strategist-c',
|
||||
'northern-twelfth-cavalry-e'
|
||||
];
|
||||
const threatFixture = await page.evaluate((enemyIds) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const enemies = enemyIds.map((unitId) => scene?.debugUnitById?.(unitId)).filter(Boolean);
|
||||
if (!scene || enemies.length !== enemyIds.length) {
|
||||
return null;
|
||||
}
|
||||
scene.renderThreatDetail?.({ x: 38, y: 96, enemies, strongestBehavior: 'aggressive' });
|
||||
return { enemyIds: enemies.map((enemy) => enemy.id), enemyNames: enemies.map((enemy) => enemy.name) };
|
||||
}, expectedThreatIds);
|
||||
assert(
|
||||
sameJsonValue(threatFixture?.enemyIds, expectedThreatIds) && threatFixture.enemyNames.every((name) => name.length >= 8),
|
||||
`Expected the final-battle threat fixture to use four real long-name enemies: ${JSON.stringify(threatFixture)}`
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle()?.battleHud?.threatPanel?.enemyRows?.length === 4,
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const threatProbe = await readLargeRosterHudProbe(page);
|
||||
assertThreatPanelLayout(threatProbe, expectedThreatIds);
|
||||
await waitForBattleHudPaint(page);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-threat-panel.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'final battle threat panel');
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.__HEROS_GAME__?.scene.getScene('BattleScene')?.renderTerrainDetail?.(38, 96);
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
const panel = window.__HEROS_DEBUG__?.battle()?.battleHud?.terrainPanel;
|
||||
return panel?.tile?.x === 38 && panel?.tile?.y === 96;
|
||||
}, undefined, { timeout: 30000 });
|
||||
const terrainProbe = await readLargeRosterHudProbe(page);
|
||||
assertTerrainPanelLayout(terrainProbe, 'camp');
|
||||
await waitForBattleHudPaint(page);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-final-battle-terrain-panel.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'final battle terrain panel');
|
||||
}
|
||||
|
||||
async function waitForBattleHudPaint(page) {
|
||||
await page.evaluate(() => new Promise((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
||||
}));
|
||||
}
|
||||
|
||||
async function readLargeRosterHudProbe(page) {
|
||||
return page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
@@ -5604,6 +5760,10 @@ async function readLargeRosterHudProbe(page) {
|
||||
header: hud?.header ?? null,
|
||||
sidebar: hud?.sidebar ?? null,
|
||||
deploymentPanel: hud?.deploymentPanel ?? null,
|
||||
unitDetailPanel: hud?.unitDetailPanel ?? null,
|
||||
bondPanel: hud?.bondPanel ?? null,
|
||||
threatPanel: hud?.threatPanel ?? null,
|
||||
terrainPanel: hud?.terrainPanel ?? null,
|
||||
sceneBounds: scene
|
||||
? { x: 0, y: 0, width: scene.scale.width, height: scene.scale.height }
|
||||
: null,
|
||||
@@ -5661,7 +5821,7 @@ function assertLateBattleSidebarLayout(probe, label, expectMiniMap) {
|
||||
`Expected the late-battle ${label} sidebar below the sortie-order header: ${JSON.stringify(probe)}`
|
||||
);
|
||||
if (!expectMiniMap) {
|
||||
assert(miniMap?.visible === false, `Expected deployment to reserve the full sidebar by hiding the minimap: ${JSON.stringify(probe)}`);
|
||||
assert(miniMap?.visible === false, `Expected late-battle ${label} to reserve the full sidebar by hiding the minimap: ${JSON.stringify(probe)}`);
|
||||
return;
|
||||
}
|
||||
assert(
|
||||
@@ -5703,6 +5863,233 @@ function assertLateBattleDeploymentLayout(probe) {
|
||||
);
|
||||
}
|
||||
|
||||
function assertUnitDetailPanelLayout(probe, expectedUnitId, label) {
|
||||
const panel = probe.unitDetailPanel;
|
||||
assertLateBattleSidebarLayout(probe, `${label} unit detail`, false);
|
||||
assert(
|
||||
panel?.unitId === expectedUnitId &&
|
||||
panel.summaryBounds?.length === 3 &&
|
||||
panel.statBounds?.length === 5 &&
|
||||
panel.effectBounds?.length > 0 &&
|
||||
panel.effectBounds.length <= 3 &&
|
||||
panel.equipmentBounds?.length === 3 &&
|
||||
isFiniteBounds(panel.messageBounds),
|
||||
`Expected complete FHD ${label} unit-detail sections: ${JSON.stringify(probe)}`
|
||||
);
|
||||
|
||||
const allBounds = [
|
||||
panel.headerBounds,
|
||||
panel.nameBounds,
|
||||
panel.levelBadgeBounds,
|
||||
panel.hpBounds,
|
||||
...panel.summaryBounds,
|
||||
...panel.statBounds,
|
||||
panel.statusBounds,
|
||||
...panel.effectBounds,
|
||||
...panel.equipmentBounds,
|
||||
panel.messageBounds
|
||||
];
|
||||
assertDetailBoundsInside(probe, allBounds, `${label} unit detail`);
|
||||
assert(
|
||||
boundsInside(panel.nameBounds, panel.headerBounds) &&
|
||||
boundsInside(panel.levelBadgeBounds, panel.headerBounds) &&
|
||||
panel.nameBounds.x + panel.nameBounds.width <= panel.levelBadgeBounds.x + 1,
|
||||
`Expected the FHD ${label} unit name to remain inside the header and left of the level badge: ${JSON.stringify(panel)}`
|
||||
);
|
||||
assertHorizontalBounds(panel.summaryBounds, `${label} unit summary cards`);
|
||||
assertHorizontalBounds(panel.effectBounds, `${label} unit effect cards`);
|
||||
assertSequentialBounds(panel.statBounds, `${label} unit stat rows`);
|
||||
assertSequentialBounds(panel.equipmentBounds, `${label} unit equipment rows`);
|
||||
|
||||
const sectionBounds = [
|
||||
panel.headerBounds,
|
||||
panel.hpBounds,
|
||||
boundsUnion(panel.summaryBounds),
|
||||
boundsUnion(panel.statBounds),
|
||||
panel.statusBounds,
|
||||
boundsUnion(panel.effectBounds),
|
||||
boundsUnion(panel.equipmentBounds),
|
||||
panel.messageBounds
|
||||
];
|
||||
assertSequentialBounds(sectionBounds, `${label} unit-detail sections`);
|
||||
}
|
||||
|
||||
function assertBondPanelLayout(probe, expectedPage, expectedPageCount, expectedTotalCount) {
|
||||
const panel = probe.bondPanel;
|
||||
assertLateBattleSidebarLayout(probe, `bond page ${expectedPage + 1}`, false);
|
||||
const expectedVisibleCount = Math.min(
|
||||
panel?.rowsPerPage ?? 0,
|
||||
Math.max(0, expectedTotalCount - expectedPage * (panel?.rowsPerPage ?? 0))
|
||||
);
|
||||
assert(
|
||||
panel?.page === expectedPage &&
|
||||
panel.pageCount === expectedPageCount &&
|
||||
panel.totalCount === expectedTotalCount &&
|
||||
panel.rowsPerPage > 0 &&
|
||||
panel.visibleBondIds?.length === expectedVisibleCount &&
|
||||
panel.rowLayouts?.length === expectedVisibleCount &&
|
||||
new Set(panel.visibleBondIds).size === panel.visibleBondIds.length &&
|
||||
sameJsonValue(panel.rowLayouts.map((row) => row.bondId), panel.visibleBondIds) &&
|
||||
isFiniteBounds(panel.navigationBounds) &&
|
||||
isFiniteBounds(panel.messageBounds),
|
||||
`Expected complete paged FHD bond state on page ${expectedPage + 1}: ${JSON.stringify(probe)}`
|
||||
);
|
||||
|
||||
const rowBounds = panel.rowLayouts.map((row) => row.rowBounds);
|
||||
const allBounds = [
|
||||
panel.headerBounds,
|
||||
...rowBounds,
|
||||
...panel.rowLayouts.flatMap((row) => [row.nameBounds, row.levelBounds, row.titleBounds, row.gaugeBounds]),
|
||||
panel.navigationBounds,
|
||||
panel.messageBounds
|
||||
];
|
||||
assertDetailBoundsInside(probe, allBounds, `bond page ${expectedPage + 1}`);
|
||||
panel.rowLayouts.forEach((row) => {
|
||||
assert(
|
||||
boundsInside(row.nameBounds, row.rowBounds) &&
|
||||
boundsInside(row.levelBounds, row.rowBounds) &&
|
||||
boundsInside(row.titleBounds, row.rowBounds) &&
|
||||
boundsInside(row.gaugeBounds, row.rowBounds) &&
|
||||
row.nameBounds.x + row.nameBounds.width <= row.levelBounds.x + 1 &&
|
||||
row.titleBounds.x + row.titleBounds.width <= row.gaugeBounds.x + 1,
|
||||
`Expected bond ${row.bondId} names, levels, titles, and gauges in separate FHD columns: ${JSON.stringify(row)}`
|
||||
);
|
||||
});
|
||||
assertSequentialBounds(rowBounds, `bond rows on page ${expectedPage + 1}`);
|
||||
assertSequentialBounds(
|
||||
[panel.headerBounds, ...rowBounds, panel.navigationBounds, panel.messageBounds],
|
||||
`bond sections on page ${expectedPage + 1}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertThreatPanelLayout(probe, expectedEnemyIds) {
|
||||
const panel = probe.threatPanel;
|
||||
assertLateBattleSidebarLayout(probe, 'threat detail', true);
|
||||
assert(
|
||||
panel?.tile?.x === 38 &&
|
||||
panel.tile.y === 96 &&
|
||||
panel.summaryRows?.length === 4 &&
|
||||
panel.enemyRows?.length === expectedEnemyIds.length &&
|
||||
sameJsonValue(panel.enemyRows.map((row) => row.unitId), expectedEnemyIds) &&
|
||||
isFiniteBounds(panel.messageBounds),
|
||||
`Expected the four-enemy FHD threat fixture at the final-camp tile: ${JSON.stringify(probe)}`
|
||||
);
|
||||
const summaryBounds = panel.summaryRows.map((row) => row.bounds);
|
||||
const enemyBounds = panel.enemyRows.map((row) => row.rowBounds);
|
||||
const allBounds = [
|
||||
panel.headerBounds,
|
||||
...summaryBounds,
|
||||
...panel.summaryRows.flatMap((row) => [row.labelBounds, row.valueBounds]),
|
||||
...enemyBounds,
|
||||
...panel.enemyRows.flatMap((row) => [row.nameBounds, row.valueBounds]),
|
||||
panel.messageBounds
|
||||
];
|
||||
assertDetailBoundsInside(probe, allBounds, 'threat detail');
|
||||
assertSituationRows(panel.summaryRows, 'threat summary');
|
||||
panel.enemyRows.forEach((row) => {
|
||||
assert(
|
||||
boundsInside(row.nameBounds, row.rowBounds) &&
|
||||
boundsInside(row.valueBounds, row.rowBounds) &&
|
||||
!boundsOverlap(row.nameBounds, row.valueBounds, 2),
|
||||
`Expected threat names and metrics on separate readable lines for ${row.unitId}: ${JSON.stringify(row)}`
|
||||
);
|
||||
});
|
||||
assertSequentialBounds(enemyBounds, 'threat enemy rows');
|
||||
assertSequentialBounds(
|
||||
[panel.headerBounds, ...summaryBounds, ...enemyBounds, panel.messageBounds],
|
||||
'threat detail sections'
|
||||
);
|
||||
assert(
|
||||
panel.messageBounds.y + panel.messageBounds.height <= probe.miniMap.frameBounds.y + 1,
|
||||
`Expected the threat guidance above the FHD minimap: ${JSON.stringify(probe)}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertTerrainPanelLayout(probe, expectedTerrain) {
|
||||
const panel = probe.terrainPanel;
|
||||
assertLateBattleSidebarLayout(probe, 'terrain detail', true);
|
||||
assert(
|
||||
panel?.tile?.x === 38 &&
|
||||
panel.tile.y === 96 &&
|
||||
panel.terrain === expectedTerrain &&
|
||||
panel.summaryRows?.length === 6 &&
|
||||
isFiniteBounds(panel.messageBounds),
|
||||
`Expected complete FHD ${expectedTerrain} terrain details at the final-camp tile: ${JSON.stringify(probe)}`
|
||||
);
|
||||
const summaryBounds = panel.summaryRows.map((row) => row.bounds);
|
||||
const allBounds = [
|
||||
panel.headerBounds,
|
||||
...summaryBounds,
|
||||
...panel.summaryRows.flatMap((row) => [row.labelBounds, row.valueBounds]),
|
||||
panel.messageBounds
|
||||
];
|
||||
assertDetailBoundsInside(probe, allBounds, 'terrain detail');
|
||||
assertSituationRows(panel.summaryRows, 'terrain summary');
|
||||
assertSequentialBounds([panel.headerBounds, ...summaryBounds, panel.messageBounds], 'terrain detail sections');
|
||||
assert(
|
||||
panel.messageBounds.y + panel.messageBounds.height <= probe.miniMap.frameBounds.y + 1,
|
||||
`Expected the terrain guidance above the FHD minimap: ${JSON.stringify(probe)}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertSituationRows(rows, label) {
|
||||
rows.forEach((row, index) => {
|
||||
assert(
|
||||
boundsInside(row.labelBounds, row.bounds) &&
|
||||
boundsInside(row.valueBounds, row.bounds) &&
|
||||
row.labelBounds.x + row.labelBounds.width <= row.valueBounds.x + 1,
|
||||
`Expected ${label} row ${index + 1} labels and values in separate FHD columns: ${JSON.stringify(row)}`
|
||||
);
|
||||
});
|
||||
assertSequentialBounds(rows.map((row) => row.bounds), `${label} rows`);
|
||||
}
|
||||
|
||||
function assertDetailBoundsInside(probe, boundsList, label) {
|
||||
assert(
|
||||
isFiniteBounds(probe.sidebar?.bounds) &&
|
||||
isFiniteBounds(probe.panelBounds) &&
|
||||
isFiniteBounds(probe.sceneBounds) &&
|
||||
boundsList.length > 0 &&
|
||||
boundsList.every((bounds) =>
|
||||
boundsInside(bounds, probe.sidebar.bounds) &&
|
||||
boundsInside(bounds, probe.panelBounds) &&
|
||||
boundsInside(bounds, probe.sceneBounds)
|
||||
),
|
||||
`Expected every ${label} bound inside the sidebar, panel, and FHD scene: ${JSON.stringify({ boundsList, probe })}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertSequentialBounds(boundsList, label) {
|
||||
assert(
|
||||
boundsList.length > 0 &&
|
||||
boundsList.every(isFiniteBounds) &&
|
||||
boundsList.every((bounds, index) =>
|
||||
index === 0 || boundsList[index - 1].y + boundsList[index - 1].height <= bounds.y + 1
|
||||
),
|
||||
`Expected non-overlapping sequential ${label}: ${JSON.stringify(boundsList)}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertHorizontalBounds(boundsList, label) {
|
||||
const ordered = [...boundsList].sort((left, right) => left.x - right.x);
|
||||
assert(
|
||||
ordered.length > 0 &&
|
||||
ordered.every(isFiniteBounds) &&
|
||||
ordered.every((bounds, index) =>
|
||||
index === 0 || ordered[index - 1].x + ordered[index - 1].width <= bounds.x + 1
|
||||
),
|
||||
`Expected non-overlapping horizontal ${label}: ${JSON.stringify(ordered)}`
|
||||
);
|
||||
}
|
||||
|
||||
function boundsUnion(boundsList) {
|
||||
const left = Math.min(...boundsList.map((bounds) => bounds.x));
|
||||
const top = Math.min(...boundsList.map((bounds) => bounds.y));
|
||||
const right = Math.max(...boundsList.map((bounds) => bounds.x + bounds.width));
|
||||
const bottom = Math.max(...boundsList.map((bounds) => bounds.y + bounds.height));
|
||||
return { x: left, y: top, width: right - left, height: bottom - top };
|
||||
}
|
||||
|
||||
function assertLargeRosterPageLayout(probe, label) {
|
||||
const { roster, panelBounds } = probe;
|
||||
assert(boundsInside(roster.listBounds, panelBounds), `Expected roster rows on page ${label} inside the side panel: ${JSON.stringify(probe)}`);
|
||||
|
||||
Reference in New Issue
Block a user