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