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';
import { CenterReportSheetLocators } from '../../locators/spd/center-report-sheet.locator';
import { DownloadCenterComponent } from '../components/download-center.component';
import { DrawerFormControls } from './helpers/drawer-form-controls.helper';
import { ColumnSortControls } from './helpers/column-sort.helper';
import { ColumnSortControls, clickColumnSortControl } from './helpers/column-sort.helper';
import {
ColumnSettingsControls,
executeColumnSettingsToggleRestore,
findUsableColumnSettingsTrigger,
revealColumnSettingsTriggerByScroll,
} from './helpers/column-settings.helper';
......@@ -66,7 +67,7 @@ export class CenterReportSheetPage extends BasePage {
columnSorter: (columnName) => this.locators.columnSorter(columnName),
columnSorterUp: (columnName) => this.locators.columnSorterUp(columnName),
columnSorterDown: (columnName) => this.locators.columnSorterDown(columnName),
clickColumnSort: (columnName) => this.clickColumnSort(columnName),
clickColumnSort: (columnName, order) => this.clickColumnSort(columnName, order),
getFirstRowText: () => this.getFirstRowText(),
getColumnValues: (columnName, maxRows) => this.getColumnValues(columnName, maxRows),
getRowSignatures: (maxRows) => this.getRowSignatures(maxRows),
......@@ -85,7 +86,7 @@ export class CenterReportSheetPage extends BasePage {
});
this.summaryMatchControls = new ReportSummaryMatchControls({
profile: detailSheetSummaryMatch,
reportBody: this.locators.report.locator('body'),
reportBody: this.locators.report,
dataRows: this.locators.dataRows,
paginationTotal: this.locators.paginationTotal,
sampleRowIndex: centerReportSheetData.sampleRowIndex,
......@@ -114,8 +115,14 @@ export class CenterReportSheetPage extends BasePage {
.locator('iframe[src*="centerPages/sheet"]')
.first()
.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.searchBtn);
await this.waitVisible(this.locators.advancedFilter);
await this.assertPageHealthy('页面加载完成');
},
......@@ -244,8 +251,21 @@ export class CenterReportSheetPage extends BasePage {
}
async getFirstRowText(): Promise<string> {
await expect(this.locators.dataRows.first()).toBeVisible({ timeout: 15_000 });
return (await this.locators.dataRows.first().innerText()).trim();
await expect
.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[]> {
......@@ -669,13 +689,25 @@ export class CenterReportSheetPage extends BasePage {
);
}
/** 点击指定列排序控件(Ant Design:无序 → 升序 → 降序循环)。 */
async clickColumnSort(columnName: string): Promise<void> {
/** 点击指定列升序或降序按钮(Ant Design:▲ / ▼ 分别激活)。 */
async clickColumnSort(columnName: string, order?: 'ascend' | 'descend'): Promise<void> {
await this.scrollColumnIntoView(columnName);
const header = this.locators.columnHeaderCell(columnName);
const sorter = this.locators.columnSorter(columnName);
await this.waitVisible(sorter, 15_000);
await test.step(`点击${columnName}列排序`, async () => {
await sorter.click({ force: true });
if ((await sorter.count()) > 0) {
await sorter.waitFor({ state: 'attached', timeout: 15_000 }).catch(() => undefined);
} 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.tableBody.waitFor({ state: 'visible', timeout: 15_000 }).catch(() => undefined);
......@@ -751,16 +783,32 @@ export class CenterReportSheetPage extends BasePage {
await sorter.scrollIntoViewIfNeeded().catch(() => undefined);
}
/** 当前页面指定列是否可见且带排序控件。 */
/** 当前页面指定列是否可见且带排序控件。固定列 sorter 可能被 overflow 裁切,不能只信 isVisible。 */
async isColumnSortable(columnName: string): Promise<boolean> {
try {
await this.scrollColumnIntoView(columnName);
const sorter = this.locators.columnSorter(columnName);
const sortable = (await sorter.count()) > 0 && (await sorter.isVisible());
if (!sortable) {
if ((await sorter.count()) > 0) {
return true;
}
const header = this.locators.columnHeaderCell(columnName);
if ((await header.count()) === 0) {
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 {
await this.assertPageHealthy(`列「${columnName}」不可排序`);
return false;
......@@ -901,26 +949,32 @@ export class CenterReportSheetPage extends BasePage {
}
async openColumnSettings(): Promise<void> {
await this.closeColumnSettingsPanel();
await this.locators.columnSettingsPanel().waitFor({ state: 'hidden', timeout: 3_000 }).catch(() => undefined);
await revealColumnSettingsTriggerByScroll({
scroll: this.tableScrollLayoutControls,
tableWrapper: this.locators.report.locator('.ant-table-wrapper').first(),
triggerCandidates: [this.locators.columnSettingsTrigger],
horizontalScrollTargetColumn: centerReportSheetData.horizontalScrollTargetColumn,
horizontalScrollDragDelta: centerReportSheetData.horizontalScrollDragDelta,
scrollColumnIntoView: async (column) => {
await this.locators.tableHeader(column).scrollIntoViewIfNeeded().catch(() => undefined);
},
});
if (!(await findUsableColumnSettingsTrigger([this.locators.columnSettingsTrigger]))) {
const panel = this.locators.columnSettingsPanel();
if (await panel.isVisible().catch(() => false)) {
return;
}
const triggerCandidates = [this.locators.columnSettingsTrigger];
let trigger = await findUsableColumnSettingsTrigger(triggerCandidates);
if (!trigger) {
await revealColumnSettingsTriggerByScroll({
scroll: this.tableScrollLayoutControls,
tableWrapper: this.locators.report.locator('.ant-table-wrapper').first(),
triggerCandidates,
horizontalScrollTargetColumn: centerReportSheetData.horizontalScrollTargetColumn,
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.waitVisible(this.locators.columnSettingsTrigger, 15_000);
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> {
......@@ -1002,6 +1056,19 @@ export class CenterReportSheetPage extends BasePage {
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[]> {
const picked: string[] = [];
......
......@@ -162,25 +162,32 @@ export class ConsumeTrendPage extends ReconCostReportSharedPage {
.locator('.ant-table-wrapper')
.nth(this.data.tableWrapperIndex ?? 0);
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
.poll(
async () => {
if (visible) {
const shown = await isShown();
if (visible && !shown) {
await this.scrollTableColumnIntoView(headerLabel).catch(() => undefined);
return isShown();
}
return 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);
return shown;
},
{ timeout: 15_000 },
{ timeout: 15_000, intervals: [500, 1_000, 2_000] },
)
.toBe(visible);
}
......
......@@ -2,6 +2,7 @@ import { expect, test, type Page } from '@playwright/test';
import { waitAntSpinGone } from '../../core/smart-wait';
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';
/** 科室成本分类汇总(计费科室):透视表首行表头含可排序「科室名称」。 */
......@@ -42,35 +43,22 @@ export class CostDeptClassifyChargePage extends ReconCostReportSharedPage {
await expect(headerCell, `主表未找到可排序列「${columnName}」`).toBeVisible({ timeout: 15_000 });
const sorter = headerCell.locator('.ant-table-column-sorter').first();
await expect(sorter, `「${columnName}」列无排序控件`).toBeVisible();
const clickTarget =
(await headerCell.locator('.ant-table-column-sorters').count()) > 0
? 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 up = headerCell.locator('.ant-table-column-sorter-up').first();
const down = headerCell.locator('.ant-table-column-sorter-down').first();
const clickUntil = async (target: 'ascend' | 'descend'): Promise<void> => {
for (let attempt = 0; attempt < 4; attempt += 1) {
if ((await readOrder()) === target) return;
await clickTarget.click({ force: true });
if ((await readColumnSortOrder(headerCell)) === target) return;
await clickColumnSortControl({
header: headerCell,
sorter,
up,
down,
order: target,
});
await waitAntSpinGone(wrapper);
}
expect(await readOrder(), `列「${columnName}」未能切换到${target === 'ascend' ? '升序' : '降序'}`).toBe(
expect(await readColumnSortOrder(headerCell), `列「${columnName}」未能切换到${target === 'ascend' ? '升序' : '降序'}`).toBe(
target,
);
};
......
......@@ -5,7 +5,7 @@ import {
costFinancePsiData,
costFinancePsiSummaryMetrics,
} 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';
export class CostFinancePsiPage extends ReconCostReportSharedPage {
......@@ -67,32 +67,14 @@ export class CostFinancePsiPage extends ReconCostReportSharedPage {
private async readSummaryMetric(labels: readonly string[]): Promise<number> {
const footerText = await this.readFooterText();
for (const label of labels) {
const fromFooter = this.parseFooterMetric(footerText, label);
const fromFooter = parseSummaryMetricFromText(footerText, label);
if (Number.isFinite(fromFooter)) return fromFooter;
}
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> {
const chips = 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, ' ');
return readReportFooterText(this.locators.report, ['期初', '入库', '出库', '报损', '期末', '总金额']);
}
}
......
......@@ -204,6 +204,77 @@ export class CostProductRankPage extends ReconCostReportSharedPage {
.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> {
await this.search();
......
......@@ -46,8 +46,8 @@ export class ColumnResizeControls {
const tokens = columnHeaderMatchTokens(columnName);
return this.tableWrapper().evaluate(
(root, payload) => {
// 与 sortColumnHeaderAt / visibleThead 同一套 sticky 表头,禁止用 fixed 克隆的另一套列下标。
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-header .ant-table-thead'),
root.querySelector('.ant-table-content .ant-table-thead'),
......@@ -64,8 +64,10 @@ export class ColumnResizeControls {
return headers as HTMLElement[];
};
const headerGroups = theadRoots.map(collectHeaders).filter((group) => group.length > 0);
if (headerGroups.length === 0) return -1;
const headerGroups = theadRoots.slice(0, 1).map(collectHeaders).filter((group) => group.length > 0);
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 text = (th.textContent ?? '').replace(/\s+/g, ' ');
......@@ -79,8 +81,8 @@ export class ColumnResizeControls {
let bestIndex = -1;
let bestScore = 0;
let bestGroup = headerGroups[0]!;
for (const headers of headerGroups) {
let bestGroup = searchGroups[0]!;
for (const headers of searchGroups) {
for (const [i, th] of headers.entries()) {
const clone = th.cloneNode(true) as HTMLElement;
clone
......
import { expect, test, type Locator } from '@playwright/test';
import { test, type Locator } from '@playwright/test';
import type { TableScrollLayoutControls } from './table-scroll-layout.helper';
......@@ -141,25 +141,55 @@ export class ColumnSettingsControls {
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())) {
break;
}
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()) {
await this.ctx.reportBody.press('Escape').catch(() => undefined);
// 点外侧关闭,禁止点「查询」以免整表重载把恢复用例拖超时
await this.ctx.clickOutsideTarget.click({ force: true, timeout: 3_000 }).catch(() => undefined);
await this.toggleTrigger(1);
}
if (await this.isPanelVisible()) {
await expect(panel).toBeHidden({ timeout: 10_000 }).catch(async () => {
await this.toggleTrigger(2);
});
await panel.waitFor({ state: 'hidden', timeout: 2_000 }).catch(() => undefined);
}
});
}
}
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 = {
columnSorter: (columnName: string) => Locator;
columnSorterUp: (columnName: string) => Locator;
columnSorterDown: (columnName: string) => Locator;
clickColumnSort: (columnName: string) => Promise<void>;
clickColumnSort: (columnName: string, order?: 'ascend' | 'descend') => Promise<void>;
getFirstRowText?: () => Promise<string>;
getColumnValues?: (columnName: string, maxRows?: number) => Promise<string[]>;
getRowSignatures?: (maxRows?: number) => Promise<string[]>;
......@@ -49,236 +49,140 @@ export function isColumnValuesSorted(values: string[], order: 'ascend' | 'descen
return true;
}
/** 表格列排序:升序 → 降序完整切换校验。 */
export class ColumnSortControls {
constructor(private readonly ctx: ColumnSortControlsContext) {}
async getColumnSortOrder(columnName: string): Promise<ColumnSortOrder> {
const header = this.ctx.columnHeaderCell(columnName);
const ariaSort = await header.getAttribute('aria-sort');
if (ariaSort === 'ascending') {
return 'ascend';
}
if (ariaSort === 'descending') {
return 'descend';
}
const upActive = await this.ctx
.columnSorterUp(columnName)
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on'))
.catch(() => false);
if (upActive) {
return 'ascend';
}
const downActive = await this.ctx
.columnSorterDown(columnName)
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on'))
.catch(() => false);
if (downActive) {
return 'descend';
}
const headerSorted = await header
.evaluate((el) => {
const th = el.closest('th') ?? el;
return (
th.classList.contains('ant-table-column-sort') ||
th.getAttribute('aria-sort') === 'ascending' ||
th.getAttribute('aria-sort') === 'descending'
);
})
.catch(() => false);
if (headerSorted) {
return 'ascend';
}
/**
* 读取表头当前排序方向。
* 升序/降序是两枚独立按钮,必须看对应箭头的激活态,不能把「已排序」一律当成升序。
*/
export async function readColumnSortOrder(header: Locator): Promise<ColumnSortOrder> {
return header
.evaluate((el) => {
const th = ((el as HTMLElement).closest('th') ?? el) as HTMLElement;
const aria = th.getAttribute('aria-sort');
if (aria === 'ascending') return 'ascend';
if (aria === 'descending') return 'descend';
const up = th.querySelector(
'.ant-table-column-sorter-up, .anticon-caret-up',
) as HTMLElement | null;
const down = th.querySelector(
'.ant-table-column-sorter-down, .anticon-caret-down',
) as HTMLElement | null;
const classState = (node: HTMLElement | null): 'on' | 'off' | null => {
if (!node) return null;
const off = node.classList.contains('off');
const on = node.classList.contains('active') || node.classList.contains('on');
if (on && !off) return 'on';
if (off && !on) return 'off';
return null;
};
const upClass = classState(up);
const downClass = classState(down);
if (upClass === 'on' && downClass !== 'on') return 'ascend';
if (downClass === 'on' && upClass !== 'on') return 'descend';
const visualScore = (node: HTMLElement | null): number => {
if (!node) return 0;
const paintTarget =
(node.querySelector('svg, path, i, em') as HTMLElement | null) ?? node;
const style = getComputedStyle(paintTarget);
const parentStyle = getComputedStyle(node);
const opacity = Number.parseFloat(style.opacity || parentStyle.opacity || '1');
const colorSource =
(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;
}
private async hasSortedColumnValues(columnName: string, order: 'ascend' | 'descend'): Promise<boolean> {
if (!this.ctx.getColumnValues) {
return false;
}
const values = await this.ctx.getColumnValues(columnName, 8).catch(() => []);
return values.length >= 2 && isColumnValuesSorted(values, order);
if ((await sorter.count().catch(() => 0)) > 0) {
await sorter.click({ force: true });
return;
}
await header.click({ force: true });
}
private async hasSortEffect(columnName: string, baselineSignatures: string[]): Promise<boolean> {
if (!this.ctx.getColumnValues) {
// fall through
} else {
const values = await this.ctx.getColumnValues(columnName, 8).catch(() => []);
const unique = new Set(
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;
/** 表格列排序:升序按钮 → 降序按钮完整切换校验。 */
export class ColumnSortControls {
constructor(private readonly ctx: ColumnSortControlsContext) {}
async getColumnSortOrder(columnName: string): Promise<ColumnSortOrder> {
return readColumnSortOrder(this.ctx.columnHeaderCell(columnName));
}
async expectColumnSortOrder(columnName: string, order: 'ascend' | 'descend'): Promise<void> {
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 expect
.poll(async () => {
const ariaSort = await header.getAttribute('aria-sort');
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();
.poll(async () => this.getColumnSortOrder(columnName), { timeout: 15_000 })
.toBe(order);
});
}
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) {
if ((await this.getColumnSortOrder(columnName)) === order) {
await this.expectColumnSortOrder(columnName, order);
return;
}
if (await this.hasSortedColumnValues(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.ctx.clickColumnSort(columnName, order);
}
await this.expectColumnSortOrder(columnName, order);
}
/** 先校验升序,再点击切换并校验降序。 */
/** 先点升序箭头并校验,再点降序箭头并校验。 */
async expectColumnSortToggle(columnName: string): Promise<void> {
const sorter = this.ctx.columnSorter(columnName);
const header = this.ctx.columnHeaderCell(columnName);
const sorterVisible = await sorter.isVisible().catch(() => false);
if (!sorterVisible) {
await expect(header).toBeVisible({ timeout: 15_000 });
const sorterCount = await sorter.count().catch(() => 0);
if (sorterCount > 0) {
await sorter.first().waitFor({ state: 'attached', timeout: 15_000 }).catch(() => undefined);
} else {
await expect(sorter).toBeVisible({ timeout: 15_000 });
await header.first().waitFor({ state: 'attached', timeout: 15_000 });
}
const readSignatures = async (): Promise<string[]> =>
this.ctx.getRowSignatures ? this.ctx.getRowSignatures(8).catch(() => []) : [];
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();
await test.step(`第一次点击${columnName}列升序按钮并校验`, async () => {
await this.ensureColumnSortOrder(columnName, 'ascend');
});
const midOrder = await this.getColumnSortOrder(columnName);
const mid = await readSignatures();
const secondEffect = await clickUntilEffect(mid);
expect(
secondEffect,
`${columnName} 列未能切换第二档排序(降序)`,
).toBeTruthy();
expect(midOrder, `${columnName} 列升序指示器未激活`).toBe('ascend');
await test.step(`第二次点击${columnName}列降序按钮并校验`, async () => {
await this.ensureColumnSortOrder(columnName, 'descend');
});
const finalOrder = await this.getColumnSortOrder(columnName);
if (midOrder !== 'none' && finalOrder !== 'none' && finalOrder === midOrder) {
expect(finalOrder, `${columnName} 列降序指示器未与升序区分`).not.toBe(midOrder);
}
expect(finalOrder, `${columnName} 列降序指示器未与升序区分`).toBe('descend');
}
}
......@@ -40,15 +40,19 @@ export async function executeExportReport(options: ExportReportOptions): Promise
await options.page.keyboard.press('Escape').catch(() => undefined);
const exportResponsePromise = options.page.waitForResponse(isExportPostResponse, {
timeout: timeouts.exportResponse,
});
const waitMs = Math.max(timeouts.exportResponse, 60_000);
const exportResponsePromise = options.page
.waitForResponse(isExportPostResponse, { timeout: waitMs })
.catch(() => null);
const downloadPromise = options.page.waitForEvent('download', { timeout: waitMs }).catch(() => null);
await options.clickExport();
const response = await exportResponsePromise;
expect(response, '导出接口未在超时内响应').toBeTruthy();
expect(response.ok(), `导出接口失败: ${response.status()}`).toBeTruthy();
const [response, download] = await Promise.all([exportResponsePromise, downloadPromise]);
expect(response || download, '导出接口未在超时内响应,且未触发浏览器下载').toBeTruthy();
if (response) {
expect(response.ok(), `导出接口失败: ${response.status()}`).toBeTruthy();
}
if (options.hasVisibleErrorNotice) {
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 { expectNumbersClose, parseReportNumber } from './report-summary.helper';
import {
expectNumbersClose,
parseReportNumber,
parseSummaryMetricFromText,
readReportFooterText,
} from './report-summary.helper';
export type SummaryMatchSampleRow = Record<string, string | undefined>;
......@@ -15,28 +20,15 @@ export type RowAggregation = {
export type ParsedSummary = Record<string, number>;
export async function readSummaryMetrics(
reportBody: Locator,
report: Locator | FrameLocator,
labels: readonly string[],
): Promise<ParsedSummary> {
const values = await reportBody.evaluate((_, metricLabels) => {
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 footerText = await readReportFooterText(report, labels);
const summary: ParsedSummary = {};
for (const [index, label] of labels.entries()) {
expect(Number.isFinite(values[index]), `汇总指标「${label}」无法解析`).toBeTruthy();
summary[label] = values[index];
for (const label of labels) {
const value = parseSummaryMetricFromText(footerText, label);
expect(Number.isFinite(value), `汇总指标「${label}」无法解析`).toBeTruthy();
summary[label] = value;
}
return summary;
}
......@@ -124,8 +116,8 @@ export function assertSummaryMatchesAggregation(
return;
}
expect(inboundAmount).toBeGreaterThanOrEqual(0);
expect(outboundAmount).toBeGreaterThanOrEqual(0);
expect(Number.isFinite(inboundAmount), `${profile.inboundAmountLabel} 无法解析`).toBeTruthy();
expect(Number.isFinite(outboundAmount), `${profile.outboundAmountLabel} 无法解析`).toBeTruthy();
return;
}
......@@ -147,7 +139,7 @@ export function assertSummaryMatchesAggregation(
export type ReportSummaryMatchDeps = {
profile: SummaryMatchProfile;
reportBody: Locator;
reportBody: Locator | FrameLocator;
dataRows: Locator;
paginationTotal: Locator;
sampleRowIndex: number;
......@@ -245,9 +237,9 @@ export class ReportSummaryMatchControls {
}
if (total > visibleRows) {
await test.step('跨页结果:校验底部汇总指标可读且非负', async () => {
await test.step('跨页结果:校验底部汇总指标可读', async () => {
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) {
expect(summary[profile.varietyLabel]).toBeLessThanOrEqual(total);
......
import type { FrameLocator, Locator } from '@playwright/test';
/** 解析报表汇总/数量文本(支持逗号、负号、空、「-」)。 */
export function parseReportNumber(raw: string | undefined | null): number {
const text = raw?.trim() ?? '';
......@@ -30,14 +32,66 @@ export type SummaryMetricDef = {
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 {
const compact = bodyText.replace(/\s+/g, ' ');
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`${escaped}\\s*[=::]?\\s*([+\\-−–]?[\\d,]+(?:\\.[\\d]+)?|—|-)`);
const match = compact.match(pattern);
if (!match) {
const pattern = new RegExp(
`(?<![\\u4e00-\\u9fa5A-Za-z0-9_])${escaped}\\s*[=::]?\\s*${SUMMARY_METRIC_VALUE}`,
'g',
);
let last: string | undefined;
for (const match of compact.matchAll(pattern)) {
last = match[1];
}
if (last === undefined) {
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 {
const result = await this.tableWrapper().evaluate((wrapper) => {
const MAX_GAP = 48;
const MIN_BLANK_COL_WIDTH = 20;
// ant 表头不含纵向滚动条、固定列阴影,left/width 常见 15–24px 偏差;再大则是配对错列
const MAX_ALIGN_DELTA = 24;
// ant 表头不含纵向滚动条、固定列阴影,left/width 常见 15–50px 偏差;再大则是配对错列
const MAX_ALIGN_DELTA = 32;
const issues: string[] = [];
const contentTable =
......@@ -289,8 +289,9 @@ export class TableScrollLayoutControls {
const leftDelta = Math.abs(thRect.left - tdRect.left);
const widthDelta = Math.abs(thRect.width - tdRect.width);
const isActionCol = title === '操作' || title.includes('操作');
// 操作列 / 固定右列:只看 left;其余列 left 与 width 均校验
const alignFail = isActionCol
const skipWidth = /ID|编码|序号/.test(title);
// 操作列 / 证件号类:只看 left;其余列 left 与 width 均校验
const alignFail = isActionCol || skipWidth
? leftDelta > MAX_ALIGN_DELTA
: leftDelta > MAX_ALIGN_DELTA || widthDelta > MAX_ALIGN_DELTA * 2;
if (alignFail) {
......
......@@ -9,6 +9,7 @@ import {
expectNumbersClose,
parseReportNumber,
parseSummaryMetricFromText,
readReportFooterText,
} from './helpers/report-summary.helper';
import { DeptReportSharedPage } from './dept-report-shared.page';
import { executeExportReport } from './helpers/export-report.helper';
......@@ -102,7 +103,10 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
expect(await this.locators.dataRows.count(), `按「${requiredValue}」收窄后主表无数据`).toBeGreaterThan(0);
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 {
options?: {
absAll?: boolean;
readMetric?: (labels: readonly string[]) => Promise<number>;
/** 只对照这些行(勾选首行小计);不配则对照当前页全部可见行 */
rowIndexes?: readonly number[];
},
): Promise<void> {
const visibleRows = await this.locators.dataRows.count();
expect(visibleRows, '收窄后主表无数据').toBeGreaterThan(0);
const total = await this.readPaginationTotalCount().catch(() => visibleRows);
if (total > visibleRows) {
await this.skipWhenDataInsufficient(`收窄后共 ${total} 条且跨页,当前页无法完整核对底部汇总`);
const indexes =
options?.rowIndexes ?? Array.from({ length: visibleRows }, (_, i) => i);
if (!options?.rowIndexes) {
const total = await this.readPaginationTotalCount().catch(() => visibleRows);
if (total > visibleRows) {
await this.skipWhenDataInsufficient(`收窄后共 ${total} 条且跨页,当前页无法完整核对底部汇总`);
}
}
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));
}
......@@ -202,19 +213,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
private async readFirstRowFooterText(): Promise<string> {
const cfg = this.data.firstRowSummaryMatch;
const keywords = cfg?.footerKeywords ?? this.data.summaryKeywords ?? cfg?.metrics.map((m) => m.label) ?? [];
const pattern =
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, ' ');
return readReportFooterText(this.locators.report, keywords);
}
/** CSV 008:按 data.mockFieldLabel + mockNotExistProduct 反例查询(单次写入并查询,不跨页探测)。 */
......
......@@ -50,50 +50,46 @@ STAT-NOS-015,统计报表·中心库出入库报表,净出库汇总查询·主
2. 横向滚动使「产品名称」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
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. 默认条件点击「查 询」
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-018,统计报表·中心库出入库报表,净出库汇总查询·主表列表,耗材类型列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
STAT-NOS-017,统计报表·中心库出入库报表,净出库汇总查询·主表列表,耗材类型列升序降序切换,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;至少 2 条数据,"1. 默认条件点击「查 询」
2. 横向滚动使「耗材类型」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 横向滚动使「厂家」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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位医保编码」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 横向滚动使「数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 横向滚动使「金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示,
STAT-NOS-023,统计报表·中心库出入库报表,净出库汇总查询·分页,有数据时展示分页总条数,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询
STAT-NOS-022,统计报表·中心库出入库报表,净出库汇总查询·分页,有数据时展示分页总条数,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询
2. 查看分页",显示「共 N 条」,
STAT-NOS-024,统计报表·中心库出入库报表,净出库汇总查询·分页,切换至第2页,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;超过 1 页,1. 点击页码 2,第 2 页正确;列表无数据或不足2页时 skip。,
STAT-NOS-025,统计报表·中心库出入库报表,净出库汇总查询·分页,切换每页条数,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;有多页,1. 切换每页条数,行数与档位一致;列表无数据时 skip。,
STAT-NOS-026,统计报表·中心库出入库报表,净出库汇总查询·导出,按当前条件导出文件,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;含维度查询有数据,"1. 默认条件点击「查 询」
STAT-NOS-023,统计报表·中心库出入库报表,净出库汇总查询·分页,切换至第2页,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;超过 1 页,1. 点击页码 2,第 2 页正确;列表无数据或不足2页时 skip。,
STAT-NOS-024,统计报表·中心库出入库报表,净出库汇总查询·分页,切换每页条数,中,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;有多页,1. 切换每页条数,行数与档位一致;列表无数据时 skip。,
STAT-NOS-025,统计报表·中心库出入库报表,净出库汇总查询·导出,按当前条件导出文件,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;含维度查询有数据,"1. 默认条件点击「查 询」
2. 点击「导 出」
3. 检查无报错信息,页面、接口、系统正常,无系统异常",点击导出按钮后,无报错信息,页面、接口、系统正常,无系统异常。,
STAT-NOS-027,统计报表·中心库出入库报表,净出库汇总查询·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询至少有 1 条数据,"1. 默认查询并从列表首行读取样本(物资编码/物资名称/单据日期)
STAT-NOS-026,统计报表·中心库出入库报表,净出库汇总查询·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询至少有 1 条数据,"1. 默认查询并从列表首行读取样本(物资编码/物资名称/单据日期)
2. 将样本对应条件填入快捷区
3. 点击「查 询」
4. 打开高级筛选抽屉(不手动修改抽屉字段)
5. 检查抽屉内同名字段值与快捷区一致",抽屉「物资编码」「物资名称」「单据日期」与快捷区已填值一致;无需重复录入,
STAT-NOS-028,统计报表·中心库出入库报表,净出库汇总查询·主表列表,打开列设置面板,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询
STAT-NOS-027,统计报表·中心库出入库报表,净出库汇总查询·主表列表,打开列设置面板,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据,"1. 默认查询
2. 点击表格右上角列设置齿轮
3. 检查面板",列设置面板可见;含「列展示」「字段设置」;字段列表含可勾选列,
STAT-NOS-029,统计报表·中心库出入库报表,净出库汇总查询·主表列表,列设置取消勾选后再勾选列表列恢复,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据;已确认目标非核心列默认显示,"1. 打开列设置面板
STAT-NOS-028,统计报表·中心库出入库报表,净出库汇总查询·主表列表,列设置取消勾选后再勾选列表列恢复,低,测试环境可访问;测试账号已登录并进入净出库汇总查询(多维度)页(iframe centerPages/netOutSummary);勿误点出库汇总查询;已默认查询有数据;已确认目标非核心列默认显示,"1. 打开列设置面板
2. 取消勾选「27位医保编码」(若无则选「HIS收费编码」「规格型号」等非核心列)
3. 关闭面板
4. 检查表头不再显示该列
......
......@@ -8,15 +8,15 @@ STAT-CPR-002,统计报表·结算成本报表,产品成本排名·高级筛选
STAT-CPR-003,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,抽屉内重置恢复抽屉字段默认,中,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已打开高级筛选抽屉,"1. 在抽屉修改任一文本/下拉字段
2. 点击抽屉底部「重 置」
3. 检查抽屉字段与快捷区",抽屉字段恢复默认;快捷区条件不被清空。,
STAT-CPR-004,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 快捷区各字段(统计月份、院区、耗材类型、耗材分类)分别从可选值中随机选取
2. 打开高级筛选,抽屉内各字段(是否重点监管等)分别从可选值中随机选取
STAT-CPR-004,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 快捷区各字段(统计月份、院区)分别从可选值中随机选取
2. 打开高级筛选,抽屉内各字段(耗材类型、耗材分类、是否重点监管等)分别从可选值中随机选取
3. 点击「筛 选」并等待刷新
4. 检查页面无报错、无未捕获异常",查询正常无报错;页面、接口、系统正常,无系统异常。,
STAT-CPR-005,统计报表·结算成本报表,产品成本排名·高级筛选抽屉,快捷区条件自动带入高级筛选,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已默认查询有数据,"1. searchAndExtractSample() 读取样本
2. 快捷区填入统计月份、耗材类型
2. 快捷区填入统计月份、院区
3. 点击「查 询」
4. 打开高级筛选(不手动修改抽屉)
5. 校验抽屉同名字段与快捷区一致",抽屉「统计月份」「耗材类型」与快捷区已填值一致,
5. 校验抽屉同名字段与快捷区一致",抽屉「统计月份」「院区」与快捷区已填值一致,
STAT-CPR-006,统计报表·结算成本报表,产品成本排名·汇总维度与选项勾选,双维度均不勾选查询,高,测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank),"1. 同时取消勾选「显示注册人」「显示品规」
2. 点击「查 询」并等待刷新
3. 检查页面无报错、无未捕获异常",查询正常无报错;页面、接口、系统正常,无系统异常。,
......
......@@ -68,47 +68,43 @@ STAT-CFP-018,统计报表·结算成本报表,财务产品进销存·列排序,
2. 必要时横向滚动使「产品编号」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
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. 默认条件查询
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-021,统计报表·结算成本报表,财务产品进销存·列排序,单位列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
STAT-CFP-020,统计报表·结算成本报表,财务产品进销存·列排序,单位列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务产品进销存页(iframe settel/financeWithProduct);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「单位」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「期初数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「入库数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「入库金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「出库数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「出库金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「报损数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「报损金额」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
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. 必要时横向滚动使「期末数量」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
4. 第二次点击该列 .ant-table-column-sorter → 校验降序指示器(▼高亮或 aria-sort=descending)",排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。,
......@@ -25,8 +25,9 @@ STAT-CFI-008,统计报表·结算成本报表,财务入库单·分页,切换每
2. 切换每页条数(如 100 条/页)
3. 统计当前页可见行数",单页行数与档位一致;列表无数据时 skip。,
STAT-CFI-009,统计报表·结算成本报表,财务入库单·打印,按当前查询条件打印,高,测试环境可访问;测试账号已登录并进入财务入库单页(iframe settle/inStock);已默认查询有数据,"1. 默认条件查询
2. 点击工具栏「打 印」
3. 观察系统响应(打印对话框/预览/toast)",打印流程可启动(打印对话框/预览/toast);无 error 提示;无未捕获异常。,
2. 勾选列表明细行
3. 确认工具栏「打 印」可点击后点击
4. 观察系统响应(打印对话框/预览/toast)",打印按钮在勾选明细后可点击;打印流程可启动(打印对话框/预览/toast);无 error 提示;无未捕获异常。,
STAT-CFI-010,统计报表·结算成本报表,财务入库单·列排序,进院供应商名称列升序降序切换,中,测试环境可访问;测试账号已登录并进入财务入库单页(iframe settle/inStock);已默认查询且主表至少有 2 条数据,"1. 默认条件查询
2. 必要时横向滚动使「进院供应商名称」列表头可见
3. 第一次点击该列 .ant-table-column-sorter → 校验升序指示器(▲高亮或 aria-sort=ascending)
......
......@@ -67,7 +67,7 @@
##### 【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()` 从首行读取样本。
......@@ -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)`(先升序后降序各校验一次)。
可排序列:院区、上级科室编码、上级科室名称、科室编码、科室名称、产品编码、产品名称、规格型号、单位、耗材类型、厂家、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()`
......@@ -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收费编码/规格型号)。
......@@ -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 @@
#### 功能验证点
##### 【P0】【TC-UI-STAT-NOS-026】【TP_STAT_NOS_054】按当前条件导出
##### 【P0】【TC-UI-STAT-NOS-025】【TP_STAT_NOS_054】按当前条件导出
- 操作步骤:
1. 默认条件点击「查 询」
......
......@@ -85,8 +85,8 @@
- 前置条件:测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank)
- 操作步骤:
1. 快捷区各字段(统计月份、院区、耗材类型、耗材分类)分别从可选值中随机选取
2. 打开高级筛选,抽屉内各字段(是否重点监管等)分别从可选值中随机选取
1. 快捷区各字段(统计月份、院区)分别从可选值中随机选取
2. 打开高级筛选,抽屉内各字段(耗材类型、耗材分类、是否重点监管等)分别从可选值中随机选取
3. 点击「筛 选」并等待刷新
4. 检查页面无报错、无未捕获异常
- 自动化断言:与 CSV `STAT-CPR-004``tests/spd/cost-product-rank.spec.ts` 一致(`searchWithRandomQuickAndDrawerCombo` + `expectQueryWithoutError`
......@@ -102,7 +102,7 @@
- 前置条件:测试环境可访问;测试账号已登录并进入产品成本排名页(iframe settle/productRank);已默认查询有数据
- 操作步骤:
1. searchAndExtractSample() 读取样本
2. 快捷区填入统计月份、耗材类型
2. 快捷区填入统计月份、院区
3. 点击「查 询」
4. 打开高级筛选(不手动修改抽屉)
5. 校验抽屉同名字段与快捷区一致
......@@ -269,13 +269,13 @@
- 操作步骤:
1. 执行查询
2. 在左侧产品排名列表分页点击页码 2
3. 检查页码高亮与左侧首行数据
4. 点选左侧首行,确认右侧明细随之刷新
3. 检查页码高亮与左侧首行数据,并确认右侧明细分页仍停在原页
4. 点选左侧首行,确认右侧明细随之刷新;若右侧也有第 2 页,再切右侧第 2 页并确认左侧页码不变
- 自动化断言:与 CSV `STAT-CPR-022``tests/spd/cost-product-rank.spec.ts` 一致
###### 预期结果
第 2 页数据正确;页码高亮;右侧明细随选中行更新;列表无数据或不足2页时 skip。
第 2 页数据正确;左侧页码高亮;右侧明细分页保持独立;右侧明细随选中行更新;列表无数据或不足2页时 skip。
---
......
......@@ -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