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+条/);
}
......
This diff is collapsed.
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