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

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

parent 35fe36af
......@@ -6,7 +6,6 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
['院区名称', '院区'],
['27位医保编码', '医保编码', '医保码'],
['医保单件名称', '单件名称', '医保名称'],
['数量', '净入数量', '净出数量', '入库数量', '出库数量', '成本数量'],
['HIS编码', 'HIS码', 'His编码'],
['结算单标签', '标签'],
['产品编号', '产品编码', '物资编码', '商品编码'],
......@@ -18,10 +17,7 @@ function aliasTokensFor(columnName: string): string[] {
const normalized = columnName.replace(/\s+/g, '').trim();
const out: string[] = [];
for (const group of COLUMN_HEADER_ALIAS_GROUPS) {
const hit = group.some((alias) => {
const a = alias.replace(/\s+/g, '');
return a === normalized || a.includes(normalized) || normalized.includes(a);
});
const hit = group.some((alias) => alias.replace(/\s+/g, '') === normalized);
if (hit) {
for (const alias of group) {
const a = alias.replace(/\s+/g, '').trim();
......
......@@ -110,6 +110,15 @@ export const anaChgData: CenterReportPageData & {
comboSamplePickKeys: ['field', 'productName', 'spec', 'supplierName', 'field2', 'consumableType'],
comboSampleMaxRowScan: 5,
drawerFields: anaChgComboDrawerFields,
firstRowSummaryMatch: {
metrics: [
{ key: 'field8', label: '选中总数量', summaryLabels: ['选中总数量'] },
{ key: 'field9', label: '选中总差额', summaryLabels: ['选中总差额'] },
],
requiredKey: 'productName',
fields: anaChgComboQuickFields,
footerKeywords: ['选中总数量', '选中总差额', '合计'],
},
};
export const anaChgSortCases = buildSortColumnCases('STAT-ACH', anaChgSortColumns, 10);
......@@ -24,7 +24,6 @@ export const centerReportNetOutSortColumns = [
'科室名称',
'产品编码',
'产品名称',
'规格型号',
'单位',
'耗材类型',
'厂家',
......
......@@ -26,6 +26,8 @@ export type CenterReportPageData = {
sortColumns?: readonly string[];
sortCasePrefix?: string;
sortCaseStartIndex?: number;
/** 列排序前需勾选的维度(如「产品批号」依赖「区分批号」) */
sortColumnPrerequisites?: Readonly<Record<string, readonly string[]>>;
mockNotExistProduct: string;
mockNotExistCode?: string;
mockNotExistMaterial?: string;
......
......@@ -44,6 +44,7 @@ export const costFinInData: CenterReportPageData = {
hasExpandRow: false,
hasBatchPrint: false,
hasToolbarPrint: true,
printRequiresDetailSelection: true,
hasFilterToggle: false,
exportTaskName: '财务入库单',
exportDirectDownload: false,
......
......@@ -66,7 +66,6 @@ export const costFinancePsiSortColumns = [
'产品名称',
'规格型号',
'产品编号',
'生产厂家',
'耗材类型',
'单位',
'期初数量',
......
......@@ -37,11 +37,11 @@ export const costProductRankDetailHeaders = ['科室名称', '成本数量', '
export const costProductRankComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'statMonth', label: '统计月份', type: 'date' },
{ key: 'campus', label: '院区', type: 'select' },
{ key: 'consumableType', label: '耗材类型', type: 'select' },
{ key: 'consumableCategory', label: '耗材分类', type: 'select' },
];
export const costProductRankComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'consumableType', type: 'select', label: '耗材类型' },
{ key: 'consumableCategory', type: 'select', label: '耗材分类' },
{ key: 'isKeySupervised', type: 'select', label: '是否重点监管' },
];
......
......@@ -51,7 +51,7 @@ export const deptReportOutData: CenterReportPageData = {
exportDirectDownload: false,
exportSelectFirstRow: false,
exportQueryFromSample: true,
comboSampleScrollColumns: ['结算方式', '单据形态', '批号', '科室名称', '单号', '进院供应商名称'],
comboSampleScrollColumns: ['产品编号', '产品名称', '结算方式', '单据形态', '批号', '科室名称', '单号', '进院供应商名称'],
comboSamplePickKeys: ['productName', 'accountDate', 'billType', 'deptName', 'dept', 'orderNo', 'settlement', 'billMode', 'batchNo'],
comboSampleFieldAliases: { dept: 'deptName' },
comboSampleMaxRowScan: 5,
......
......@@ -38,6 +38,9 @@ export const deptReportTurnoverData: CenterReportPageData = {
sortCasePrefix: 'STAT-DTR',
sortCaseStartIndex: 6,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '库存周转率',
horizontalScrollDragDelta: 320,
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
......
......@@ -33,7 +33,7 @@ export const invoiceSettleComboAssertKeys = ['supplierName', 'field4'] as const;
export const invoiceSettleDomainQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '结算月份', type: 'date' },
{ key: 'supplierName', label: '进院供应商名称', type: 'select' },
{ key: 'supplierName', label: '进院供应商', type: 'select' },
];
export const invoiceSettleDomainAssertKeys = ['supplierName', 'field4'] as const;
......@@ -84,8 +84,8 @@ export const invoiceSettleData: CenterReportPageData & {
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['线上线下'],
comboSampleScrollColumns: ['院区名称'],
comboSamplePickKeys: ['supplierName', 'field4', 'isVolume', 'field2'],
comboSampleScrollColumns: ['结算月份', '进院供应商', '院区名称'],
comboSamplePickKeys: ['field', 'supplierName', 'field4', 'isVolume', 'field2'],
drawerFields: invoiceSettleComboDrawerFields,
};
......
......@@ -103,7 +103,7 @@ export const polConsumerData: CenterReportPageData & {
},
horizontalScrollAnchorColumn: '消耗日期',
horizontalScrollTargetColumn: '带量项目合同号',
horizontalScrollTargetColumn: '完成数量',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockFieldLabel: '带量项目',
......
......@@ -55,6 +55,9 @@ export const reconPurchaseEntryData: CenterReportPageData = {
sortColumns: reconPurchaseEntrySortColumns,
sortCasePrefix: 'STAT-RPE',
sortCaseStartIndex: 16,
sortColumnPrerequisites: {
产品批号: ['区分批号'],
},
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: false,
......@@ -69,7 +72,8 @@ export const reconPurchaseEntryData: CenterReportPageData = {
horizontalScrollAnchorColumn: '产品名称',
horizontalScrollTargetColumn: '产品类型',
horizontalScrollDragDelta: 320,
comboSamplePickKeys: ['productName', 'statDate', 'supplierName', 'subSupplierName'],
comboSamplePickKeys: ['productName', 'statDate', 'supplierName', 'subSupplierName', 'inboundQty'],
comboSampleScrollColumns: ['采购入库数量', '产品类型'],
drawerFields: reconPurchaseEntryComboDrawerFields,
firstRowSummaryMatch: {
metrics: [
......
......@@ -72,7 +72,7 @@ export const warnDetainData: CenterReportPageData & {
},
horizontalScrollAnchorColumn: '院区名称',
horizontalScrollTargetColumn: '院区名称',
horizontalScrollTargetColumn: '待发数量',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockFieldLabel: '产品名称',
......
......@@ -9,7 +9,7 @@
| 任务 | 套件 | 建议 cron | 说明 |
|------|------|-----------|------|
| `spd-ui-smoke` | `smoke` | `H 8-18 * * 1-5` | 工作时段冒烟:全部报表模块已打 `@smoke` 的用例 |
| `spd-ui-nightly` | `nightly` | `H 2 * * *` | 夜间主 spec 回归,不含 `@full`、不含调试 `*.stat-*.spec.ts` |
| `spd-ui-nightly` / `SPD` | `nightly` / `full` | `H 23 * * 1,3,5` | 周一、三、五 **23 点** 回归(Jenkins 任务 `SPD` 当前按此时段跑 full) |
| `spd-ui-full` | `full` | `H 3 * * 0` | 周末含全列排序,最慢 |
同一套 `Jenkinsfile` 用参数 `SUITE` 区分即可;定时任务在 Jenkins 里「Build periodically」分别写 cron。
......@@ -84,7 +84,7 @@ npm run test:spd:shard
| Jenkins 任务名 | 默认 `SUITE` | Build periodically | 建议参数 |
|----------------|--------------|--------------------|----------|
| `spd-ui-smoke` | `smoke` | `H 8-18 * * 1-5` | `WORKERS=3` `SHARDS=1` |
| `spd-ui-nightly` | `nightly` | `H 2 * * *` | `WORKERS=2` `SHARDS=2` |
| `spd-ui-nightly` / `SPD` | `nightly` / `full` | `H 23 * * 1,3,5` | 周一、三、五 23 点;`WORKERS=2` `SHARDS=2` |
| `spd-ui-full` | `full` | `H 3 * * 0` | `WORKERS=2` `SHARDS=2`,勾选 `RUN_FULL` |
每个任务:
......
......@@ -88,19 +88,41 @@ export class CenterReportEntryLocators {
}
tableHeaderCells(): Locator {
return this.report.locator('.ant-table-wrapper').first().locator('.ant-table-thead tr').last().locator('th');
return this.visibleThead().locator('tr').last().locator('th');
}
/**
* 可见 sticky 表头。宽表会在 body / fixed 区再克隆 thead,
* `.ant-table-thead tr`.last() 会命中测宽空行或克隆列,waitFor 触发 strict mode。
*/
visibleThead(): Locator {
const wrapper = this.report.locator('.ant-table-wrapper').first();
return wrapper
.locator('.ant-table-container > .ant-table-header .ant-table-thead')
.or(wrapper.locator('.ant-table-header .ant-table-thead'))
.or(wrapper.locator('.ant-table-content > table > .ant-table-thead'))
.or(wrapper.locator('.ant-table-thead'))
.first();
}
/** 指定列的表头单元格;每条分支先 .first(),避免 or() 在多匹配 waitFor 时 strict mode。 */
columnHeaderCell(columnName: string): Locator {
const thead = this.report.locator('.ant-table-wrapper').first().locator('.ant-table-thead tr').last();
const thead = this.visibleThead().locator('tr').last();
const byTitle = thead
.locator('th:visible')
.filter({
has: this.report.locator('.ant-table-column-title').getByText(columnName, { exact: true }),
})
.filter({ has: this.report.locator('.ant-table-column-sorter') });
const byRole = this.report.getByRole('columnheader', { name: columnName, exact: true });
return byTitle.or(byRole).first();
.locator('th')
.filter({ has: thead.locator('.ant-table-column-title').getByText(columnName, { exact: true }) })
.first();
const bySorter = thead
.locator('th')
.filter({ has: thead.locator('.ant-table-column-title').getByText(columnName, { exact: true }) })
.filter({ has: thead.locator('.ant-table-column-sorter') })
.first();
const byRole = this.report
.locator('.ant-table-wrapper')
.first()
.getByRole('columnheader', { name: columnName, exact: true })
.first();
return byTitle.or(bySorter).or(byRole).first();
}
columnSorter(columnName: string): Locator {
......@@ -146,14 +168,21 @@ export class CenterReportEntryLocators {
}
subTableRow(rowIndex = 0): Locator {
return this.expandedRow.locator('.ant-table tbody tr').nth(rowIndex);
return this.expandedRow
.locator('.ant-table tbody tr.ant-table-row:not(.ant-table-measure-row)')
.nth(rowIndex);
}
subTableViewButton(rowIndex = 0): Locator {
const row = this.subTableRow(rowIndex);
return row
.getByRole('button', { name: '查看' })
.or(row.getByRole('link', { name: '查看' }))
.first();
const fixedRightRow = this.expandedRow
.locator('.ant-table-fixed-right tbody tr.ant-table-row:not(.ant-table-measure-row)')
.nth(rowIndex);
const inRow = (host: Locator) =>
host
.getByRole('button', { name: /查\s*看/ })
.or(host.getByRole('link', { name: /查\s*看/ }))
.or(host.locator('a, button, span, .ant-btn').filter({ hasText: /查\s*看/ }));
return inRow(fixedRightRow).or(inRow(row)).first();
}
}
......@@ -39,7 +39,6 @@ export class CenterReportSharedLocators {
readonly tableBody: Locator;
readonly dataRows: Locator;
sortDataRows: Locator;
readonly paginationTotal: Locator;
readonly drawerTitle: Locator;
readonly drawerFilterBtn: Locator;
readonly drawerResetBtn: Locator;
......@@ -50,6 +49,7 @@ export class CenterReportSharedLocators {
readonly tableWrapperIndex: number;
readonly detailTableWrapperIndex: number;
sortTableWrapperIndex: number;
paginationTableWrapperIndex: number;
readonly detailDataRows: Locator;
constructor(page: Page, options: CenterReportLocatorOptions) {
......@@ -57,6 +57,7 @@ export class CenterReportSharedLocators {
this.tableWrapperIndex = options.tableWrapperIndex ?? 0;
this.detailTableWrapperIndex = options.detailTableWrapperIndex ?? this.tableWrapperIndex;
this.sortTableWrapperIndex = options.sortTableWrapperIndex ?? this.tableWrapperIndex;
this.paginationTableWrapperIndex = this.tableWrapperIndex;
this.productName = this.report.locator(options.productNameSelector).first();
this.goodsCode = options.goodsCodeSelector
? this.report.locator(options.goodsCodeSelector).first()
......@@ -112,7 +113,6 @@ export class CenterReportSharedLocators {
'.ant-table-scroll .ant-table-body table tbody > tr.ant-table-row',
].join(', '),
);
this.paginationTotal = this.report.locator('.ant-pagination-total-text').first();
this.drawerTitle = this.report.locator('.ant-drawer-open .ant-drawer-title');
this.drawerFilterBtn = this.report.getByRole('button', { name: '筛 选' });
this.drawerResetBtn = this.report.locator('.ant-drawer-footer').getByRole('button', { name: '重 置' });
......@@ -241,6 +241,24 @@ export class CenterReportSharedLocators {
);
}
bindPaginationTable(index: number): void {
this.paginationTableWrapperIndex = index;
}
/** 按 tableWrapper 定位对应 ProTable 底部分页,避免左右双表互相抢页码。 */
paginationHost(): Locator {
const wrapper = this.report.locator('.ant-table-wrapper').nth(this.paginationTableWrapperIndex);
return wrapper
.locator('xpath=ancestor::*[contains(@class,"ant-pro-table")][1]//ul[contains(@class,"ant-pagination")]')
.first()
.or(wrapper.locator('xpath=following::ul[contains(@class,"ant-pagination")][1]'))
.or(this.report.locator('.ant-pagination').nth(this.paginationTableWrapperIndex));
}
get paginationTotal(): Locator {
return this.paginationHost().locator('.ant-pagination-total-text').first();
}
private sortTableWrapper(): Locator {
return this.report.locator('.ant-table-wrapper').nth(this.sortTableWrapperIndex);
}
......@@ -257,17 +275,16 @@ export class CenterReportSharedLocators {
let chain: Locator | undefined;
const append = (loc: Locator) => {
chain = chain ? chain.or(loc) : loc;
const unique = loc.first();
chain = chain ? chain.or(unique) : unique;
};
append(thead.locator('th:visible').filter({ has: thead.getByText(columnName, { exact: true }) }));
append(withSorter(thead.locator('th:visible').filter({ has: thead.getByText(columnName, { exact: true }) })));
append(thead.locator('th').filter({ has: thead.getByText(columnName, { exact: true }) }));
append(withSorter(thead.locator('th').filter({ has: thead.getByText(columnName, { exact: true }) })));
append(
withSorter(
thead.locator('th:visible').filter({
has: this.report
.locator('.ant-table-column-title')
.getByText(columnName, { exact: true }),
thead.locator('th').filter({
has: thead.locator('.ant-table-column-title').getByText(columnName, { exact: true }),
}),
),
);
......@@ -283,11 +300,11 @@ export class CenterReportSharedLocators {
for (const token of tokens) {
const normalizedFull = columnName.replace(/\s+/g, '');
if (token === normalizedFull) continue;
append(withSorter(thead.locator('th:visible').filter({ hasText: token })));
append(withSorter(thead.locator('th').filter({ hasText: token })));
append(
withSorter(
thead.locator('th:visible').filter({
has: this.report.locator('.ant-table-column-title').filter({ hasText: token }),
thead.locator('th').filter({
has: thead.locator('.ant-table-column-title').filter({ hasText: token }),
}),
),
);
......@@ -347,11 +364,11 @@ export class CenterReportSharedLocators {
}
paginationItem(pageNum: number): Locator {
return this.report.locator(`.ant-pagination-item-${pageNum}`);
return this.paginationHost().locator(`.ant-pagination-item-${pageNum}`);
}
paginationActiveItem(): Locator {
return this.report.locator('.ant-pagination-item-active');
return this.paginationHost().locator('.ant-pagination-item-active').first();
}
expandedSubTableHeader(name: string): Locator {
......@@ -359,7 +376,9 @@ export class CenterReportSharedLocators {
}
subTableRow(rowIndex = 0): Locator {
return this.expandedRow.locator('.ant-table tbody tr').nth(rowIndex);
return this.expandedRow
.locator('.ant-table tbody tr.ant-table-row:not(.ant-table-measure-row)')
.nth(rowIndex);
}
subTableRowCheckbox(rowIndex = 0): Locator {
......@@ -368,10 +387,14 @@ export class CenterReportSharedLocators {
subTableViewButton(rowIndex = 0): Locator {
const row = this.subTableRow(rowIndex);
return row
.getByRole('button', { name: '查看' })
.or(row.getByRole('link', { name: '查看' }))
.or(row.locator('a, span, .ant-btn').filter({ hasText: /^查看$/ }))
.first();
const fixedRightRow = this.expandedRow
.locator('.ant-table-fixed-right tbody tr.ant-table-row:not(.ant-table-measure-row)')
.nth(rowIndex);
const inRow = (host: Locator) =>
host
.getByRole('button', { name: /查\s*看/ })
.or(host.getByRole('link', { name: /查\s*看/ }))
.or(host.locator('a, button, span, .ant-btn').filter({ hasText: /查\s*看/ }));
return inRow(fixedRightRow).or(inRow(row)).first();
}
}
......@@ -81,18 +81,41 @@ export class CenterReportSheetLocators {
}
tableHeaderCells(): Locator {
return this.report.locator('.ant-table-wrapper').first().locator('.ant-table-thead tr').last().locator('th');
return this.visibleThead().locator('tr').last().locator('th');
}
/**
* 可见 sticky 表头。宽表会在 body / fixed 区再克隆 thead,
* `.ant-table-thead tr`.last() 会命中测宽空行或克隆列,waitFor 触发 strict mode。
*/
visibleThead(): Locator {
const wrapper = this.report.locator('.ant-table-wrapper').first();
return wrapper
.locator('.ant-table-container > .ant-table-header .ant-table-thead')
.or(wrapper.locator('.ant-table-header .ant-table-thead'))
.or(wrapper.locator('.ant-table-content > table > .ant-table-thead'))
.or(wrapper.locator('.ant-table-thead'))
.first();
}
/** 指定列的表头单元格(可见列 + 含排序控件,规避固定列重复表头)。 */
/** 指定列的表头单元格;每条分支先 .first(),避免 or() 在多匹配 waitFor 时 strict mode。 */
columnHeaderCell(columnName: string): Locator {
const thead = this.report.locator('.ant-table-wrapper').first().locator('.ant-table-thead tr').last();
const thead = this.visibleThead().locator('tr').last();
const byTitle = thead
.locator('th:visible')
.filter({ has: this.report.locator('.ant-table-column-title').getByText(columnName, { exact: true }) })
.filter({ has: this.report.locator('.ant-table-column-sorter') });
const byRole = this.report.getByRole('columnheader', { name: columnName, exact: true });
return byTitle.or(byRole).first();
.locator('th')
.filter({ has: thead.locator('.ant-table-column-title').getByText(columnName, { exact: true }) })
.first();
const bySorter = thead
.locator('th')
.filter({ has: thead.locator('.ant-table-column-title').getByText(columnName, { exact: true }) })
.filter({ has: thead.locator('.ant-table-column-sorter') })
.first();
const byRole = this.report
.locator('.ant-table-wrapper')
.first()
.getByRole('columnheader', { name: columnName, exact: true })
.first();
return byTitle.or(bySorter).or(byRole).first();
}
/** 指定列的排序按钮(▲▼ 区域,见 ant-table-column-sorter)。 */
......
import { expect, type Page } from '@playwright/test';
import { anaChgData } from '../../data/ana-chg.data';
import {
expectNumbersClose,
parseReportNumber,
} from './helpers/report-summary.helper';
import { readSummaryMetrics } from './helpers/report-summary-match.helper';
import { ReconCostReportSharedPage } from './recon-cost-report-shared.page';
export class AnaChgPage extends ReconCostReportSharedPage {
......@@ -17,35 +12,15 @@ export class AnaChgPage extends ReconCostReportSharedPage {
* STAT-ACH-007:勾选首行后,明细「数量/总差额」与底部「选中总数量/选中总差额」一致。
*/
async expectSelectedSummaryMatchesFirstRow(rowIndex = this.data.sampleRowIndex): Promise<void> {
const cfg = this.data.firstRowSummaryMatch;
expect(cfg, '未配置 firstRowSummaryMatch,无法做选中汇总勾稽').toBeTruthy();
if (!cfg) return;
await this.waitForTableReady();
const rowCount = await this.locators.dataRows.count();
expect(rowCount, '主表应至少有 1 条数据以便勾选').toBeGreaterThan(0);
expect(await this.locators.dataRows.count(), '主表应至少有 1 条数据以便勾选').toBeGreaterThan(0);
await this.checkMainTableRow(rowIndex);
const sample = await this.extractSampleRow(rowIndex);
const expectedQty = parseReportNumber(this.getSampleValue(sample, 'field8'));
const expectedDiff = parseReportNumber(this.getSampleValue(sample, 'field9'));
await expect
.poll(
async () => {
const summary = await readSummaryMetrics(this.locators.report.locator('body'), [
'选中总数量',
'选中总差额',
]);
return Math.abs(summary['选中总数量'] - expectedQty) <= 0.01 * Math.max(Math.abs(expectedQty), 1)
&& Math.abs(summary['选中总差额'] - expectedDiff) <= 0.01 * Math.max(Math.abs(expectedDiff), 1);
},
{ timeout: 15_000, message: '勾选后底部选中汇总未与明细数量/总差额一致' },
)
.toBe(true);
const summary = await readSummaryMetrics(this.locators.report.locator('body'), [
'选中总数量',
'选中总差额',
]);
expectNumbersClose(summary['选中总数量'], expectedQty, 0.01, '选中总数量');
expectNumbersClose(summary['选中总差额'], expectedDiff, 0.01, '选中总差额');
await this.assertVisibleRowsMatchFooter(cfg.metrics, { rowIndexes: [rowIndex] });
}
}
......
......@@ -11,9 +11,10 @@ import { BasePage } from '../../core/base.page';
import { columnHeaderMatchTokens } from '../../core/column-header-match.util';
import { CenterReportEntryLocators } from '../../locators/spd/center-report-entry.locator';
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';
......@@ -68,7 +69,7 @@ export class CenterReportEntryPage 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),
......@@ -668,8 +669,20 @@ export class CenterReportEntryPage extends BasePage {
async clickSubTableViewDetail(rowIndex = 0): Promise<string> {
const orderNo = await this.getSubTableInboundOrderNo(rowIndex);
const expanded = this.locators.expandedRow;
const scrollHosts = expanded.locator('.ant-table-body, .ant-table-content, .ant-table-scroll');
const hostCount = await scrollHosts.count();
for (let i = 0; i < hostCount; i += 1) {
await scrollHosts
.nth(i)
.evaluate((el) => {
(el as HTMLElement).scrollLeft = (el as HTMLElement).scrollWidth;
})
.catch(() => undefined);
}
const viewBtn = this.locators.subTableViewButton(rowIndex);
await expect(viewBtn).toBeVisible();
await viewBtn.scrollIntoViewIfNeeded().catch(() => undefined);
await expect(viewBtn, '子表「查看」按钮应可见').toBeVisible({ timeout: 10_000 });
await this.click(viewBtn, '点击子表查看');
return orderNo;
}
......@@ -888,19 +901,43 @@ export class CenterReportEntryPage extends BasePage {
async isColumnSortable(columnName: string): Promise<boolean> {
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);
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 sortable;
return hasSorter;
}
async clickColumnSort(columnName: string): Promise<void> {
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);
}
......@@ -940,26 +977,32 @@ export class CenterReportEntryPage 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: centerReportEntryData.horizontalScrollTargetColumn,
horizontalScrollDragDelta: centerReportEntryData.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: centerReportEntryData.horizontalScrollTargetColumn,
horizontalScrollDragDelta: centerReportEntryData.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> {
......@@ -1010,6 +1053,19 @@ export class CenterReportEntryPage extends BasePage {
return centerReportEntryData.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 expectPaginationTotal(): Promise<void> {
await expect(this.locators.paginationTotal).toHaveText(/共\s+\d+\s+条/);
}
......
......@@ -18,9 +18,10 @@ import {
} from '../../locators/spd/center-report-shared.locator';
import { retryReportLoad } from './helpers/report-load-retry.helper';
import { DrawerFormControls } from './helpers/drawer-form-controls.helper';
import { ColumnSortControls, isColumnValuesSorted, type ColumnSortOrder } from './helpers/column-sort.helper';
import { ColumnSortControls, isColumnValuesSorted, clickColumnSortControl, readColumnSortOrder, type ColumnSortOrder } from './helpers/column-sort.helper';
import {
ColumnSettingsControls,
executeColumnSettingsToggleRestore,
findUsableColumnSettingsTrigger,
revealColumnSettingsTriggerByScroll as scrollToRevealColumnSettingsTrigger,
} from './helpers/column-settings.helper';
......@@ -104,6 +105,14 @@ export class CenterReportSharedPage extends BasePage {
* 未列入的列无控件则 skip。
*/
async expectColumnSortCase(columnName: string, tableWrapperIndex?: number): Promise<void> {
const prereqs = this.data.sortColumnPrerequisites?.[columnName];
if (prereqs?.length) {
for (const label of prereqs) {
await this.checkDimension(label);
}
await this.search();
await this.waitForTableReady().catch(() => undefined);
}
const required = (this.data.sortColumns ?? []).includes(columnName);
const sortable = await this.isColumnSortable(columnName, tableWrapperIndex);
if (!sortable) {
......@@ -137,7 +146,11 @@ export class CenterReportSharedPage extends BasePage {
protected async assertComboQueryHasMatchingRows(_rowCount?: number): Promise<void> {
await this.waitForTableReady().catch(() => undefined);
const count = await this.countVisibleDataRows();
let count = await this.countVisibleDataRows();
if (count > 0) return;
await this.search();
await this.waitForTableReady().catch(() => undefined);
count = await this.countVisibleDataRows();
if (count > 0) return;
expect(
this.hadInitialListDataForCombo,
......@@ -170,7 +183,7 @@ export class CenterReportSharedPage 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),
......@@ -179,10 +192,18 @@ export class CenterReportSharedPage extends BasePage {
this.columnSettingsControls = new ColumnSettingsControls({
columnSettingsPanel: () => this.locators.columnSettingsPanel(),
columnSettingsTrigger: this.locators.columnSettingsTrigger,
resolveTrigger: async () =>
this.resolveColumnSettingsTrigger(this.defaultColumnSettingsTableIndex()),
resolveTrigger: async () => {
const idx = this.defaultColumnSettingsTableIndex();
const found = await findUsableColumnSettingsTrigger(
this.columnSettingsTriggerCandidates(idx),
);
if (found) {
return found;
}
return this.resolveColumnSettingsTrigger(idx);
},
reportBody: this.locators.report.locator('body'),
clickOutsideTarget: this.locators.searchBtn,
clickOutsideTarget: this.locators.productName,
tableClickTarget: this.locators.tableBody,
});
this.tableScrollLayoutControls = new TableScrollLayoutControls({
......@@ -208,7 +229,7 @@ export class CenterReportSharedPage extends BasePage {
this.summaryMatchControls = data.summaryMatch
? new ReportSummaryMatchControls({
profile: data.summaryMatch,
reportBody: this.locators.report.locator('body'),
reportBody: this.locators.report,
dataRows: this.locators.dataRows,
paginationTotal: this.locators.paginationTotal,
sampleRowIndex: data.sampleRowIndex,
......@@ -419,7 +440,20 @@ export class CenterReportSharedPage extends BasePage {
}
protected async getTableRowCount(): Promise<number> {
return this.locators.dataRows.count();
return this.paginationDataRows().count();
}
/** 当前分页绑定表的数据行(双表页右侧分页读明细行)。 */
protected paginationDataRows(): Locator {
const idx = this.locators.paginationTableWrapperIndex;
if (
this.data.detailTableWrapperIndex !== undefined &&
idx === this.data.detailTableWrapperIndex &&
idx !== (this.data.tableWrapperIndex ?? 0)
) {
return this.locators.detailDataRows;
}
return this.locators.dataRows;
}
protected async readPaginationTotalCount(): Promise<number> {
......@@ -1223,18 +1257,34 @@ export class CenterReportSharedPage extends BasePage {
async getFirstRowText(): Promise<string> {
await this.waitForTableReady();
await expect
.poll(async () => this.locators.dataRows.count(), { timeout: 20_000 })
.poll(async () => this.paginationDataRows().count(), { timeout: 20_000 })
.toBeGreaterThan(0);
await expect(this.locators.dataRows.first()).toBeVisible({ timeout: 15_000 });
return (await this.locators.dataRows.first().innerText()).trim();
const row = await this.firstVisibleDataRow();
await row.scrollIntoViewIfNeeded().catch(() => undefined);
const text = ((await row.innerText().catch(() => '')) ?? '').trim();
expect(text, '列表首行应有文本').toBeTruthy();
return text;
}
protected async firstVisibleDataRow(): Promise<Locator> {
const rows = this.paginationDataRows();
const count = await rows.count();
for (let i = 0; i < Math.min(count, 20); i += 1) {
const row = rows.nth(i);
if (await row.isVisible().catch(() => false)) {
return row;
}
}
return rows.first();
}
async expectFirstRowContains(keyword: string | RegExp): Promise<void> {
if (typeof keyword === 'string' && isDateRangeFilterValue(keyword)) {
return;
}
await expect(this.locators.dataRows.first()).toBeVisible({ timeout: 15_000 });
await expect(this.locators.dataRows.first()).toContainText(keyword);
const row = await this.firstVisibleDataRow();
await row.scrollIntoViewIfNeeded().catch(() => undefined);
await expect(row).toContainText(keyword);
}
async expandFirstRow(): Promise<void> {
......@@ -1283,9 +1333,9 @@ export class CenterReportSharedPage extends BasePage {
hits.length,
`子表缺少列,期望之一:${required.join('')},实际表头:${headerText}`,
).toBeGreaterThan(0);
const viewBtn = this.locators.subTableViewButton(0);
const viewBtn = await this.revealSubTableViewButton(0).catch(() => this.locators.subTableViewButton(0));
if ((await viewBtn.count()) > 0) {
await expect(viewBtn).toBeVisible();
await expect(viewBtn).toBeVisible({ timeout: 10_000 });
}
}
......@@ -1313,15 +1363,17 @@ export class CenterReportSharedPage extends BasePage {
}
/** 场景1:切换至第2页标准流程(数据不足 skip,否则校验两页首行不同)。 */
async executePage2SwitchTest(): Promise<void> {
await this.skipIfInsufficientForPage2();
const first = await this.getFirstRowText();
await this.goToPage(2);
await this.expectActivePage(2);
const second = await this.getFirstRowText();
expect(first, '第1页首行应有数据').toBeTruthy();
expect(second, '第2页首行应有数据').toBeTruthy();
expect(second).not.toEqual(first);
async executePage2SwitchTest(tableWrapperIndex?: number): Promise<void> {
return this.withPaginationTableIndex(tableWrapperIndex, async () => {
await this.skipIfInsufficientForPage2();
const first = await this.getFirstRowText();
await this.goToPage(2);
await this.expectActivePage(2);
const second = await this.getFirstRowText();
expect(first, '第1页首行应有数据').toBeTruthy();
expect(second, '第2页首行应有数据').toBeTruthy();
expect(second).not.toEqual(first);
});
}
async goToPage(pageNum: number): Promise<void> {
......@@ -1379,15 +1431,21 @@ export class CenterReportSharedPage extends BasePage {
fromLabel: string,
toLabel: string,
maxRows: number,
options?: { requireMoreThanOnePage?: boolean },
options?: { requireMoreThanOnePage?: boolean; tableWrapperIndex?: number },
): Promise<void> {
await this.skipIfInsufficientForPageSizeChange(options);
await this.changePageSize(fromLabel, toLabel);
await this.expectVisibleRowCountAtMost(maxRows);
return this.withPaginationTableIndex(options?.tableWrapperIndex, async () => {
await this.skipIfInsufficientForPageSizeChange(options);
await this.changePageSize(fromLabel, toLabel);
await this.expectVisibleRowCountAtMost(maxRows);
});
}
async changePageSize(fromLabel: string, toLabel: string): Promise<void> {
const changer = this.locators.report.locator('.ant-pagination-options-size-changer').first();
const changer = this.locators
.paginationHost()
.locator('.ant-pagination-options-size-changer')
.first()
.or(this.locators.report.locator('.ant-pagination-options-size-changer').nth(this.locators.paginationTableWrapperIndex));
if ((await changer.count()) === 0) {
await this.guardedSkip('当前页面无每页条数切换');
}
......@@ -1996,8 +2054,15 @@ export class CenterReportSharedPage extends BasePage {
return sample;
}
/** 组合查询后页面无错误提示。 */
/** 组合查询后页面无错误提示。瞬时「未知错误」先关 toast 再查一次。 */
async expectQueryWithoutError(): Promise<void> {
if (await this.hasVisibleErrorNotice()) {
const transient = await this.isTransientUnknownError();
if (transient) {
await this.dismissVisibleErrorNotice();
await this.search();
}
}
expect(await this.hasVisibleErrorNotice(), '查询后页面出现错误提示').toBeFalsy();
}
......@@ -2176,6 +2241,23 @@ export class CenterReportSharedPage extends BasePage {
}
}
/** 临时切换分页目标表,避免左右双表页码互相干扰。 */
protected async withPaginationTableIndex<T>(
tableWrapperIndex: number | undefined,
fn: () => Promise<T>,
): Promise<T> {
if (tableWrapperIndex === undefined) {
return fn();
}
const prev = this.locators.paginationTableWrapperIndex;
this.locators.bindPaginationTable(tableWrapperIndex);
try {
return await fn();
} finally {
this.locators.bindPaginationTable(prev);
}
}
/** 按表头文本+排序图标精确定位列索引(排除空表头/无 sorter 列)。 */
protected async headerHasSortControl(header: Locator): Promise<boolean> {
const sorterCount = await header.locator('.ant-table-column-sorter').count().catch(() => 0);
......@@ -2214,7 +2296,8 @@ export class CenterReportSharedPage extends BasePage {
}
if (index >= 0) {
const headerAtIndex = this.locators.sortColumnHeaderAt(index);
if (await this.headerHasSortControl(headerAtIndex)) {
const text = ((await headerAtIndex.innerText().catch(() => '')) ?? '').replace(/\s+/g, ' ').trim();
if (scoreColumnHeaderMatch(text, columnName) > 0 && (await this.headerHasSortControl(headerAtIndex))) {
return index;
}
}
......@@ -2253,6 +2336,16 @@ export class CenterReportSharedPage extends BasePage {
);
const header = this.locators.sortColumnHeaderAt(columnIndex);
const resolved = ((await header.innerText().catch(() => '')) ?? '').replace(/\s+/g, ' ').trim();
if (!resolved) {
const retried = await this.findSortColumnHeaderIndex(columnName);
const retryHeader = this.locators.sortColumnHeaderAt(retried);
const retryText = ((await retryHeader.innerText().catch(() => '')) ?? '').replace(/\s+/g, ' ').trim();
expect(
scoreColumnHeaderMatch(retryText, columnName),
`表头定位偏差:期望「${columnName}」,实际「${retryText}」`,
).toBeGreaterThan(0);
return;
}
expect(
scoreColumnHeaderMatch(resolved, columnName),
`表头定位偏差:期望「${columnName}」,实际「${resolved}」`,
......@@ -2284,19 +2377,23 @@ export class CenterReportSharedPage extends BasePage {
});
}
async clickColumnSort(columnName: string): Promise<void> {
async clickColumnSort(columnName: string, order?: 'ascend' | 'descend'): Promise<void> {
await this.scrollColumnIntoView(columnName);
const headerCell = this.locators.columnHeaderCell(columnName);
const columnIndex = await this.findSortColumnHeaderIndex(columnName);
const header = (await headerCell.count()) > 0 ? headerCell : this.locators.sortColumnHeaderAt(columnIndex);
const sorter = this.locators.columnSorter(columnName);
await this.waitVisible(header, 15_000);
await test.step(`点击${columnName}列排序`, async () => {
if ((await sorter.count()) > 0 && (await sorter.isVisible().catch(() => false))) {
await sorter.click({ force: true });
} else {
await header.click({ force: true });
}
const header = this.locators.sortColumnHeaderAt(columnIndex);
const sorter = this.locators.sortColumnSorterAt(columnIndex);
const up = header.locator('.ant-table-column-sorter-up').first();
const down = header.locator('.ant-table-column-sorter-down').first();
await header.waitFor({ state: 'attached', timeout: 15_000 });
const label = order === 'descend' ? '降序' : order === 'ascend' ? '升序' : '排序';
await test.step(`点击${columnName}${label}`, async () => {
await clickColumnSortControl({
header,
sorter,
up,
down,
order,
});
await this.waitForSortDone();
});
}
......@@ -2309,61 +2406,29 @@ export class CenterReportSharedPage extends BasePage {
await this.locators.tableBody.waitFor({ state: 'visible', timeout: 15_000 }).catch(() => undefined);
await this.waitForSearchLoadingDone().catch(() => undefined);
await waitAntSpinGone(this.locators.report.locator('.ant-table-wrapper').first());
if (await this.isTransientUnknownError()) {
await this.dismissVisibleErrorNotice();
}
}
private async readSortOrderAt(columnIndex: number): Promise<ColumnSortOrder> {
const header = this.locators.sortColumnHeaderAt(columnIndex);
const ariaSort = await header.getAttribute('aria-sort');
if (ariaSort === 'ascending') return 'ascend';
if (ariaSort === 'descending') return 'descend';
const upActive = await header
.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 header
.locator('.ant-table-column-sorter-down')
.evaluate((el) => el.classList.contains('active') || el.classList.contains('on'))
.catch(() => false);
if (downActive) return 'descend';
const thSorted = await header
.evaluate((th) => (th as HTMLElement).classList.contains('ant-table-column-sort'))
.catch(() => false);
if (thSorted) return 'ascend';
return 'none';
return readColumnSortOrder(this.locators.sortColumnHeaderAt(columnIndex));
}
private async clickSortUntilOrder(
columnIndex: number,
columnName: string,
target: ColumnSortOrder,
target: 'ascend' | 'descend',
): Promise<void> {
const header = this.locators.columnHeaderCell(columnName);
const headerAtIndex = this.locators.sortColumnHeaderAt(columnIndex);
const clickTarget =
(await header.count()) > 0 ? header : headerAtIndex;
const sorter = this.locators.columnSorter(columnName);
const sorterAtIndex = this.locators.sortColumnSorterAt(columnIndex);
await expect(clickTarget, `列「${columnName}」表头不可见`).toBeVisible({ timeout: 15_000 });
await headerAtIndex.scrollIntoViewIfNeeded().catch(() => undefined);
await headerAtIndex.waitFor({ state: 'attached', timeout: 15_000 });
for (let attempt = 0; attempt < 4; attempt += 1) {
for (let attempt = 0; attempt < 6; attempt += 1) {
if ((await this.readSortOrderAt(columnIndex)) === target) {
return;
}
await test.step(`点击${columnName}列排序(第 ${attempt + 1} 次)`, async () => {
if ((await sorter.count()) > 0 && (await sorter.isVisible().catch(() => false))) {
await sorter.click({ force: true });
} else if ((await sorterAtIndex.count()) > 0 && (await sorterAtIndex.isVisible().catch(() => false))) {
await sorterAtIndex.click({ force: true });
} else {
await clickTarget.click({ force: true });
}
await this.waitForSortDone();
});
await this.clickColumnSort(columnName, target);
}
expect(await this.readSortOrderAt(columnIndex), `列「${columnName}」未能切换到${target === 'ascend' ? '升序' : '降序'}`).toBe(
target,
......@@ -3220,8 +3285,14 @@ export class CenterReportSharedPage extends BasePage {
});
}
/** 解析列设置齿轮(多策略 + 横向滚动 + 等待工具条渲染)。 */
/** 解析列设置齿轮(齿轮已在视口则跳过整表横滚)。 */
protected async resolveColumnSettingsTrigger(tableWrapperIndex: number): Promise<Locator> {
const already = await findUsableColumnSettingsTrigger(
this.columnSettingsTriggerCandidates(tableWrapperIndex),
);
if (already) {
return already;
}
await this.revealColumnSettingsTriggerByScroll(tableWrapperIndex);
let trigger = this.columnSettingsTriggerCandidates(tableWrapperIndex)[0]!;
let scrollPasses = 0;
......@@ -3244,27 +3315,27 @@ export class CenterReportSharedPage extends BasePage {
return trigger;
}
/** 面板仍可见时强制关闭(Escape / 点外侧 / 再点齿轮)。 */
/** 面板仍可见时强制关闭(Escape / 点产品名称 / 再点齿轮;禁止点查询)。 */
protected async forceDismissColumnSettingsPanel(tableWrapperIndex: number): Promise<void> {
const panel = this.locators.columnSettingsPanel();
if (!(await panel.isVisible().catch(() => false))) {
return;
}
await test.step(`强制关闭列设置(table=${tableWrapperIndex})`, async () => {
for (let i = 0; i < 5; i += 1) {
for (let i = 0; i < 4; i += 1) {
if (!(await panel.isVisible().catch(() => false))) {
break;
}
await this.locators.report.locator('body').press('Escape').catch(() => undefined);
await this.page.keyboard.press('Escape').catch(() => undefined);
await this.locators.searchBtn.click({ force: true }).catch(() => undefined);
await this.locators.productName.click({ force: true, timeout: 3_000 }).catch(() => undefined);
for (const loc of this.columnSettingsTriggerCandidates(tableWrapperIndex)) {
if ((await loc.count()) > 0) {
await loc.first().click({ force: true }).catch(() => undefined);
await loc.first().click({ force: true, timeout: 3_000 }).catch(() => undefined);
break;
}
}
await panel.waitFor({ state: 'hidden', timeout: 2_000 }).catch(() => undefined);
await panel.waitFor({ state: 'hidden', timeout: 1_000 }).catch(() => undefined);
}
});
}
......@@ -3273,12 +3344,8 @@ export class CenterReportSharedPage extends BasePage {
async openColumnSettingsForTable(tableWrapperIndex: number): Promise<void> {
const panel = this.locators.columnSettingsPanel();
if (await panel.isVisible().catch(() => false)) {
await this.closeColumnSettingsForTable(tableWrapperIndex);
}
if (await panel.isVisible().catch(() => false)) {
await this.forceDismissColumnSettingsPanel(tableWrapperIndex);
return;
}
await panel.waitFor({ state: 'hidden', timeout: 3_000 }).catch(() => undefined);
const trigger = await this.resolveColumnSettingsTrigger(tableWrapperIndex);
await test.step(`打开列设置(table=${tableWrapperIndex})`, async () => {
await trigger.scrollIntoViewIfNeeded().catch(() => undefined);
......@@ -3300,17 +3367,19 @@ export class CenterReportSharedPage extends BasePage {
return;
}
await test.step(`关闭列设置(table=${tableWrapperIndex})`, async () => {
for (let i = 0; i < 6; i += 1) {
await this.page.keyboard.press('Escape').catch(() => undefined);
await this.locators.report.locator('body').press('Escape').catch(() => undefined);
for (let i = 0; i < 4; i += 1) {
if (!(await panel.isVisible().catch(() => false))) {
break;
}
for (const loc of this.columnSettingsTriggerCandidates(tableWrapperIndex)) {
if ((await loc.count()) > 0) {
await loc.first().click({ force: true }).catch(() => undefined);
await loc.first().click({ force: true, timeout: 3_000 }).catch(() => undefined);
break;
}
}
await panel.waitFor({ state: 'hidden', timeout: 2_000 }).catch(() => undefined);
await panel.waitFor({ state: 'hidden', timeout: 1_000 }).catch(() => undefined);
}
if (await panel.isVisible().catch(() => false)) {
await this.forceDismissColumnSettingsPanel(tableWrapperIndex);
......@@ -3397,16 +3466,20 @@ export class CenterReportSharedPage extends BasePage {
.filter({ hasText: headerLabel });
if (visible) {
const isShown = async () =>
(await sorterOrLeaf.first().isVisible().catch(() => false)) ||
(await anyHeader.first().isVisible().catch(() => false));
await this.scrollTableColumnIntoView(headerLabel).catch(() => undefined);
await expect
.poll(
async () => {
if (await isShown()) {
return true;
}
await this.scrollTableColumnIntoView(headerLabel).catch(() => undefined);
return (
(await sorterOrLeaf.first().isVisible().catch(() => false)) ||
(await anyHeader.first().isVisible().catch(() => false))
);
return isShown();
},
{ timeout: 15_000 },
{ timeout: 15_000, intervals: [500, 1_000, 2_000] },
)
.toBeTruthy();
return;
......@@ -3441,25 +3514,8 @@ export class CenterReportSharedPage extends BasePage {
async pickToggleColumnNameForTable(tableWrapperIndex: number): Promise<string> {
const panel = this.locators.columnSettingsPanel();
const trigger = await this.resolveColumnSettingsTrigger(tableWrapperIndex);
const wasOpen = await panel.isVisible().catch(() => false);
if (!wasOpen) {
await trigger.click({ force: true });
await panel.waitFor({ state: 'visible', timeout: 15_000 });
}
const finishPick = async (name: string): Promise<string> => {
if (!wasOpen) {
for (let i = 0; i < 4; i += 1) {
if (!(await panel.isVisible().catch(() => false))) break;
await trigger.click({ force: true });
}
}
return name;
};
await this.openColumnSettingsForTable(tableWrapperIndex);
const mainIdx = this.data.tableWrapperIndex ?? 0;
const detailIdx = this.data.detailTableWrapperIndex ?? mainIdx;
const candidates =
tableWrapperIndex === mainIdx
? (this.data.leftColumnSettingsToggleCandidates ?? this.data.columnSettingsToggleCandidates ?? [])
......@@ -3490,7 +3546,7 @@ export class CenterReportSharedPage extends BasePage {
.catch(() => false));
if (!leafVisible) return undefined;
}
return finishPick(name);
return name;
};
for (const name of candidates) {
......@@ -3516,7 +3572,7 @@ export class CenterReportSharedPage extends BasePage {
if (Object.keys(assertMap).length === 0) {
const serialRow = this.locators.columnSettingsCheckboxRow('序号列');
if (await serialRow.isVisible().catch(() => false)) {
return await finishPick('序号列');
return '序号列';
}
}
......@@ -3533,6 +3589,28 @@ export class CenterReportSharedPage extends BasePage {
return '序号列';
}
/** 场景:取消勾选非核心列后表头隐藏,再勾选后表头恢复。 */
async executeColumnSettingsToggleRestoreTest(tableWrapperIndex?: number): Promise<void> {
const idx = tableWrapperIndex ?? this.defaultColumnSettingsTableIndex();
await executeColumnSettingsToggleRestore({
pickToggleColumnName: () => this.pickToggleColumnName(idx),
openColumnSettings: () => this.openColumnSettings(idx),
setColumnVisibleInSettings: (name, visible) => this.setColumnVisibleInSettings(name, visible),
closeColumnSettingsPanel: () => this.closeColumnSettingsPanel(idx),
expectTableHeaderVisible: (name, visible) =>
tableWrapperIndex === undefined
? this.expectTableHeaderVisible(name, visible)
: this.expectTableHeaderVisibleInTable(name, visible, idx),
expectSettingsCheckboxChecked: (name, checked) =>
this.expectSettingsCheckboxChecked(name, checked),
});
}
/** 双表页按 tableWrapperIndex 执行列设置恢复。 */
async executeColumnSettingsToggleRestoreForTable(tableWrapperIndex: number): Promise<void> {
await this.executeColumnSettingsToggleRestoreTest(tableWrapperIndex);
}
async getSubTableOrderNo(rowIndex = 0): Promise<string> {
const keywords = this.data.subTableOrderNoKeywords ?? ['单号'];
const subTable = this.locators.expandedRow.locator('.ant-table').first();
......@@ -3563,6 +3641,34 @@ export class CenterReportSharedPage extends BasePage {
return false;
}
private errorNoticeLocators() {
return [
this.page.locator('.ant-message-error'),
this.page.locator('.ant-notification-notice-error'),
this.locators.report.locator('.ant-message-error'),
this.locators.report.locator('.ant-notification-notice-error'),
];
}
protected async isTransientUnknownError(): Promise<boolean> {
for (const locator of this.errorNoticeLocators()) {
if ((await locator.count()) === 0) continue;
const text = ((await locator.first().innerText().catch(() => '')) ?? '').replace(/\s+/g, '');
if (/未知错误|请重试/.test(text)) return true;
}
return false;
}
protected async dismissVisibleErrorNotice(): Promise<void> {
for (const locator of this.errorNoticeLocators()) {
const closer = locator.locator('.anticon-close, .ant-notification-notice-close').first();
if ((await closer.count()) > 0) {
await closer.click({ force: true, timeout: 2_000 }).catch(() => undefined);
}
}
await this.page.keyboard.press('Escape').catch(() => undefined);
}
private async isOrderNoVisibleOnDetailSurfaces(orderNo: string): Promise<boolean> {
const popupPages = this.page.context().pages().filter((p) => p !== this.page);
const surfaces: Array<{ getByText: (text: string, options?: { exact: boolean }) => Locator }> = [
......@@ -3631,31 +3737,52 @@ export class CenterReportSharedPage extends BasePage {
}
async openSubTableViewDetail(rowIndex = 0): Promise<void> {
const viewBtn = this.locators.subTableViewButton(rowIndex);
let actionBtn = viewBtn;
if ((await viewBtn.count()) === 0 || !(await viewBtn.isVisible().catch(() => false))) {
const link = this.locators.expandedRow
.locator('a, button, span')
.filter({ hasText: /^查看$/ })
.first();
if ((await link.count()) > 0 && (await link.isVisible().catch(() => false))) {
actionBtn = link;
} else {
await this.guardedSkip('子表无「查看」按钮,无法跳转详情');
}
}
const actionBtn = await this.revealSubTableViewButton(rowIndex);
const tabCountBefore = await this.page.locator('.ant-tabs-tab').count();
const urlBefore = this.page.url();
const popupPromise = this.page.waitForEvent('popup', { timeout: 20_000 }).catch(() => null);
const orderNo = await this.getSubTableOrderNo(rowIndex);
await expect(actionBtn).toBeVisible();
await expect(actionBtn, '子表「查看」按钮应可见').toBeVisible({ timeout: 10_000 });
await this.click(actionBtn, '点击子表查看');
const popup = await popupPromise;
await this.expectViewDetailNavigation(orderNo, { tabCountBefore, urlBefore, popup });
}
/** 子表明细过宽时「查看」可能在视口外,先横向滚到操作列。 */
protected async revealSubTableViewButton(rowIndex = 0): Promise<Locator> {
const expanded = this.locators.expandedRow;
await expect(expanded, '应已展开子表').toBeVisible({ timeout: 15_000 });
const scrollHosts = expanded.locator('.ant-table-body, .ant-table-content, .ant-table-scroll');
const hostCount = await scrollHosts.count();
for (let i = 0; i < hostCount; i += 1) {
await scrollHosts
.nth(i)
.evaluate((el) => {
(el as HTMLElement).scrollLeft = (el as HTMLElement).scrollWidth;
})
.catch(() => undefined);
}
const row = this.locators.subTableRow(rowIndex);
await row.scrollIntoViewIfNeeded().catch(() => undefined);
const viewBtn = this.locators.subTableViewButton(rowIndex);
await viewBtn.scrollIntoViewIfNeeded().catch(() => undefined);
if ((await viewBtn.count()) > 0 && (await viewBtn.isVisible().catch(() => false))) {
return viewBtn;
}
const fallback = expanded
.locator('a, button, span, .ant-btn')
.filter({ hasText: /查\s*看/ })
.nth(rowIndex);
await fallback.scrollIntoViewIfNeeded().catch(() => undefined);
if ((await fallback.count()) > 0 && (await fallback.isVisible().catch(() => false))) {
return fallback;
}
await this.guardedSkip('子表无「查看」按钮,无法跳转详情');
return viewBtn;
}
/** 子表「查看」:详情可能以弹窗/抽屉/新 Tab 打开,放宽导航断言。 */
async openSubTableViewDetailSoft(rowIndex = 0): Promise<void> {
const viewBtn = this.locators.subTableViewButton(rowIndex);
......
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