Commit 79a08ff4 authored by liguangyu06's avatar liguangyu06
Browse files

修复报表表头 strict mode、列设置关面板超时及汇总/排序定位,避免夜间全量被脚本误杀。

parent 35fe36af
...@@ -7,9 +7,10 @@ import { columnHeaderMatchTokens } from '../../core/column-header-match.util'; ...@@ -7,9 +7,10 @@ import { columnHeaderMatchTokens } from '../../core/column-header-match.util';
import { CenterReportSheetLocators } from '../../locators/spd/center-report-sheet.locator'; import { CenterReportSheetLocators } from '../../locators/spd/center-report-sheet.locator';
import { DownloadCenterComponent } from '../components/download-center.component'; import { DownloadCenterComponent } from '../components/download-center.component';
import { DrawerFormControls } from './helpers/drawer-form-controls.helper'; import { DrawerFormControls } from './helpers/drawer-form-controls.helper';
import { ColumnSortControls } from './helpers/column-sort.helper'; import { ColumnSortControls, clickColumnSortControl } from './helpers/column-sort.helper';
import { import {
ColumnSettingsControls, ColumnSettingsControls,
executeColumnSettingsToggleRestore,
findUsableColumnSettingsTrigger, findUsableColumnSettingsTrigger,
revealColumnSettingsTriggerByScroll, revealColumnSettingsTriggerByScroll,
} from './helpers/column-settings.helper'; } from './helpers/column-settings.helper';
...@@ -66,7 +67,7 @@ export class CenterReportSheetPage extends BasePage { ...@@ -66,7 +67,7 @@ export class CenterReportSheetPage extends BasePage {
columnSorter: (columnName) => this.locators.columnSorter(columnName), columnSorter: (columnName) => this.locators.columnSorter(columnName),
columnSorterUp: (columnName) => this.locators.columnSorterUp(columnName), columnSorterUp: (columnName) => this.locators.columnSorterUp(columnName),
columnSorterDown: (columnName) => this.locators.columnSorterDown(columnName), columnSorterDown: (columnName) => this.locators.columnSorterDown(columnName),
clickColumnSort: (columnName) => this.clickColumnSort(columnName), clickColumnSort: (columnName, order) => this.clickColumnSort(columnName, order),
getFirstRowText: () => this.getFirstRowText(), getFirstRowText: () => this.getFirstRowText(),
getColumnValues: (columnName, maxRows) => this.getColumnValues(columnName, maxRows), getColumnValues: (columnName, maxRows) => this.getColumnValues(columnName, maxRows),
getRowSignatures: (maxRows) => this.getRowSignatures(maxRows), getRowSignatures: (maxRows) => this.getRowSignatures(maxRows),
...@@ -85,7 +86,7 @@ export class CenterReportSheetPage extends BasePage { ...@@ -85,7 +86,7 @@ export class CenterReportSheetPage extends BasePage {
}); });
this.summaryMatchControls = new ReportSummaryMatchControls({ this.summaryMatchControls = new ReportSummaryMatchControls({
profile: detailSheetSummaryMatch, profile: detailSheetSummaryMatch,
reportBody: this.locators.report.locator('body'), reportBody: this.locators.report,
dataRows: this.locators.dataRows, dataRows: this.locators.dataRows,
paginationTotal: this.locators.paginationTotal, paginationTotal: this.locators.paginationTotal,
sampleRowIndex: centerReportSheetData.sampleRowIndex, sampleRowIndex: centerReportSheetData.sampleRowIndex,
...@@ -114,8 +115,14 @@ export class CenterReportSheetPage extends BasePage { ...@@ -114,8 +115,14 @@ export class CenterReportSheetPage extends BasePage {
.locator('iframe[src*="centerPages/sheet"]') .locator('iframe[src*="centerPages/sheet"]')
.first() .first()
.waitFor({ state: 'attached', timeout: timeouts.pageReady }); .waitFor({ state: 'attached', timeout: timeouts.pageReady });
await this.waitVisible(this.locators.searchBtn, timeouts.pageReady);
if (!(await this.locators.productName.isVisible().catch(() => false))) {
const toggle = this.locators.report.getByText(/^(展开|收起)$/).first();
if (await toggle.isVisible().catch(() => false)) {
await toggle.click().catch(() => undefined);
}
}
await this.waitVisible(this.locators.productName, timeouts.pageReady); await this.waitVisible(this.locators.productName, timeouts.pageReady);
await this.waitVisible(this.locators.searchBtn);
await this.waitVisible(this.locators.advancedFilter); await this.waitVisible(this.locators.advancedFilter);
await this.assertPageHealthy('页面加载完成'); await this.assertPageHealthy('页面加载完成');
}, },
...@@ -244,8 +251,21 @@ export class CenterReportSheetPage extends BasePage { ...@@ -244,8 +251,21 @@ export class CenterReportSheetPage extends BasePage {
} }
async getFirstRowText(): Promise<string> { async getFirstRowText(): Promise<string> {
await expect(this.locators.dataRows.first()).toBeVisible({ timeout: 15_000 }); await expect
return (await this.locators.dataRows.first().innerText()).trim(); .poll(async () => this.locators.dataRows.count(), { timeout: 20_000 })
.toBeGreaterThan(0);
const rows = this.locators.dataRows;
const count = await rows.count();
let row = rows.first();
for (let i = 0; i < Math.min(count, 20); i += 1) {
const candidate = rows.nth(i);
if (await candidate.isVisible().catch(() => false)) {
row = candidate;
break;
}
}
await row.scrollIntoViewIfNeeded().catch(() => undefined);
return ((await row.innerText().catch(() => '')) ?? '').trim();
} }
async getColumnValues(columnName: string, maxRows = 8): Promise<string[]> { async getColumnValues(columnName: string, maxRows = 8): Promise<string[]> {
...@@ -669,13 +689,25 @@ export class CenterReportSheetPage extends BasePage { ...@@ -669,13 +689,25 @@ export class CenterReportSheetPage extends BasePage {
); );
} }
/** 点击指定列排序控件(Ant Design:无序 → 升序 → 降序循环)。 */ /** 点击指定列升序或降序按钮(Ant Design:▲ / ▼ 分别激活)。 */
async clickColumnSort(columnName: string): Promise<void> { async clickColumnSort(columnName: string, order?: 'ascend' | 'descend'): Promise<void> {
await this.scrollColumnIntoView(columnName); await this.scrollColumnIntoView(columnName);
const header = this.locators.columnHeaderCell(columnName);
const sorter = this.locators.columnSorter(columnName); const sorter = this.locators.columnSorter(columnName);
await this.waitVisible(sorter, 15_000); if ((await sorter.count()) > 0) {
await test.step(`点击${columnName}列排序`, async () => { await sorter.waitFor({ state: 'attached', timeout: 15_000 }).catch(() => undefined);
await sorter.click({ force: true }); } else {
await this.waitVisible(header, 15_000);
}
const label = order === 'descend' ? '降序' : order === 'ascend' ? '升序' : '排序';
await test.step(`点击${columnName}${label}`, async () => {
await clickColumnSortControl({
header,
sorter,
up: this.locators.columnSorterUp(columnName),
down: this.locators.columnSorterDown(columnName),
order,
});
}); });
await this.locators.report.locator('.ant-spin-spinning').waitFor({ state: 'hidden', timeout: 30_000 }).catch(() => undefined); await this.locators.report.locator('.ant-spin-spinning').waitFor({ state: 'hidden', timeout: 30_000 }).catch(() => undefined);
await this.locators.tableBody.waitFor({ state: 'visible', timeout: 15_000 }).catch(() => undefined); await this.locators.tableBody.waitFor({ state: 'visible', timeout: 15_000 }).catch(() => undefined);
...@@ -751,16 +783,32 @@ export class CenterReportSheetPage extends BasePage { ...@@ -751,16 +783,32 @@ export class CenterReportSheetPage extends BasePage {
await sorter.scrollIntoViewIfNeeded().catch(() => undefined); await sorter.scrollIntoViewIfNeeded().catch(() => undefined);
} }
/** 当前页面指定列是否可见且带排序控件。 */ /** 当前页面指定列是否可见且带排序控件。固定列 sorter 可能被 overflow 裁切,不能只信 isVisible。 */
async isColumnSortable(columnName: string): Promise<boolean> { async isColumnSortable(columnName: string): Promise<boolean> {
try { try {
await this.scrollColumnIntoView(columnName); await this.scrollColumnIntoView(columnName);
const sorter = this.locators.columnSorter(columnName); const sorter = this.locators.columnSorter(columnName);
const sortable = (await sorter.count()) > 0 && (await sorter.isVisible()); if ((await sorter.count()) > 0) {
if (!sortable) { return true;
}
const header = this.locators.columnHeaderCell(columnName);
if ((await header.count()) === 0) {
await this.assertPageHealthy(`列「${columnName}」不可排序`); await this.assertPageHealthy(`列「${columnName}」不可排序`);
return false;
} }
return sortable; const hasSorter = await header
.evaluate((el) => {
const th = ((el as HTMLElement).closest('th') ?? el) as HTMLElement;
return Boolean(
th.querySelector('.ant-table-column-sorter') ||
th.classList.contains('ant-table-column-has-sorters'),
);
})
.catch(() => false);
if (!hasSorter) {
await this.assertPageHealthy(`列「${columnName}」不可排序`);
}
return hasSorter;
} catch { } catch {
await this.assertPageHealthy(`列「${columnName}」不可排序`); await this.assertPageHealthy(`列「${columnName}」不可排序`);
return false; return false;
...@@ -901,26 +949,32 @@ export class CenterReportSheetPage extends BasePage { ...@@ -901,26 +949,32 @@ export class CenterReportSheetPage extends BasePage {
} }
async openColumnSettings(): Promise<void> { async openColumnSettings(): Promise<void> {
await this.closeColumnSettingsPanel(); const panel = this.locators.columnSettingsPanel();
await this.locators.columnSettingsPanel().waitFor({ state: 'hidden', timeout: 3_000 }).catch(() => undefined); if (await panel.isVisible().catch(() => false)) {
await revealColumnSettingsTriggerByScroll({ return;
scroll: this.tableScrollLayoutControls, }
tableWrapper: this.locators.report.locator('.ant-table-wrapper').first(), const triggerCandidates = [this.locators.columnSettingsTrigger];
triggerCandidates: [this.locators.columnSettingsTrigger], let trigger = await findUsableColumnSettingsTrigger(triggerCandidates);
horizontalScrollTargetColumn: centerReportSheetData.horizontalScrollTargetColumn, if (!trigger) {
horizontalScrollDragDelta: centerReportSheetData.horizontalScrollDragDelta, await revealColumnSettingsTriggerByScroll({
scrollColumnIntoView: async (column) => { scroll: this.tableScrollLayoutControls,
await this.locators.tableHeader(column).scrollIntoViewIfNeeded().catch(() => undefined); tableWrapper: this.locators.report.locator('.ant-table-wrapper').first(),
}, triggerCandidates,
}); horizontalScrollTargetColumn: centerReportSheetData.horizontalScrollTargetColumn,
if (!(await findUsableColumnSettingsTrigger([this.locators.columnSettingsTrigger]))) { horizontalScrollDragDelta: centerReportSheetData.horizontalScrollDragDelta,
scrollColumnIntoView: async (column) => {
await this.locators.tableHeader(column).scrollIntoViewIfNeeded().catch(() => undefined);
},
});
trigger = await findUsableColumnSettingsTrigger(triggerCandidates);
}
if (!trigger) {
await this.guardedSkip('列设置齿轮不可见'); await this.guardedSkip('列设置齿轮不可见');
} }
await this.waitVisible(this.locators.columnSettingsTrigger, 15_000);
await test.step('打开列设置', async () => { await test.step('打开列设置', async () => {
await this.locators.columnSettingsTrigger.click({ force: true }); await trigger!.click({ force: true });
}); });
await this.waitVisible(this.locators.columnSettingsPanel(), 10_000); await this.waitVisible(panel, 10_000);
} }
async expectColumnSettingsPanel(): Promise<void> { async expectColumnSettingsPanel(): Promise<void> {
...@@ -1002,6 +1056,19 @@ export class CenterReportSheetPage extends BasePage { ...@@ -1002,6 +1056,19 @@ export class CenterReportSheetPage extends BasePage {
return centerReportSheetData.columnSettingsToggleCandidates[0]; return centerReportSheetData.columnSettingsToggleCandidates[0];
} }
/** 场景:取消勾选非核心列后表头隐藏,再勾选后表头恢复。 */
async executeColumnSettingsToggleRestoreTest(): Promise<void> {
await executeColumnSettingsToggleRestore({
pickToggleColumnName: () => this.pickToggleColumnName(),
openColumnSettings: () => this.openColumnSettings(),
setColumnVisibleInSettings: (name, visible) => this.setColumnVisibleInSettings(name, visible),
closeColumnSettingsPanel: () => this.closeColumnSettingsPanel(),
expectTableHeaderVisible: (name, visible) => this.expectTableHeaderVisible(name, visible),
expectSettingsCheckboxChecked: (name, checked) =>
this.expectSettingsCheckboxChecked(name, checked),
});
}
/** 选取多个当前可见的非核心列(用于多列调整场景)。 */ /** 选取多个当前可见的非核心列(用于多列调整场景)。 */
async pickMultipleToggleColumnNames(count = 2): Promise<string[]> { async pickMultipleToggleColumnNames(count = 2): Promise<string[]> {
const picked: string[] = []; const picked: string[] = [];
......
...@@ -162,25 +162,32 @@ export class ConsumeTrendPage extends ReconCostReportSharedPage { ...@@ -162,25 +162,32 @@ export class ConsumeTrendPage extends ReconCostReportSharedPage {
.locator('.ant-table-wrapper') .locator('.ant-table-wrapper')
.nth(this.data.tableWrapperIndex ?? 0); .nth(this.data.tableWrapperIndex ?? 0);
const want = headerLabel.replace(/\s+/g, ''); const want = headerLabel.replace(/\s+/g, '');
const isShown = async () =>
wrapper.evaluate((root, label) => {
const ths = Array.from(root.querySelectorAll('.ant-table-thead th'));
return ths.some((th) => {
const text = (th.textContent ?? '').replace(/\s+/g, '');
if (text !== label) {
return false;
}
const el = th as HTMLElement;
return el.offsetWidth > 0 && el.offsetHeight > 0;
});
}, want);
if (visible) {
await this.scrollTableColumnIntoView(headerLabel).catch(() => undefined);
}
await expect await expect
.poll( .poll(
async () => { async () => {
if (visible) { const shown = await isShown();
if (visible && !shown) {
await this.scrollTableColumnIntoView(headerLabel).catch(() => undefined); await this.scrollTableColumnIntoView(headerLabel).catch(() => undefined);
return isShown();
} }
return wrapper.evaluate((root, label) => { return shown;
const ths = Array.from(root.querySelectorAll('.ant-table-thead th'));
return ths.some((th) => {
const text = (th.textContent ?? '').replace(/\s+/g, '');
if (text !== label) {
return false;
}
const el = th as HTMLElement;
return el.offsetWidth > 0 && el.offsetHeight > 0;
});
}, want);
}, },
{ timeout: 15_000 }, { timeout: 15_000, intervals: [500, 1_000, 2_000] },
) )
.toBe(visible); .toBe(visible);
} }
......
...@@ -2,6 +2,7 @@ import { expect, test, type Page } from '@playwright/test'; ...@@ -2,6 +2,7 @@ import { expect, test, type Page } from '@playwright/test';
import { waitAntSpinGone } from '../../core/smart-wait'; import { waitAntSpinGone } from '../../core/smart-wait';
import { costDeptClassifyChargeData } from '../../data/cost-dept-classify-charge.data'; import { costDeptClassifyChargeData } from '../../data/cost-dept-classify-charge.data';
import { clickColumnSortControl, readColumnSortOrder } from './helpers/column-sort.helper';
import { ReconCostReportSharedPage } from './recon-cost-report-shared.page'; import { ReconCostReportSharedPage } from './recon-cost-report-shared.page';
/** 科室成本分类汇总(计费科室):透视表首行表头含可排序「科室名称」。 */ /** 科室成本分类汇总(计费科室):透视表首行表头含可排序「科室名称」。 */
...@@ -42,35 +43,22 @@ export class CostDeptClassifyChargePage extends ReconCostReportSharedPage { ...@@ -42,35 +43,22 @@ export class CostDeptClassifyChargePage extends ReconCostReportSharedPage {
await expect(headerCell, `主表未找到可排序列「${columnName}」`).toBeVisible({ timeout: 15_000 }); await expect(headerCell, `主表未找到可排序列「${columnName}」`).toBeVisible({ timeout: 15_000 });
const sorter = headerCell.locator('.ant-table-column-sorter').first(); const sorter = headerCell.locator('.ant-table-column-sorter').first();
await expect(sorter, `「${columnName}」列无排序控件`).toBeVisible(); await expect(sorter, `「${columnName}」列无排序控件`).toBeVisible();
const clickTarget = const up = headerCell.locator('.ant-table-column-sorter-up').first();
(await headerCell.locator('.ant-table-column-sorters').count()) > 0 const down = headerCell.locator('.ant-table-column-sorter-down').first();
? headerCell.locator('.ant-table-column-sorters').first()
: sorter;
const readOrder = async (): Promise<'ascend' | 'descend' | null> => {
const ariaSort = await headerCell.getAttribute('aria-sort');
if (ariaSort === 'ascending') return 'ascend';
if (ariaSort === 'descending') return 'descend';
const upActive = await headerCell
.locator('.ant-table-column-sorter-up')
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on'))
.catch(() => false);
if (upActive) return 'ascend';
const downActive = await headerCell
.locator('.ant-table-column-sorter-down')
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on'))
.catch(() => false);
if (downActive) return 'descend';
return null;
};
const clickUntil = async (target: 'ascend' | 'descend'): Promise<void> => { const clickUntil = async (target: 'ascend' | 'descend'): Promise<void> => {
for (let attempt = 0; attempt < 4; attempt += 1) { for (let attempt = 0; attempt < 4; attempt += 1) {
if ((await readOrder()) === target) return; if ((await readColumnSortOrder(headerCell)) === target) return;
await clickTarget.click({ force: true }); await clickColumnSortControl({
header: headerCell,
sorter,
up,
down,
order: target,
});
await waitAntSpinGone(wrapper); await waitAntSpinGone(wrapper);
} }
expect(await readOrder(), `列「${columnName}」未能切换到${target === 'ascend' ? '升序' : '降序'}`).toBe( expect(await readColumnSortOrder(headerCell), `列「${columnName}」未能切换到${target === 'ascend' ? '升序' : '降序'}`).toBe(
target, target,
); );
}; };
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
costFinancePsiData, costFinancePsiData,
costFinancePsiSummaryMetrics, costFinancePsiSummaryMetrics,
} from '../../data/cost-finance-psi.data'; } from '../../data/cost-finance-psi.data';
import { parseReportNumber } from './helpers/report-summary.helper'; import { parseSummaryMetricFromText, readReportFooterText } from './helpers/report-summary.helper';
import { ReconCostReportSharedPage } from './recon-cost-report-shared.page'; import { ReconCostReportSharedPage } from './recon-cost-report-shared.page';
export class CostFinancePsiPage extends ReconCostReportSharedPage { export class CostFinancePsiPage extends ReconCostReportSharedPage {
...@@ -67,32 +67,14 @@ export class CostFinancePsiPage extends ReconCostReportSharedPage { ...@@ -67,32 +67,14 @@ export class CostFinancePsiPage extends ReconCostReportSharedPage {
private async readSummaryMetric(labels: readonly string[]): Promise<number> { private async readSummaryMetric(labels: readonly string[]): Promise<number> {
const footerText = await this.readFooterText(); const footerText = await this.readFooterText();
for (const label of labels) { for (const label of labels) {
const fromFooter = this.parseFooterMetric(footerText, label); const fromFooter = parseSummaryMetricFromText(footerText, label);
if (Number.isFinite(fromFooter)) return fromFooter; if (Number.isFinite(fromFooter)) return fromFooter;
} }
return NaN; return NaN;
} }
private parseFooterMetric(bodyText: string, label: string): number {
const compact = bodyText.replace(/\s+/g, ' ');
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const matched = compact.match(new RegExp(`${escaped}\\s*[=::]?\\s*([+\\-−–]?[\\d,]+(?:\\.[\\d]+)?|—|-)`));
if (!matched) {
return NaN;
}
return parseReportNumber(matched[1]);
}
private async readFooterText(): Promise<string> { private async readFooterText(): Promise<string> {
const chips = this.locators.report return readReportFooterText(this.locators.report, ['期初', '入库', '出库', '报损', '期末', '总金额']);
.locator('span.margin-left-8, .summary-footer, .report-summary, .ant-table-summary')
.filter({ hasText: /期初|入库|出库|报损|期末/ });
const parts = await chips.allInnerTexts();
const joined = parts.map((text) => text.replace(/\s+/g, ' ').trim()).filter(Boolean).join(' ');
if (/期初|入库|出库|报损|期末/.test(joined)) {
return joined;
}
return (await this.locators.report.locator('body').innerText()).replace(/\s+/g, ' ');
} }
} }
......
...@@ -204,6 +204,77 @@ export class CostProductRankPage extends ReconCostReportSharedPage { ...@@ -204,6 +204,77 @@ export class CostProductRankPage extends ReconCostReportSharedPage {
.getByText(name, { exact: false }); .getByText(name, { exact: false });
} }
/** 左侧分页总条数(勿读右侧明细分页)。 */
async expectLeftPaginationTotal(): Promise<void> {
await this.withPaginationTableIndex(this.data.tableWrapperIndex ?? 0, () =>
this.expectPaginationTotal(),
);
}
/**
* STAT-CPR-022:左右分页独立。
* 先切左侧第 2 页并确认右侧页码不变,再在右侧有 2 页时切右侧第 2 页并确认左侧仍停在第 2 页。
*/
async executeIndependentDualTablePage2Test(): Promise<void> {
const leftIdx = this.data.tableWrapperIndex ?? 0;
const rightIdx = this.data.detailTableWrapperIndex ?? 1;
const rightPageBefore = await this.readActivePageForTable(rightIdx);
await this.executePage2SwitchTest(leftIdx);
await this.expectActivePageForTable(leftIdx, 2);
if (rightPageBefore) {
const rightPageNum = Number.parseInt(rightPageBefore, 10);
if (Number.isFinite(rightPageNum)) {
await this.expectActivePageForTable(rightIdx, rightPageNum);
}
}
await this.selectProductRankRow();
await expect(this.locators.detailDataRows.first()).toBeVisible({ timeout: 15_000 });
const rightHasPage2 = await this.withPaginationTableIndex(rightIdx, async () =>
(await this.locators.paginationItem(2).count()) > 0,
);
if (!rightHasPage2) {
return;
}
const leftFirst = await this.getFirstRowText();
await this.withPaginationTableIndex(rightIdx, async () => {
const first = await this.getFirstRowText();
const item = this.locators.paginationItem(2);
await this.locators.paginationTotal.scrollIntoViewIfNeeded().catch(() => undefined);
await item.evaluate((node) => {
const link = node.querySelector('a') as HTMLElement | null;
(link ?? (node as HTMLElement)).click();
});
await this.locators.report
.locator('.ant-spin-spinning')
.waitFor({ state: 'hidden', timeout: 30_000 })
.catch(() => undefined);
await this.waitForTableReady();
await this.expectActivePage(2);
const second = await this.getFirstRowText();
expect(first, '右侧第1页首行应有数据').toBeTruthy();
expect(second, '右侧第2页首行应有数据').toBeTruthy();
expect(second).not.toEqual(first);
});
await this.expectActivePageForTable(leftIdx, 2);
const leftAfterRightPage2 = await this.getFirstRowText();
expect(leftAfterRightPage2, '右侧翻页不应改写左侧当前页数据').toEqual(leftFirst);
}
private async readActivePageForTable(tableWrapperIndex: number): Promise<string> {
return this.withPaginationTableIndex(tableWrapperIndex, async () => {
const active = this.locators.paginationActiveItem();
if ((await active.count()) === 0) return '';
return ((await active.innerText().catch(() => '')) ?? '').trim();
});
}
private async expectActivePageForTable(tableWrapperIndex: number, pageNum: number): Promise<void> {
await this.withPaginationTableIndex(tableWrapperIndex, () => this.expectActivePage(pageNum));
}
/** 左侧产品排名列表:查询后导出。 */ /** 左侧产品排名列表:查询后导出。 */
async exportLeftRankList(): Promise<void> { async exportLeftRankList(): Promise<void> {
await this.search(); await this.search();
......
...@@ -46,8 +46,8 @@ export class ColumnResizeControls { ...@@ -46,8 +46,8 @@ export class ColumnResizeControls {
const tokens = columnHeaderMatchTokens(columnName); const tokens = columnHeaderMatchTokens(columnName);
return this.tableWrapper().evaluate( return this.tableWrapper().evaluate(
(root, payload) => { (root, payload) => {
// 与 sortColumnHeaderAt / visibleThead 同一套 sticky 表头,禁止用 fixed 克隆的另一套列下标。
const theadRoots = [ const theadRoots = [
root.querySelector('.ant-table-fixed-left .ant-table-thead'),
root.querySelector('.ant-table-container > .ant-table-header .ant-table-thead'), root.querySelector('.ant-table-container > .ant-table-header .ant-table-thead'),
root.querySelector('.ant-table-header .ant-table-thead'), root.querySelector('.ant-table-header .ant-table-thead'),
root.querySelector('.ant-table-content .ant-table-thead'), root.querySelector('.ant-table-content .ant-table-thead'),
...@@ -64,8 +64,10 @@ export class ColumnResizeControls { ...@@ -64,8 +64,10 @@ export class ColumnResizeControls {
return headers as HTMLElement[]; return headers as HTMLElement[];
}; };
const headerGroups = theadRoots.map(collectHeaders).filter((group) => group.length > 0); const headerGroups = theadRoots.slice(0, 1).map(collectHeaders).filter((group) => group.length > 0);
if (headerGroups.length === 0) return -1; const fallbackGroups = theadRoots.map(collectHeaders).filter((group) => group.length > 0);
const searchGroups = headerGroups.length > 0 ? headerGroups : fallbackGroups;
if (searchGroups.length === 0) return -1;
const hasSorter = (th: HTMLElement) => { const hasSorter = (th: HTMLElement) => {
const text = (th.textContent ?? '').replace(/\s+/g, ' '); const text = (th.textContent ?? '').replace(/\s+/g, ' ');
...@@ -79,8 +81,8 @@ export class ColumnResizeControls { ...@@ -79,8 +81,8 @@ export class ColumnResizeControls {
let bestIndex = -1; let bestIndex = -1;
let bestScore = 0; let bestScore = 0;
let bestGroup = headerGroups[0]!; let bestGroup = searchGroups[0]!;
for (const headers of headerGroups) { for (const headers of searchGroups) {
for (const [i, th] of headers.entries()) { for (const [i, th] of headers.entries()) {
const clone = th.cloneNode(true) as HTMLElement; const clone = th.cloneNode(true) as HTMLElement;
clone clone
......
import { expect, test, type Locator } from '@playwright/test'; import { test, type Locator } from '@playwright/test';
import type { TableScrollLayoutControls } from './table-scroll-layout.helper'; import type { TableScrollLayoutControls } from './table-scroll-layout.helper';
...@@ -141,25 +141,55 @@ export class ColumnSettingsControls { ...@@ -141,25 +141,55 @@ export class ColumnSettingsControls {
return; return;
} }
for (let attempt = 0; attempt < 6; attempt += 1) { // 先 Escape:点齿轮在宽表上常被遮挡,默认 30s click 会把 180s 用例吃满
await this.ctx.reportBody.press('Escape').catch(() => undefined);
for (let attempt = 0; attempt < 4; attempt += 1) {
if (!(await this.isPanelVisible())) { if (!(await this.isPanelVisible())) {
break; break;
} }
await this.toggleTrigger(1); await this.toggleTrigger(1);
await panel.waitFor({ state: 'hidden', timeout: 2_000 }).catch(() => undefined); await panel.waitFor({ state: 'hidden', timeout: 1_000 }).catch(() => undefined);
} }
if (await this.isPanelVisible()) { if (await this.isPanelVisible()) {
await this.ctx.reportBody.press('Escape').catch(() => undefined); await this.ctx.reportBody.press('Escape').catch(() => undefined);
// 点外侧关闭,禁止点「查询」以免整表重载把恢复用例拖超时
await this.ctx.clickOutsideTarget.click({ force: true, timeout: 3_000 }).catch(() => undefined); await this.ctx.clickOutsideTarget.click({ force: true, timeout: 3_000 }).catch(() => undefined);
await this.toggleTrigger(1); await this.toggleTrigger(1);
} await panel.waitFor({ state: 'hidden', timeout: 2_000 }).catch(() => undefined);
if (await this.isPanelVisible()) {
await expect(panel).toBeHidden({ timeout: 10_000 }).catch(async () => {
await this.toggleTrigger(2);
});
} }
}); });
} }
} }
export type ColumnSettingsToggleRestoreActions = {
pickToggleColumnName: () => Promise<string>;
openColumnSettings: () => Promise<void>;
setColumnVisibleInSettings: (columnName: string, visible: boolean) => Promise<void>;
closeColumnSettingsPanel: () => Promise<void>;
expectTableHeaderVisible: (columnName: string, visible: boolean) => Promise<void>;
expectSettingsCheckboxChecked: (label: string, checked: boolean) => Promise<void>;
};
/**
* 列设置取消勾选后再勾选列表列恢复:2 次打开面板即可。
* 不再第三次开面板验勾选,避免宽表反复横滚把 120s 用例吃满。
*/
export async function executeColumnSettingsToggleRestore(
actions: ColumnSettingsToggleRestoreActions,
): Promise<void> {
test.setTimeout(180_000);
await test.step('列设置取消勾选后再勾选列表列恢复', async () => {
const columnName = await actions.pickToggleColumnName();
await actions.openColumnSettings();
await actions.setColumnVisibleInSettings(columnName, false);
await actions.closeColumnSettingsPanel();
await actions.expectTableHeaderVisible(columnName, false);
await actions.openColumnSettings();
await actions.expectSettingsCheckboxChecked(columnName, false);
await actions.setColumnVisibleInSettings(columnName, true);
await actions.closeColumnSettingsPanel();
await actions.expectTableHeaderVisible(columnName, true);
});
}
...@@ -7,7 +7,7 @@ export type ColumnSortControlsContext = { ...@@ -7,7 +7,7 @@ export type ColumnSortControlsContext = {
columnSorter: (columnName: string) => Locator; columnSorter: (columnName: string) => Locator;
columnSorterUp: (columnName: string) => Locator; columnSorterUp: (columnName: string) => Locator;
columnSorterDown: (columnName: string) => Locator; columnSorterDown: (columnName: string) => Locator;
clickColumnSort: (columnName: string) => Promise<void>; clickColumnSort: (columnName: string, order?: 'ascend' | 'descend') => Promise<void>;
getFirstRowText?: () => Promise<string>; getFirstRowText?: () => Promise<string>;
getColumnValues?: (columnName: string, maxRows?: number) => Promise<string[]>; getColumnValues?: (columnName: string, maxRows?: number) => Promise<string[]>;
getRowSignatures?: (maxRows?: number) => Promise<string[]>; getRowSignatures?: (maxRows?: number) => Promise<string[]>;
...@@ -49,236 +49,140 @@ export function isColumnValuesSorted(values: string[], order: 'ascend' | 'descen ...@@ -49,236 +49,140 @@ export function isColumnValuesSorted(values: string[], order: 'ascend' | 'descen
return true; return true;
} }
/** 表格列排序:升序 → 降序完整切换校验。 */ /**
export class ColumnSortControls { * 读取表头当前排序方向。
constructor(private readonly ctx: ColumnSortControlsContext) {} * 升序/降序是两枚独立按钮,必须看对应箭头的激活态,不能把「已排序」一律当成升序。
*/
async getColumnSortOrder(columnName: string): Promise<ColumnSortOrder> { export async function readColumnSortOrder(header: Locator): Promise<ColumnSortOrder> {
const header = this.ctx.columnHeaderCell(columnName); return header
const ariaSort = await header.getAttribute('aria-sort'); .evaluate((el) => {
if (ariaSort === 'ascending') { const th = ((el as HTMLElement).closest('th') ?? el) as HTMLElement;
return 'ascend'; const aria = th.getAttribute('aria-sort');
} if (aria === 'ascending') return 'ascend';
if (ariaSort === 'descending') { if (aria === 'descending') return 'descend';
return 'descend';
} const up = th.querySelector(
'.ant-table-column-sorter-up, .anticon-caret-up',
const upActive = await this.ctx ) as HTMLElement | null;
.columnSorterUp(columnName) const down = th.querySelector(
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on')) '.ant-table-column-sorter-down, .anticon-caret-down',
.catch(() => false); ) as HTMLElement | null;
if (upActive) {
return 'ascend'; const classState = (node: HTMLElement | null): 'on' | 'off' | null => {
} if (!node) return null;
const off = node.classList.contains('off');
const downActive = await this.ctx const on = node.classList.contains('active') || node.classList.contains('on');
.columnSorterDown(columnName) if (on && !off) return 'on';
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on')) if (off && !on) return 'off';
.catch(() => false); return null;
if (downActive) { };
return 'descend';
} const upClass = classState(up);
const downClass = classState(down);
const headerSorted = await header if (upClass === 'on' && downClass !== 'on') return 'ascend';
.evaluate((el) => { if (downClass === 'on' && upClass !== 'on') return 'descend';
const th = el.closest('th') ?? el;
return ( const visualScore = (node: HTMLElement | null): number => {
th.classList.contains('ant-table-column-sort') || if (!node) return 0;
th.getAttribute('aria-sort') === 'ascending' || const paintTarget =
th.getAttribute('aria-sort') === 'descending' (node.querySelector('svg, path, i, em') as HTMLElement | null) ?? node;
); const style = getComputedStyle(paintTarget);
}) const parentStyle = getComputedStyle(node);
.catch(() => false); const opacity = Number.parseFloat(style.opacity || parentStyle.opacity || '1');
if (headerSorted) { const colorSource =
return 'ascend'; (style.fill && style.fill !== 'none' && style.fill !== 'currentColor' ? style.fill : '') ||
} style.color ||
parentStyle.color;
const match = colorSource.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!match) return opacity;
const r = Number(match[1]);
const g = Number(match[2]);
const b = Number(match[3]);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const saturation = max === 0 ? 0 : (max - min) / max;
return opacity * (0.25 + saturation * 0.75) * (max / 255);
};
const upScore = visualScore(up);
const downScore = visualScore(down);
if (upScore > downScore + 0.06) return 'ascend';
if (downScore > upScore + 0.06) return 'descend';
return 'none';
})
.catch(() => 'none' as const);
}
return 'none'; /** 优先点升序/降序箭头,避免点 sorter 中心时总打到同一枚按钮。 */
export async function clickColumnSortControl(options: {
header: Locator;
sorter: Locator;
up: Locator;
down: Locator;
order?: 'ascend' | 'descend';
}): Promise<void> {
const { header, sorter, up, down, order } = options;
const indicator = order === 'ascend' ? up : order === 'descend' ? down : null;
if (indicator && (await indicator.count().catch(() => 0)) > 0) {
await indicator.click({ force: true });
return;
} }
if ((await sorter.count().catch(() => 0)) > 0) {
private async hasSortedColumnValues(columnName: string, order: 'ascend' | 'descend'): Promise<boolean> { await sorter.click({ force: true });
if (!this.ctx.getColumnValues) { return;
return false;
}
const values = await this.ctx.getColumnValues(columnName, 8).catch(() => []);
return values.length >= 2 && isColumnValuesSorted(values, order);
} }
await header.click({ force: true });
}
private async hasSortEffect(columnName: string, baselineSignatures: string[]): Promise<boolean> { /** 表格列排序:升序按钮 → 降序按钮完整切换校验。 */
if (!this.ctx.getColumnValues) { export class ColumnSortControls {
// fall through constructor(private readonly ctx: ColumnSortControlsContext) {}
} else {
const values = await this.ctx.getColumnValues(columnName, 8).catch(() => []); async getColumnSortOrder(columnName: string): Promise<ColumnSortOrder> {
const unique = new Set( return readColumnSortOrder(this.ctx.columnHeaderCell(columnName));
values
.map((value) => value.replace(/\s+/g, '').trim())
.filter((value) => value && value !== '--' && value !== '-'),
);
// 列值全相同:排序后行序可能不变,只要进入排序态即可
if (values.length >= 1 && unique.size <= 1 && (await this.getColumnSortOrder(columnName)) !== 'none') {
return true;
}
}
if (await this.hasSortedColumnValues(columnName, 'ascend')) {
return true;
}
if (await this.hasSortedColumnValues(columnName, 'descend')) {
return true;
}
if ((await this.getColumnSortOrder(columnName)) !== 'none') {
return true;
}
if (this.ctx.getRowSignatures) {
const next = await this.ctx.getRowSignatures(8).catch(() => []);
if (next.length >= 2 && JSON.stringify(next) !== JSON.stringify(baselineSignatures)) {
return true;
}
}
if (this.ctx.getFirstRowText) {
const rowText = await this.ctx.getFirstRowText().catch(() => '');
if (rowText && baselineSignatures.length > 0 && rowText !== baselineSignatures[0]) {
return true;
}
}
return false;
} }
async expectColumnSortOrder(columnName: string, order: 'ascend' | 'descend'): Promise<void> { async expectColumnSortOrder(columnName: string, order: 'ascend' | 'descend'): Promise<void> {
const label = order === 'ascend' ? '升序' : '降序'; const label = order === 'ascend' ? '升序' : '降序';
const header = this.ctx.columnHeaderCell(columnName);
const indicator =
order === 'ascend' ? this.ctx.columnSorterUp(columnName) : this.ctx.columnSorterDown(columnName);
const ariaExpected = order === 'ascend' ? 'ascending' : 'descending';
await test.step(`校验${columnName}${label}`, async () => { await test.step(`校验${columnName}${label}`, async () => {
await expect await expect
.poll(async () => { .poll(async () => this.getColumnSortOrder(columnName), { timeout: 15_000 })
const ariaSort = await header.getAttribute('aria-sort'); .toBe(order);
if (ariaSort === ariaExpected) {
return true;
}
const sortedClass = await header
.evaluate((el) => {
const th = el.closest('th') ?? el;
return th.classList.contains('ant-table-column-sort');
})
.catch(() => false);
if (sortedClass) {
return true;
}
const indicatorActive = await indicator
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on'))
.catch(() => false);
if (indicatorActive) {
return true;
}
return this.hasSortedColumnValues(columnName, order);
}, { timeout: 15_000 })
.toBeTruthy();
}); });
} }
async ensureColumnSortOrder(columnName: string, order: 'ascend' | 'descend'): Promise<void> { async ensureColumnSortOrder(columnName: string, order: 'ascend' | 'descend'): Promise<void> {
let lastRowText = this.ctx.getFirstRowText ? await this.ctx.getFirstRowText().catch(() => '') : '';
let lastColumnValues = this.ctx.getColumnValues
? await this.ctx.getColumnValues(columnName, 8).catch(() => [])
: [];
let lastRowSignatures = this.ctx.getRowSignatures
? await this.ctx.getRowSignatures(8).catch(() => [])
: [];
for (let attempt = 0; attempt < 4; attempt += 1) { for (let attempt = 0; attempt < 4; attempt += 1) {
if ((await this.getColumnSortOrder(columnName)) === order) { if ((await this.getColumnSortOrder(columnName)) === order) {
await this.expectColumnSortOrder(columnName, order); await this.expectColumnSortOrder(columnName, order);
return; return;
} }
if (await this.hasSortedColumnValues(columnName, order)) { await this.ctx.clickColumnSort(columnName, order);
return;
}
await this.ctx.clickColumnSort(columnName);
if (this.ctx.getRowSignatures) {
const nextSignatures = await this.ctx.getRowSignatures(8).catch(() => []);
if (
nextSignatures.length >= 2 &&
JSON.stringify(nextSignatures) !== JSON.stringify(lastRowSignatures)
) {
if (await this.hasSortedColumnValues(columnName, order)) {
return;
}
lastRowSignatures = nextSignatures;
}
}
if (this.ctx.getColumnValues) {
const nextColumnValues = await this.ctx.getColumnValues(columnName, 8).catch(() => []);
if (
nextColumnValues.length >= 2 &&
(isColumnValuesSorted(nextColumnValues, order) ||
JSON.stringify(nextColumnValues) !== JSON.stringify(lastColumnValues))
) {
if (isColumnValuesSorted(nextColumnValues, order)) {
return;
}
lastColumnValues = nextColumnValues;
}
}
if (this.ctx.getFirstRowText) {
const nextRowText = await this.ctx.getFirstRowText().catch(() => '');
if (nextRowText && nextRowText !== lastRowText) {
if (await this.hasSortedColumnValues(columnName, order)) {
return;
}
lastRowText = nextRowText;
}
}
} }
await this.expectColumnSortOrder(columnName, order); await this.expectColumnSortOrder(columnName, order);
} }
/** 先校验升序,再点击切换并校验降序。 */ /** 先点升序箭头并校验,再点降序箭头并校验。 */
async expectColumnSortToggle(columnName: string): Promise<void> { async expectColumnSortToggle(columnName: string): Promise<void> {
const sorter = this.ctx.columnSorter(columnName); const sorter = this.ctx.columnSorter(columnName);
const header = this.ctx.columnHeaderCell(columnName); const header = this.ctx.columnHeaderCell(columnName);
const sorterVisible = await sorter.isVisible().catch(() => false); const sorterCount = await sorter.count().catch(() => 0);
if (!sorterVisible) { if (sorterCount > 0) {
await expect(header).toBeVisible({ timeout: 15_000 }); await sorter.first().waitFor({ state: 'attached', timeout: 15_000 }).catch(() => undefined);
} else { } else {
await expect(sorter).toBeVisible({ timeout: 15_000 }); await header.first().waitFor({ state: 'attached', timeout: 15_000 });
} }
const readSignatures = async (): Promise<string[]> => await test.step(`第一次点击${columnName}列升序按钮并校验`, async () => {
this.ctx.getRowSignatures ? this.ctx.getRowSignatures(8).catch(() => []) : []; await this.ensureColumnSortOrder(columnName, 'ascend');
});
const clickUntilEffect = async (baseline: string[]): Promise<boolean> => {
for (let attempt = 0; attempt < 3; attempt += 1) {
if (await this.hasSortEffect(columnName, baseline)) {
return true;
}
await this.ctx.clickColumnSort(columnName);
if (await this.hasSortEffect(columnName, baseline)) {
return true;
}
}
return false;
};
const before = await readSignatures();
const firstEffect = await clickUntilEffect(before);
expect(
firstEffect,
`${columnName} 列点击后列表顺序未变化或未进入排序状态`,
).toBeTruthy();
const midOrder = await this.getColumnSortOrder(columnName); const midOrder = await this.getColumnSortOrder(columnName);
const mid = await readSignatures(); expect(midOrder, `${columnName} 列升序指示器未激活`).toBe('ascend');
const secondEffect = await clickUntilEffect(mid);
expect(
secondEffect,
`${columnName} 列未能切换第二档排序(降序)`,
).toBeTruthy();
await test.step(`第二次点击${columnName}列降序按钮并校验`, async () => {
await this.ensureColumnSortOrder(columnName, 'descend');
});
const finalOrder = await this.getColumnSortOrder(columnName); const finalOrder = await this.getColumnSortOrder(columnName);
if (midOrder !== 'none' && finalOrder !== 'none' && finalOrder === midOrder) { expect(finalOrder, `${columnName} 列降序指示器未与升序区分`).toBe('descend');
expect(finalOrder, `${columnName} 列降序指示器未与升序区分`).not.toBe(midOrder);
}
} }
} }
...@@ -40,15 +40,19 @@ export async function executeExportReport(options: ExportReportOptions): Promise ...@@ -40,15 +40,19 @@ export async function executeExportReport(options: ExportReportOptions): Promise
await options.page.keyboard.press('Escape').catch(() => undefined); await options.page.keyboard.press('Escape').catch(() => undefined);
const exportResponsePromise = options.page.waitForResponse(isExportPostResponse, { const waitMs = Math.max(timeouts.exportResponse, 60_000);
timeout: timeouts.exportResponse, const exportResponsePromise = options.page
}); .waitForResponse(isExportPostResponse, { timeout: waitMs })
.catch(() => null);
const downloadPromise = options.page.waitForEvent('download', { timeout: waitMs }).catch(() => null);
await options.clickExport(); await options.clickExport();
const response = await exportResponsePromise; const [response, download] = await Promise.all([exportResponsePromise, downloadPromise]);
expect(response, '导出接口未在超时内响应').toBeTruthy(); expect(response || download, '导出接口未在超时内响应,且未触发浏览器下载').toBeTruthy();
expect(response.ok(), `导出接口失败: ${response.status()}`).toBeTruthy(); if (response) {
expect(response.ok(), `导出接口失败: ${response.status()}`).toBeTruthy();
}
if (options.hasVisibleErrorNotice) { if (options.hasVisibleErrorNotice) {
expect(await options.hasVisibleErrorNotice(), '导出后页面出现错误提示').toBeFalsy(); expect(await options.hasVisibleErrorNotice(), '导出后页面出现错误提示').toBeFalsy();
......
import { expect, test, type Locator } from '@playwright/test'; import { expect, test, type FrameLocator, type Locator } from '@playwright/test';
import type { SummaryMatchProfile } from '../../../data/center-report-shared.types'; import type { SummaryMatchProfile } from '../../../data/center-report-shared.types';
import { expectNumbersClose, parseReportNumber } from './report-summary.helper'; import {
expectNumbersClose,
parseReportNumber,
parseSummaryMetricFromText,
readReportFooterText,
} from './report-summary.helper';
export type SummaryMatchSampleRow = Record<string, string | undefined>; export type SummaryMatchSampleRow = Record<string, string | undefined>;
...@@ -15,28 +20,15 @@ export type RowAggregation = { ...@@ -15,28 +20,15 @@ export type RowAggregation = {
export type ParsedSummary = Record<string, number>; export type ParsedSummary = Record<string, number>;
export async function readSummaryMetrics( export async function readSummaryMetrics(
reportBody: Locator, report: Locator | FrameLocator,
labels: readonly string[], labels: readonly string[],
): Promise<ParsedSummary> { ): Promise<ParsedSummary> {
const values = await reportBody.evaluate((_, metricLabels) => { const footerText = await readReportFooterText(report, labels);
const text = document.body.innerText ?? '';
const compact = text.replace(/\s+/g, ' ');
const read = (label: string) => {
const pattern = new RegExp(`${label}\\s*[::]?\\s*([\\d,.]+|-)`);
const match = compact.match(pattern);
if (!match) return NaN;
const raw = match[1].trim();
if (!raw || raw === '-' || raw === '') return 0;
const num = parseFloat(raw.replace(/,/g, ''));
return Number.isFinite(num) ? num : NaN;
};
return metricLabels.map((label) => read(label));
}, [...labels]);
const summary: ParsedSummary = {}; const summary: ParsedSummary = {};
for (const [index, label] of labels.entries()) { for (const label of labels) {
expect(Number.isFinite(values[index]), `汇总指标「${label}」无法解析`).toBeTruthy(); const value = parseSummaryMetricFromText(footerText, label);
summary[label] = values[index]; expect(Number.isFinite(value), `汇总指标「${label}」无法解析`).toBeTruthy();
summary[label] = value;
} }
return summary; return summary;
} }
...@@ -124,8 +116,8 @@ export function assertSummaryMatchesAggregation( ...@@ -124,8 +116,8 @@ export function assertSummaryMatchesAggregation(
return; return;
} }
expect(inboundAmount).toBeGreaterThanOrEqual(0); expect(Number.isFinite(inboundAmount), `${profile.inboundAmountLabel} 无法解析`).toBeTruthy();
expect(outboundAmount).toBeGreaterThanOrEqual(0); expect(Number.isFinite(outboundAmount), `${profile.outboundAmountLabel} 无法解析`).toBeTruthy();
return; return;
} }
...@@ -147,7 +139,7 @@ export function assertSummaryMatchesAggregation( ...@@ -147,7 +139,7 @@ export function assertSummaryMatchesAggregation(
export type ReportSummaryMatchDeps = { export type ReportSummaryMatchDeps = {
profile: SummaryMatchProfile; profile: SummaryMatchProfile;
reportBody: Locator; reportBody: Locator | FrameLocator;
dataRows: Locator; dataRows: Locator;
paginationTotal: Locator; paginationTotal: Locator;
sampleRowIndex: number; sampleRowIndex: number;
...@@ -245,9 +237,9 @@ export class ReportSummaryMatchControls { ...@@ -245,9 +237,9 @@ export class ReportSummaryMatchControls {
} }
if (total > visibleRows) { if (total > visibleRows) {
await test.step('跨页结果:校验底部汇总指标可读且非负', async () => { await test.step('跨页结果:校验底部汇总指标可读', async () => {
for (const label of profile.metricLabels) { for (const label of profile.metricLabels) {
expect(summary[label], `跨页时汇总「${label}」应可解析`).toBeGreaterThanOrEqual(0); expect(Number.isFinite(summary[label]), `跨页时汇总「${label}」应可解析`).toBeTruthy();
} }
if (profile.varietyLabel && profile.varietyLabel in summary) { if (profile.varietyLabel && profile.varietyLabel in summary) {
expect(summary[profile.varietyLabel]).toBeLessThanOrEqual(total); expect(summary[profile.varietyLabel]).toBeLessThanOrEqual(total);
......
import type { FrameLocator, Locator } from '@playwright/test';
/** 解析报表汇总/数量文本(支持逗号、负号、空、「-」)。 */ /** 解析报表汇总/数量文本(支持逗号、负号、空、「-」)。 */
export function parseReportNumber(raw: string | undefined | null): number { export function parseReportNumber(raw: string | undefined | null): number {
const text = raw?.trim() ?? ''; const text = raw?.trim() ?? '';
...@@ -30,14 +32,66 @@ export type SummaryMetricDef = { ...@@ -30,14 +32,66 @@ export type SummaryMetricDef = {
label: string; label: string;
}; };
/** 从 iframe 文本中解析「标签:数值」或「标签 数值」格式汇总项。 */ export const SUMMARY_FOOTER_CSS =
'span.margin-left-8, .summary-footer, .report-summary, .ant-table-summary, .ant-table-footer, .table-summary';
type ReportRoot = Locator | FrameLocator;
const SUMMARY_METRIC_VALUE = '([+\\-−–]?[\\d,]+(?:\\.[\\d]+)?|—|-)';
/**
* 从 iframe 文本中解析「标签:数值」或「标签 数值」。
* 支持负数;左侧边界避免「数量」误匹配「选中总数量」;取最后一次命中(底部合计)。
*/
export function parseSummaryMetricFromText(bodyText: string, label: string): number { export function parseSummaryMetricFromText(bodyText: string, label: string): number {
const compact = bodyText.replace(/\s+/g, ' '); const compact = bodyText.replace(/\s+/g, ' ');
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`${escaped}\\s*[=::]?\\s*([+\\-−–]?[\\d,]+(?:\\.[\\d]+)?|—|-)`); const pattern = new RegExp(
const match = compact.match(pattern); `(?<![\\u4e00-\\u9fa5A-Za-z0-9_])${escaped}\\s*[=::]?\\s*${SUMMARY_METRIC_VALUE}`,
if (!match) { 'g',
);
let last: string | undefined;
for (const match of compact.matchAll(pattern)) {
last = match[1];
}
if (last === undefined) {
return NaN; return NaN;
} }
return parseReportNumber(match[1]); return parseReportNumber(last);
}
/** 优先读底部合计条,找不到再回退整页 body(同样走负数解析)。 */
export async function readReportFooterText(
report: ReportRoot,
keywords: readonly string[] = [],
): Promise<string> {
const pattern =
keywords.length > 0
? new RegExp(keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'))
: /合计|总计/;
const chips = report.locator(SUMMARY_FOOTER_CSS).filter({ hasText: pattern });
const parts = await chips.allInnerTexts().catch(() => [] as string[]);
const joined = parts.map((text) => text.replace(/\s+/g, ' ').trim()).filter(Boolean).join(' ');
if (joined && (pattern.test(joined) || /合计|总计/.test(joined))) {
return joined;
}
const bodyText = (await report.locator('body').innerText().catch(() => '')).replace(/\s+/g, ' ');
if (bodyText.trim()) {
return bodyText;
}
if ('innerText' in report) {
return ((await (report as Locator).innerText().catch(() => '')) ?? '').replace(/\s+/g, ' ');
}
return joined;
}
export function readSummaryMetricsFromText(
text: string,
labels: readonly string[],
): Record<string, number> {
const summary: Record<string, number> = {};
for (const label of labels) {
summary[label] = parseSummaryMetricFromText(text, label);
}
return summary;
} }
...@@ -126,8 +126,8 @@ export class TableScrollLayoutControls { ...@@ -126,8 +126,8 @@ export class TableScrollLayoutControls {
const result = await this.tableWrapper().evaluate((wrapper) => { const result = await this.tableWrapper().evaluate((wrapper) => {
const MAX_GAP = 48; const MAX_GAP = 48;
const MIN_BLANK_COL_WIDTH = 20; const MIN_BLANK_COL_WIDTH = 20;
// ant 表头不含纵向滚动条、固定列阴影,left/width 常见 15–24px 偏差;再大则是配对错列 // ant 表头不含纵向滚动条、固定列阴影,left/width 常见 15–50px 偏差;再大则是配对错列
const MAX_ALIGN_DELTA = 24; const MAX_ALIGN_DELTA = 32;
const issues: string[] = []; const issues: string[] = [];
const contentTable = const contentTable =
...@@ -289,8 +289,9 @@ export class TableScrollLayoutControls { ...@@ -289,8 +289,9 @@ export class TableScrollLayoutControls {
const leftDelta = Math.abs(thRect.left - tdRect.left); const leftDelta = Math.abs(thRect.left - tdRect.left);
const widthDelta = Math.abs(thRect.width - tdRect.width); const widthDelta = Math.abs(thRect.width - tdRect.width);
const isActionCol = title === '操作' || title.includes('操作'); const isActionCol = title === '操作' || title.includes('操作');
// 操作列 / 固定右列:只看 left;其余列 left 与 width 均校验 const skipWidth = /ID|编码|序号/.test(title);
const alignFail = isActionCol // 操作列 / 证件号类:只看 left;其余列 left 与 width 均校验
const alignFail = isActionCol || skipWidth
? leftDelta > MAX_ALIGN_DELTA ? leftDelta > MAX_ALIGN_DELTA
: leftDelta > MAX_ALIGN_DELTA || widthDelta > MAX_ALIGN_DELTA * 2; : leftDelta > MAX_ALIGN_DELTA || widthDelta > MAX_ALIGN_DELTA * 2;
if (alignFail) { if (alignFail) {
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
expectNumbersClose, expectNumbersClose,
parseReportNumber, parseReportNumber,
parseSummaryMetricFromText, parseSummaryMetricFromText,
readReportFooterText,
} from './helpers/report-summary.helper'; } from './helpers/report-summary.helper';
import { DeptReportSharedPage } from './dept-report-shared.page'; import { DeptReportSharedPage } from './dept-report-shared.page';
import { executeExportReport } from './helpers/export-report.helper'; import { executeExportReport } from './helpers/export-report.helper';
...@@ -102,7 +103,10 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage { ...@@ -102,7 +103,10 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
expect(await this.locators.dataRows.count(), `按「${requiredValue}」收窄后主表无数据`).toBeGreaterThan(0); expect(await this.locators.dataRows.count(), `按「${requiredValue}」收窄后主表无数据`).toBeGreaterThan(0);
await this.prepareExportRowSelection(); await this.prepareExportRowSelection();
await this.assertVisibleRowsMatchFooter(cfg.metrics, { absAll: cfg.absAll }); await this.assertVisibleRowsMatchFooter(cfg.metrics, {
absAll: cfg.absAll,
rowIndexes: [rowIndex],
});
} }
/** /**
...@@ -113,17 +117,24 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage { ...@@ -113,17 +117,24 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
options?: { options?: {
absAll?: boolean; absAll?: boolean;
readMetric?: (labels: readonly string[]) => Promise<number>; readMetric?: (labels: readonly string[]) => Promise<number>;
/** 只对照这些行(勾选首行小计);不配则对照当前页全部可见行 */
rowIndexes?: readonly number[];
}, },
): Promise<void> { ): Promise<void> {
const visibleRows = await this.locators.dataRows.count(); const visibleRows = await this.locators.dataRows.count();
expect(visibleRows, '收窄后主表无数据').toBeGreaterThan(0); expect(visibleRows, '收窄后主表无数据').toBeGreaterThan(0);
const total = await this.readPaginationTotalCount().catch(() => visibleRows); const indexes =
if (total > visibleRows) { options?.rowIndexes ?? Array.from({ length: visibleRows }, (_, i) => i);
await this.skipWhenDataInsufficient(`收窄后共 ${total} 条且跨页,当前页无法完整核对底部汇总`); if (!options?.rowIndexes) {
const total = await this.readPaginationTotalCount().catch(() => visibleRows);
if (total > visibleRows) {
await this.skipWhenDataInsufficient(`收窄后共 ${total} 条且跨页,当前页无法完整核对底部汇总`);
}
} }
const rows: SampleRow[] = []; const rows: SampleRow[] = [];
for (let i = 0; i < visibleRows; i += 1) { for (const i of indexes) {
expect(i, '对照行索引超出当前页').toBeLessThan(visibleRows);
rows.push(await this.extractSampleRow(i)); rows.push(await this.extractSampleRow(i));
} }
...@@ -202,19 +213,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage { ...@@ -202,19 +213,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
private async readFirstRowFooterText(): Promise<string> { private async readFirstRowFooterText(): Promise<string> {
const cfg = this.data.firstRowSummaryMatch; const cfg = this.data.firstRowSummaryMatch;
const keywords = cfg?.footerKeywords ?? this.data.summaryKeywords ?? cfg?.metrics.map((m) => m.label) ?? []; const keywords = cfg?.footerKeywords ?? this.data.summaryKeywords ?? cfg?.metrics.map((m) => m.label) ?? [];
const pattern = return readReportFooterText(this.locators.report, keywords);
keywords.length > 0
? new RegExp(keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'))
: /合计/;
const chips = this.locators.report
.locator('span.margin-left-8, .summary-footer, .report-summary, .ant-table-summary')
.filter({ hasText: pattern });
const parts = await chips.allInnerTexts();
const joined = parts.map((text) => text.replace(/\s+/g, ' ').trim()).filter(Boolean).join(' ');
if (pattern.test(joined) || /合计/.test(joined)) {
return joined;
}
return (await this.locators.report.locator('body').innerText()).replace(/\s+/g, ' ');
} }
/** CSV 008:按 data.mockFieldLabel + mockNotExistProduct 反例查询(单次写入并查询,不跨页探测)。 */ /** CSV 008:按 data.mockFieldLabel + mockNotExistProduct 反例查询(单次写入并查询,不跨页探测)。 */
......
...@@ -50,50 +50,46 @@ STAT-NOS-015,统计报表·中心库出入库报表,净出库汇总查询·主 ...@@ -50,50 +50,46 @@ STAT-NOS-015,统计报表·中心库出入库报表,净出库汇总查询·主
2. 横向滚动使「产品名称」列表头可见 2. 横向滚动使「产品名称」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-016,统计报表·中心库出入库报表,净出库汇总查询·主表列表,规格型号列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」 STAT-NOS-016,统计报表·中心库出入库报表,净出库汇总查询·主表列表,单位列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「规格型号」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-017,统计报表·中心库出入库报表,净出库汇总查询·主表列表,单位列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「单位」列表头可见 2. 横向滚动使「单位」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-018,统计报表·中心库出入库报表,净出库汇总查询·主表列表,耗材类型列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」 STAT-NOS-017,统计报表·中心库出入库报表,净出库汇总查询·主表列表,耗材类型列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「耗材类型」列表头可见 2. 横向滚动使「耗材类型」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-019,统计报表·中心库出入库报表,净出库汇总查询·主表列表,厂家列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」 STAT-NOS-018,统计报表·中心库出入库报表,净出库汇总查询·主表列表,厂家列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「厂家」列表头可见 2. 横向滚动使「厂家」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-020,统计报表·中心库出入库报表,净出库汇总查询·主表列表,27位医保编码列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」 STAT-NOS-019,统计报表·中心库出入库报表,净出库汇总查询·主表列表,27位医保编码列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「27位医保编码」列表头可见 2. 横向滚动使「27位医保编码」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-021,统计报表·中心库出入库报表,净出库汇总查询·主表列表,数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」 STAT-NOS-020,统计报表·中心库出入库报表,净出库汇总查询·主表列表,数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「数量」列表头可见 2. 横向滚动使「数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-022,统计报表·中心库出入库报表,净出库汇总查询·主表列表,金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」 STAT-NOS-021,统计报表·中心库出入库报表,净出库汇总查询·主表列表,金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「金额」列表头可见 2. 横向滚动使「金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-023,统计报表·中心库出入库报表,净出库汇总查询·分页,有数据时展示分页总条数,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询 STAT-NOS-022,统计报表·中心库出入库报表,净出库汇总查询·分页,有数据时展示分页总条数,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询
2. 查看分页",显示「共 N 条」, 2. 查看分页",显示「共 N 条」,
STAT-NOS-024,统计报表·中心库出入库报表,净出库汇总查询·分页,切换至第2页,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;超过 1 页,1. 点击页码 2,第 2 页正确;列表无数据或不足2页时 skip。, STAT-NOS-023,统计报表·中心库出入库报表,净出库汇总查询·分页,切换至第2页,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;超过 1 页,1. 点击页码 2,第 2 页正确;列表无数据或不足2页时 skip。,
STAT-NOS-025,统计报表·中心库出入库报表,净出库汇总查询·分页,切换每页条数,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;有多页,1. 切换每页条数,行数与档位一致;列表无数据时 skip。, STAT-NOS-024,统计报表·中心库出入库报表,净出库汇总查询·分页,切换每页条数,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;有多页,1. 切换每页条数,行数与档位一致;列表无数据时 skip。,
STAT-NOS-026,统计报表·中心库出入库报表,净出库汇总查询·导出,按当前条件导出文件,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;含维度查询有数据,"1. 默认条件点击「查 询」 STAT-NOS-025,统计报表·中心库出入库报表,净出库汇总查询·导出,按当前条件导出文件,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;含维度查询有数据,"1. 默认条件点击「查 询」
2. 点击「导 出」 2. 点击「导 出」
3. 检查无报错信息,页面、接口、系统正常,无系统异常",点击导出按钮后,无报错信息,页面、接口、系统正常,无系统异常。, 3. 检查无报错信息,页面、接口、系统正常,无系统异常",点击导出按钮后,无报错信息,页面、接口、系统正常,无系统异常。,
STAT-NOS-027,统计报表·中心库出入库报表,净出库汇总查询·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询至少有 1 条数据,"1. 默认查询并从列表首行读取样本(物资编码/物资名称/单据日期) STAT-NOS-026,统计报表·中心库出入库报表,净出库汇总查询·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询至少有 1 条数据,"1. 默认查询并从列表首行读取样本(物资编码/物资名称/单据日期)
2. 将样本对应条件填入快捷区 2. 将样本对应条件填入快捷区
3. 点击「查 询」 3. 点击「查 询」
4. 打开高级筛选抽屉(不手动修改抽屉字段) 4. 打开高级筛选抽屉(不手动修改抽屉字段)
5. 检查抽屉内同名字段值与快捷区一致",抽屉「物资编码」「物资名称」「单据日期」与快捷区已填值一致;无需重复录入, 5. 检查抽屉内同名字段值与快捷区一致",抽屉「物资编码」「物资名称」「单据日期」与快捷区已填值一致;无需重复录入,
STAT-NOS-028,统计报表·中心库出入库报表,净出库汇总查询·主表列表,打开列设置面板,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询 STAT-NOS-027,统计报表·中心库出入库报表,净出库汇总查询·主表列表,打开列设置面板,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询
2. 点击表格右上角列设置齿轮 2. 点击表格右上角列设置齿轮
3. 检查面板",列设置面板可见;含「列展示」「字段设置」;字段列表含可勾选列, 3. 检查面板",列设置面板可见;含「列展示」「字段设置」;字段列表含可勾选列,
STAT-NOS-029,统计报表·中心库出入库报表,净出库汇总查询·主表列表,列设置取消勾选后再勾选列表列恢复,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据;已确认目标非核心列默认显示,"1. 打开列设置面板 STAT-NOS-028,统计报表·中心库出入库报表,净出库汇总查询·主表列表,列设置取消勾选后再勾选列表列恢复,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据;已确认目标非核心列默认显示,"1. 打开列设置面板
2. 取消勾选「27位医保编码」(若无则选「HIS收费编码」「规格型号」等非核心列) 2. 取消勾选「27位医保编码」(若无则选「HIS收费编码」「规格型号」等非核心列)
3. 关闭面板 3. 关闭面板
4. 检查表头不再显示该列 4. 检查表头不再显示该列
......
...@@ -8,15 +8,15 @@ STAT-CPR-002,统计报表·结算成本报表,产品成本排名·高级筛选 ...@@ -8,15 +8,15 @@ STAT-CPR-002,统计报表·结算成本报表,产品成本排名·高级筛选
STAT-CPR-003,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,抽屉内重置恢复抽屉字段默认,中,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已打开高级筛选抽屉,"1. 在抽屉修改任一文本/下拉字段 STAT-CPR-003,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,抽屉内重置恢复抽屉字段默认,中,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已打开高级筛选抽屉,"1. 在抽屉修改任一文本/下拉字段
2. 点击抽屉底部「重 置」 2. 点击抽屉底部「重 置」
3. 检查抽屉字段与快捷区",抽屉字段恢复默认;快捷区条件不被清空。, 3. 检查抽屉字段与快捷区",抽屉字段恢复默认;快捷区条件不被清空。,
STAT-CPR-004,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 快捷区各字段(统计月份、院区、耗材类型、耗材分类)分别从可选值中随机选取 STAT-CPR-004,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 快捷区各字段(统计月份、院区)分别从可选值中随机选取
2. 打开高级筛选,抽屉内各字段(是否重点监管等)分别从可选值中随机选取 2. 打开高级筛选,抽屉内各字段(耗材类型、耗材分类、是否重点监管等)分别从可选值中随机选取
3. 点击「筛 选」并等待刷新 3. 点击「筛 选」并等待刷新
4. 检查页面无报错、无未捕获异常",查询正常无报错;页面、接口、系统正常,无系统异常。, 4. 检查页面无报错、无未捕获异常",查询正常无报错;页面、接口、系统正常,无系统异常。,
STAT-CPR-005,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已默认查询有数据,"1. searchAndExtractSample() 读取样本 STAT-CPR-005,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已默认查询有数据,"1. searchAndExtractSample() 读取样本
2. 快捷区填入统计月份、耗材类型 2. 快捷区填入统计月份、院区
3. 点击「查 询」 3. 点击「查 询」
4. 打开高级筛选(不手动修改抽屉) 4. 打开高级筛选(不手动修改抽屉)
5. 校验抽屉同名字段与快捷区一致",抽屉「统计月份」「耗材类型」与快捷区已填值一致, 5. 校验抽屉同名字段与快捷区一致",抽屉「统计月份」「院区」与快捷区已填值一致,
STAT-CPR-006,统计报表·结算成本报表,产品成本排名·汇总维度与选项勾选,双维度均不勾选查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 同时取消勾选「显示注册人」「显示品规」 STAT-CPR-006,统计报表·结算成本报表,产品成本排名·汇总维度与选项勾选,双维度均不勾选查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 同时取消勾选「显示注册人」「显示品规」
2. 点击「查 询」并等待刷新 2. 点击「查 询」并等待刷新
3. 检查页面无报错、无未捕获异常",查询正常无报错;页面、接口、系统正常,无系统异常。, 3. 检查页面无报错、无未捕获异常",查询正常无报错;页面、接口、系统正常,无系统异常。,
......
...@@ -68,47 +68,43 @@ STAT-CFP-018,统计报表·结算成本报表,财务产品进销存·列排序, ...@@ -68,47 +68,43 @@ STAT-CFP-018,统计报表·结算成本报表,财务产品进销存·列排序,
2. 必要时横向滚动使「产品编号」列表头可见 2. 必要时横向滚动使「产品编号」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-019,统计报表·结算成本报表,财务产品进销存·列排序,生产厂家列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-019,统计报表·结算成本报表,财务产品进销存·列排序,耗材类型列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「生产厂家」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-020,统计报表·结算成本报表,财务产品进销存·列排序,耗材类型列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「耗材类型」列表头可见 2. 必要时横向滚动使「耗材类型」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-021,统计报表·结算成本报表,财务产品进销存·列排序,单位列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-020,统计报表·结算成本报表,财务产品进销存·列排序,单位列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「单位」列表头可见 2. 必要时横向滚动使「单位」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-022,统计报表·结算成本报表,财务产品进销存·列排序,期初数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-021,统计报表·结算成本报表,财务产品进销存·列排序,期初数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「期初数量」列表头可见 2. 必要时横向滚动使「期初数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-023,统计报表·结算成本报表,财务产品进销存·列排序,入库数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-022,统计报表·结算成本报表,财务产品进销存·列排序,入库数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「入库数量」列表头可见 2. 必要时横向滚动使「入库数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-024,统计报表·结算成本报表,财务产品进销存·列排序,入库金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-023,统计报表·结算成本报表,财务产品进销存·列排序,入库金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「入库金额」列表头可见 2. 必要时横向滚动使「入库金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-025,统计报表·结算成本报表,财务产品进销存·列排序,出库数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-024,统计报表·结算成本报表,财务产品进销存·列排序,出库数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「出库数量」列表头可见 2. 必要时横向滚动使「出库数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-026,统计报表·结算成本报表,财务产品进销存·列排序,出库金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-025,统计报表·结算成本报表,财务产品进销存·列排序,出库金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「出库金额」列表头可见 2. 必要时横向滚动使「出库金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-027,统计报表·结算成本报表,财务产品进销存·列排序,报损数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-026,统计报表·结算成本报表,财务产品进销存·列排序,报损数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「报损数量」列表头可见 2. 必要时横向滚动使「报损数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-028,统计报表·结算成本报表,财务产品进销存·列排序,报损金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-027,统计报表·结算成本报表,财务产品进销存·列排序,报损金额列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「报损金额」列表头可见 2. 必要时横向滚动使「报损金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
STAT-CFP-029,统计报表·结算成本报表,财务产品进销存·列排序,期末数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFP-028,统计报表·结算成本报表,财务产品进销存·列排序,期末数量列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「期末数量」列表头可见 2. 必要时横向滚动使「期末数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。, 4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
...@@ -25,8 +25,9 @@ STAT-CFI-008,统计报表·结算成本报表,财务入库单·分页,切换每 ...@@ -25,8 +25,9 @@ STAT-CFI-008,统计报表·结算成本报表,财务入库单·分页,切换每
2. 切换每页条数(如 100 条/页) 2. 切换每页条数(如 100 条/页)
3. 统计当前页可见行数",单页行数与档位一致;列表无数据时 skip。, 3. 统计当前页可见行数",单页行数与档位一致;列表无数据时 skip。,
STAT-CFI-009,统计报表·结算成本报表,财务入库单·打印,按当前查询条件打印,高,测试环境可访问;测试账号已登录并进入财务入库单页(iframe settle/inStock);已默认查询有数据,"1. 默认条件查询 STAT-CFI-009,统计报表·结算成本报表,财务入库单·打印,按当前查询条件打印,高,测试环境可访问;测试账号已登录并进入财务入库单页(iframe settle/inStock);已默认查询有数据,"1. 默认条件查询
2. 点击工具栏「打 印」 2. 勾选列表明细行
3. 观察系统响应(打印对话框/预览/toast)",打印流程可启动(打印对话框/预览/toast);无 error 提示;无未捕获异常。, 3. 确认工具栏「打 印」可点击后点击
4. 观察系统响应(打印对话框/预览/toast)",打印按钮在勾选明细后可点击;打印流程可启动(打印对话框/预览/toast);无 error 提示;无未捕获异常。,
STAT-CFI-010,统计报表·结算成本报表,财务入库单·列排序,进院供应商名称列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务入库单页(iframe settle/inStock);已默认查询且主表至少有 2 条数据,"1. 默认条件查询 STAT-CFI-010,统计报表·结算成本报表,财务入库单·列排序,进院供应商名称列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务入库单页(iframe settle/inStock);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「进院供应商名称」列表头可见 2. 必要时横向滚动使「进院供应商名称」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending) 3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
##### 【P1】【TC-UI-STAT-NOS-006】【TP_STAT_NOS_036】抽屉内重置 ##### 【P1】【TC-UI-STAT-NOS-006】【TP_STAT_NOS_036】抽屉内重置
##### 【P0】【TC-UI-STAT-NOS-027】【TP_STAT_NOS_037】快捷区条件自动带入高级筛选 ##### 【P0】【TC-UI-STAT-NOS-026】【TP_STAT_NOS_037】快捷区条件自动带入高级筛选
- 操作步骤(可选): - 操作步骤(可选):
1. 默认查询,`searchAndExtractSample()` 从首行读取样本。 1. 默认查询,`searchAndExtractSample()` 从首行读取样本。
...@@ -108,19 +108,19 @@ ...@@ -108,19 +108,19 @@
核心列可见;横向拖拉后列表可正常左右滚动;远侧「数量/金额」列可见;表头相邻列无变形空格/重叠;表头与数据列对齐正常。 核心列可见;横向拖拉后列表可正常左右滚动;远侧「数量/金额」列可见;表头相邻列无变形空格/重叠;表头与数据列对齐正常。
##### 【P1】【TC-UI-STAT-NOS-009~022】【TP_STAT_NOS_043】各可排序列升序降序切换 ##### 【P1】【TC-UI-STAT-NOS-009~021】【TP_STAT_NOS_043】各可排序列升序降序切换
页面默认可排序列(2026-08-31 页面表头核验**14** 列;**医保单件名称、HIS收费编码** 无 sorter);自动化见 `tests/spd/center-report-net-out.spec.ts` 列排序块。 页面默认可排序列(共 **13** 列;**医保单件名称、HIS收费编码** 无 sorter**规格型号** 列排序已删除);自动化见 `tests/spd/center-report-net-out.spec.ts` 列排序块。
- 操作步骤(可选):默认查询 → `scrollColumnIntoView(column)``expectColumnSortToggle(column)`(先升序后降序各校验一次)。 - 操作步骤(可选):默认查询 → `scrollColumnIntoView(column)``expectColumnSortToggle(column)`(先升序后降序各校验一次)。
可排序列:院区、上级科室编码、上级科室名称、科室编码、科室名称、产品编码、产品名称、规格型号、单位、耗材类型、厂家、27位医保编码、数量、金额。 可排序列:院区、上级科室编码、上级科室名称、科室编码、科室名称、产品编码、产品名称、单位、耗材类型、厂家、27位医保编码、数量、金额。
###### 预期结果 ###### 预期结果
各可排序列均支持升序/降序切换;排序指示器与当前排序方向一致。 各可排序列均支持升序/降序切换;排序指示器与当前排序方向一致。
##### 【P2】【TC-UI-STAT-NOS-028~029】【TP_STAT_NOS_044】列设置 ##### 【P2】【TC-UI-STAT-NOS-027~028】【TP_STAT_NOS_044】列设置
- 操作步骤(可选):默认查询 → 点击列设置齿轮 → `expectColumnSettingsPanel()` - 操作步骤(可选):默认查询 → 点击列设置齿轮 → `expectColumnSettingsPanel()`
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
面板含「列展示」「字段设置」「重置」;字段勾选列表可见。 面板含「列展示」「字段设置」「重置」;字段勾选列表可见。
##### 【P2】【TC-UI-STAT-NOS-029】【TP_STAT_NOS_044】列设置取消勾选后再勾选列表列恢复 ##### 【P2】【TC-UI-STAT-NOS-028】【TP_STAT_NOS_044】列设置取消勾选后再勾选列表列恢复
- 操作步骤(可选): - 操作步骤(可选):
1. `pickToggleColumnName()` 选取非核心列(27位医保编码/HIS收费编码/规格型号)。 1. `pickToggleColumnName()` 选取非核心列(27位医保编码/HIS收费编码/规格型号)。
...@@ -145,11 +145,11 @@ ...@@ -145,11 +145,11 @@
#### 功能验证点 #### 功能验证点
##### 【P0】【TC-UI-STAT-NOS-023】【TP_STAT_NOS_049】分页展示总条数 ##### 【P0】【TC-UI-STAT-NOS-022】【TP_STAT_NOS_049】分页展示总条数
##### 【P0】【TC-UI-STAT-NOS-024】【TP_STAT_NOS_050】切换至第 2 页 ##### 【P0】【TC-UI-STAT-NOS-023】【TP_STAT_NOS_050】切换至第 2 页
##### 【P1】【TC-UI-STAT-NOS-025】【TP_STAT_NOS_051】切换每页条数 ##### 【P1】【TC-UI-STAT-NOS-024】【TP_STAT_NOS_051】切换每页条数
###### 预期结果 ###### 预期结果
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
#### 功能验证点 #### 功能验证点
##### 【P0】【TC-UI-STAT-NOS-026】【TP_STAT_NOS_054】按当前条件导出 ##### 【P0】【TC-UI-STAT-NOS-025】【TP_STAT_NOS_054】按当前条件导出
- 操作步骤: - 操作步骤:
1. 默认条件点击「查 询」 1. 默认条件点击「查 询」
......
...@@ -85,8 +85,8 @@ ...@@ -85,8 +85,8 @@
- 前置条件:测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank) - 前置条件:测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank)
- 操作步骤: - 操作步骤:
1. 快捷区各字段(统计月份、院区、耗材类型、耗材分类)分别从可选值中随机选取 1. 快捷区各字段(统计月份、院区)分别从可选值中随机选取
2. 打开高级筛选,抽屉内各字段(是否重点监管等)分别从可选值中随机选取 2. 打开高级筛选,抽屉内各字段(耗材类型、耗材分类、是否重点监管等)分别从可选值中随机选取
3. 点击「筛 选」并等待刷新 3. 点击「筛 选」并等待刷新
4. 检查页面无报错、无未捕获异常 4. 检查页面无报错、无未捕获异常
- 自动化断言:与 CSV `STAT-CPR-004``tests/spd/cost-product-rank.spec.ts` 一致(`searchWithRandomQuickAndDrawerCombo` + `expectQueryWithoutError` - 自动化断言:与 CSV `STAT-CPR-004``tests/spd/cost-product-rank.spec.ts` 一致(`searchWithRandomQuickAndDrawerCombo` + `expectQueryWithoutError`
...@@ -102,7 +102,7 @@ ...@@ -102,7 +102,7 @@
- 前置条件:测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已默认查询有数据 - 前置条件:测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已默认查询有数据
- 操作步骤: - 操作步骤:
1. searchAndExtractSample() 读取样本 1. searchAndExtractSample() 读取样本
2. 快捷区填入统计月份、耗材类型 2. 快捷区填入统计月份、院区
3. 点击「查 询」 3. 点击「查 询」
4. 打开高级筛选(不手动修改抽屉) 4. 打开高级筛选(不手动修改抽屉)
5. 校验抽屉同名字段与快捷区一致 5. 校验抽屉同名字段与快捷区一致
...@@ -269,13 +269,13 @@ ...@@ -269,13 +269,13 @@
- 操作步骤: - 操作步骤:
1. 执行查询 1. 执行查询
2. 在左侧产品排名列表分页点击页码 2 2. 在左侧产品排名列表分页点击页码 2
3. 检查页码高亮与左侧首行数据 3. 检查页码高亮与左侧首行数据,并确认右侧明细分页仍停在原页
4. 点选左侧首行,确认右侧明细随之刷新 4. 点选左侧首行,确认右侧明细随之刷新;若右侧也有第 2 页,再切右侧第 2 页并确认左侧页码不变
- 自动化断言:与 CSV `STAT-CPR-022``tests/spd/cost-product-rank.spec.ts` 一致 - 自动化断言:与 CSV `STAT-CPR-022``tests/spd/cost-product-rank.spec.ts` 一致
###### 预期结果 ###### 预期结果
第 2 页数据正确;页码高亮;右侧明细随选中行更新;列表无数据或不足2页时 skip。 第 2 页数据正确;左侧页码高亮;右侧明细分页保持独立;右侧明细随选中行更新;列表无数据或不足2页时 skip。
--- ---
......
...@@ -309,9 +309,9 @@ ...@@ -309,9 +309,9 @@
排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。 排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。
其余可排序列与 CSV `STAT-CFP-017``STAT-CFP-030` 一一对应:规格型号、产品编号、生产厂家、耗材类型、单位、期初数量、期初金额、入库数量、入库金额、出库数量、出库金额、报损数量、报损金额、期末数量。 其余可排序列与 CSV `STAT-CFP-017``STAT-CFP-028` 一一对应:规格型号、产品编号、耗材类型、单位、期初数量、入库数量、入库金额、出库数量、出库金额、报损数量、报损金额、期末数量。
**已删除**`STAT-CFP-031` 期末金额列排序(页面无排序控件)。 **已删除**`STAT-CFP-019` 生产厂家列排序;`STAT-CFP-031` 期末金额列排序(页面无排序控件)。
--- ---
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment