Commit 89d08aa0 authored by liguangyu06's avatar liguangyu06
Browse files

修复 Jenkins #33 聚类失败:加固报表 PO、准入/资质定位与 mock 配置

针对列排序/列设置、组合查空、reuse-open 残留 toast、追溯 iframe 与 mock 反例等公共路径加固,并对齐现网筛选项与 PSI-005 数据驱动 mock。
parent 794a77fd
...@@ -20,7 +20,7 @@ export const admitData = { ...@@ -20,7 +20,7 @@ export const admitData = {
mockOrder: '__NOT_EXIST_ORDER__99999', mockOrder: '__NOT_EXIST_ORDER__99999',
/** 列表/筛选列名(现网多为「物料名称」)。 */ /** 列表/筛选列名(现网多为「物料名称」)。 */
listNameColumn: '物料名称', listNameColumn: '物料名称',
listNameColumnAliases: ['物料名称', '产品名称', '物资名称'] as const, listNameColumnAliases: ['物料名称', '产品名称', '物资名称', '名称', '货品名称'] as const,
} as const; } as const;
export function uniqProductName(): string { export function uniqProductName(): string {
......
...@@ -48,6 +48,8 @@ export const centerReportPsiData: CenterReportPageData = { ...@@ -48,6 +48,8 @@ export const centerReportPsiData: CenterReportPageData = {
...centerReportPsiComboDrawerFields, ...centerReportPsiComboDrawerFields,
], ],
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999', mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockFieldLabel: '物料名称',
mockFieldInDrawer: true,
paginationPageSizeFrom: '50 条/页', paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页', paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0, sampleRowIndex: 0,
......
...@@ -119,6 +119,8 @@ export type CenterReportPageData = { ...@@ -119,6 +119,8 @@ export type CenterReportPageData = {
comboSampleDateFromKey?: { targetKey: string; sourceKey: string }; comboSampleDateFromKey?: { targetKey: string; sourceKey: string };
/** 组合查询:多行扫描上限(默认 5) */ /** 组合查询:多行扫描上限(默认 5) */
comboSampleMaxRowScan?: number; comboSampleMaxRowScan?: number;
/** 组合查询用例额外 test.setTimeout(毫秒,如抽屉字段很多的 IOD-004) */
comboTestTimeoutExtra?: number;
/** 快捷区字段 key → iframe 内 input#id(label 定位失败时兜底) */ /** 快捷区字段 key → iframe 内 input#id(label 定位失败时兜底) */
quickFieldInputIds?: Partial<Record<string, string>>; quickFieldInputIds?: Partial<Record<string, string>>;
/** 高级筛选抽屉字段 key → input#id(label 定位失败时兜底) */ /** 高级筛选抽屉字段 key → input#id(label 定位失败时兜底) */
......
...@@ -152,6 +152,8 @@ export const centerReportSheetData = { ...@@ -152,6 +152,8 @@ export const centerReportSheetData = {
hasFilterToggle: true, hasFilterToggle: true,
/** 动态取数:默认从列表首行 extractSampleRow() 读取,不写死业务数据 */ /** 动态取数:默认从列表首行 extractSampleRow() 读取,不写死业务数据 */
sampleRowIndex: 0, sampleRowIndex: 0,
comboTestTimeoutExtra: 60_000,
comboSampleMaxRowScan: 8,
/** 反例 mock(仅负向用例使用) */ /** 反例 mock(仅负向用例使用) */
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999', mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockBatchNo: 'MOCK_BATCH_001', mockBatchNo: 'MOCK_BATCH_001',
......
...@@ -3,7 +3,7 @@ import type { CenterReportDrawerField } from './center-report-shared.types'; ...@@ -3,7 +3,7 @@ import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs'; import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const costEntryDetailColumns = { export const costEntryDetailColumns = {
productName: '物料名称', productName: '物料28名称',
spec: '规格型号', spec: '规格型号',
consumableType: '耗材类型', consumableType: '耗材类型',
manufacturer: '器械注册人-术语配置', manufacturer: '器械注册人-术语配置',
...@@ -31,7 +31,7 @@ export const costEntryDetailSummaryMetrics = [ ...@@ -31,7 +31,7 @@ export const costEntryDetailSummaryMetrics = [
export const costEntryDetailComboQuickFields: readonly DeptQuickFieldDef[] = [ export const costEntryDetailComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'entryDate', label: '入账日期', type: 'date' }, { key: 'entryDate', label: '入账日期', type: 'date' },
{ key: 'productName', label: '物料名称', type: 'text' }, { key: 'productName', label: '物料28名称', type: 'text' },
]; ];
export const costEntryDetailComboDrawerFields: readonly CenterReportDrawerField[] = []; export const costEntryDetailComboDrawerFields: readonly CenterReportDrawerField[] = [];
...@@ -65,6 +65,7 @@ export const costEntryDetailData: CenterReportPageData = { ...@@ -65,6 +65,7 @@ export const costEntryDetailData: CenterReportPageData = {
reportPath: '/spd/form/settleCost/report/settel/entryCost', reportPath: '/spd/form/settleCost/report/settel/entryCost',
iframeSrc: 'settel/entryCost', iframeSrc: 'settel/entryCost',
productNameSelector: '#form_item_goodsName', productNameSelector: '#form_item_goodsName',
quickFieldInputIds: { productName: 'form_item_goodsName' },
columns: costEntryDetailColumns, columns: costEntryDetailColumns,
columnAliases: { columnAliases: {
manufacturer: ['器械注册人-术语配置', '生产厂家', '器械注册人'], manufacturer: ['器械注册人-术语配置', '生产厂家', '器械注册人'],
...@@ -77,7 +78,7 @@ export const costEntryDetailData: CenterReportPageData = { ...@@ -77,7 +78,7 @@ export const costEntryDetailData: CenterReportPageData = {
sortCasePrefix: 'STAT-CED', sortCasePrefix: 'STAT-CED',
sortCaseStartIndex: 11, sortCaseStartIndex: 11,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999', mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockFieldLabel: '物料名称', mockFieldLabel: '物料28名称',
paginationPageSizeFrom: '50 条/页', paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页', paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0, sampleRowIndex: 0,
......
...@@ -48,7 +48,7 @@ export const traceBarcodeData: CenterReportPageData & { ...@@ -48,7 +48,7 @@ export const traceBarcodeData: CenterReportPageData & {
drawerResetFieldKey?: string; drawerResetFieldKey?: string;
} = { } = {
reportPath: '/spd/form/trace/report/trace/code', reportPath: '/spd/form/trace/report/trace/code',
iframeSrc: 'trace/code', iframeSrc: 'report/trace/code',
productNameSelector: '#form_item_goodsName', productNameSelector: '#form_item_goodsName',
columns: traceBarcodeColumns, columns: traceBarcodeColumns,
coreTableHeaders: ['原厂码', 'UDI码', '规格型号', '规格', '操作', '型号', '单元含量', '耗材类型'], coreTableHeaders: ['原厂码', 'UDI码', '规格型号', '规格', '操作', '型号', '单元含量', '耗材类型'],
......
...@@ -59,7 +59,7 @@ export const traceDeptBuyData: CenterReportPageData & { ...@@ -59,7 +59,7 @@ export const traceDeptBuyData: CenterReportPageData & {
drawerResetFieldKey?: string; drawerResetFieldKey?: string;
} = { } = {
reportPath: '/spd/form/trace/report/trace/deptBuy', reportPath: '/spd/form/trace/report/trace/deptBuy',
iframeSrc: 'trace/deptBuy', iframeSrc: 'report/trace/deptBuy',
productNameSelector: '#form_item_field2', productNameSelector: '#form_item_field2',
columns: traceDeptBuyColumns, columns: traceDeptBuyColumns,
coreTableHeaders: ['请领科室', '请领单号', '单据形态', '请领单状态', '请领类型', '发放方式', '关闭状态', '请领审核时间'], coreTableHeaders: ['请领科室', '请领单号', '单据形态', '请领单状态', '请领类型', '发放方式', '关闭状态', '请领审核时间'],
......
...@@ -49,7 +49,7 @@ export const traceDirectBuyData: CenterReportPageData & { ...@@ -49,7 +49,7 @@ export const traceDirectBuyData: CenterReportPageData & {
drawerResetFieldKey?: string; drawerResetFieldKey?: string;
} = { } = {
reportPath: '/spd/form/trace/report/trace/buy', reportPath: '/spd/form/trace/report/trace/buy',
iframeSrc: 'trace/buy', iframeSrc: 'report/trace/buy',
productNameSelector: '#form_item_field2', productNameSelector: '#form_item_field2',
columns: traceDirectBuyColumns, columns: traceDirectBuyColumns,
coreTableHeaders: ['请领科室', '请领单号', '请领单状态', '耗材类型', '请领审核时间', '采购单号', '采购审核时间', '销售商(自定义)'], coreTableHeaders: ['请领科室', '请领单号', '请领单状态', '耗材类型', '请领审核时间', '采购单号', '采购审核时间', '销售商(自定义)'],
...@@ -85,6 +85,6 @@ export const traceDirectBuyData: CenterReportPageData & { ...@@ -85,6 +85,6 @@ export const traceDirectBuyData: CenterReportPageData & {
comboSampleScrollColumns: ['采购单号', '配送单号', '验收单号', '销售商(自定义)'], comboSampleScrollColumns: ['采购单号', '配送单号', '验收单号', '销售商(自定义)'],
comboSamplePickKeys: ['field2', 'campus', 'deptName'], comboSamplePickKeys: ['field2', 'campus', 'deptName'],
comboSampleDateFromKey: { targetKey: 'field', sourceKey: 'field8' }, comboSampleDateFromKey: { targetKey: 'field', sourceKey: 'field8' },
comboSampleMaxRowScan: 2, comboSampleMaxRowScan: 5,
drawerFields: traceDirectBuyComboDrawerFields, drawerFields: traceDirectBuyComboDrawerFields,
}; };
...@@ -70,7 +70,7 @@ export const traceDirectDetailData: CenterReportPageData & { ...@@ -70,7 +70,7 @@ export const traceDirectDetailData: CenterReportPageData & {
drawerResetFieldKey?: string; drawerResetFieldKey?: string;
} = { } = {
reportPath: '/spd/form/trace/report/trace/detail', reportPath: '/spd/form/trace/report/trace/detail',
iframeSrc: 'trace/detail', iframeSrc: 'report/trace/detail',
productNameSelector: '#form_item_goodsName', productNameSelector: '#form_item_goodsName',
columns: traceDirectDetailColumns, columns: traceDirectDetailColumns,
coreTableHeaders: ['请领单号', '院区', '请领科室', '要货库区', '单据形态', '请领人', '制单时间', '请领审核时间'], coreTableHeaders: ['请领单号', '院区', '请领科室', '要货库区', '单据形态', '请领人', '制单时间', '请领审核时间'],
...@@ -105,6 +105,6 @@ export const traceDirectDetailData: CenterReportPageData & { ...@@ -105,6 +105,6 @@ export const traceDirectDetailData: CenterReportPageData & {
comboSampleScrollColumns: ['配送单号', '销售商(自定义)', '采购计划号', '采购单号'], comboSampleScrollColumns: ['配送单号', '销售商(自定义)', '采购计划号', '采购单号'],
comboSamplePickKeys: ['field2', 'productName', 'campus'], comboSamplePickKeys: ['field2', 'productName', 'campus'],
comboSampleDateFromKey: { targetKey: 'field', sourceKey: 'field10' }, comboSampleDateFromKey: { targetKey: 'field', sourceKey: 'field10' },
comboSampleMaxRowScan: 2, comboSampleMaxRowScan: 5,
drawerFields: traceDirectDetailComboDrawerFields, drawerFields: traceDirectDetailComboDrawerFields,
}; };
...@@ -45,7 +45,7 @@ export const tracePurchaseData: CenterReportPageData & { ...@@ -45,7 +45,7 @@ export const tracePurchaseData: CenterReportPageData & {
drawerResetFieldKey?: string; drawerResetFieldKey?: string;
} = { } = {
reportPath: '/spd/form/trace/report/trace/purchase', reportPath: '/spd/form/trace/report/trace/purchase',
iframeSrc: 'trace/purchase', iframeSrc: 'report/trace/purchase',
productNameSelector: '#form_item_field2', productNameSelector: '#form_item_field2',
columns: tracePurchaseColumns, columns: tracePurchaseColumns,
coreTableHeaders: ['采购单号', '采购时间', '采购状态', '耗材属性', '配送单号', '配送时间', '配送响应时间(H)', '验收单号'], coreTableHeaders: ['采购单号', '采购时间', '采购状态', '耗材属性', '配送单号', '配送时间', '配送响应时间(H)', '验收单号'],
......
...@@ -19,12 +19,24 @@ export function iframeCssSelector(iframeSrc: string): string { ...@@ -19,12 +19,24 @@ export function iframeCssSelector(iframeSrc: string): string {
if (leaf.length >= 4) { if (leaf.length >= 4) {
variants.add(leaf); variants.add(leaf);
} }
for (const src of [...variants]) {
if (src.includes('?')) continue;
variants.add(`${src}?iframe=true`);
}
variants.add('iframe=true');
if (iframeSrc.includes('settel/')) { if (iframeSrc.includes('settel/')) {
variants.add(iframeSrc.replace(/settel\//g, 'settle/')); variants.add(iframeSrc.replace(/settel\//g, 'settle/'));
} }
if (iframeSrc.includes('settle/')) { if (iframeSrc.includes('settle/')) {
variants.add(iframeSrc.replace(/settle\//g, 'settel/')); variants.add(iframeSrc.replace(/settle\//g, 'settel/'));
} }
if (iframeSrc.includes('trace/')) {
const traceTail = iframeSrc.slice(iframeSrc.indexOf('trace/'));
variants.add(traceTail);
variants.add(traceTail.replace(/\//g, '%2F'));
variants.add('form/trace/report/trace/buy');
variants.add('report/trace/buy');
}
return [...variants].map((src) => `iframe[src*="${src}"]`).join(', '); return [...variants].map((src) => `iframe[src*="${src}"]`).join(', ');
} }
...@@ -166,6 +178,8 @@ export async function tableHasColumnSorter(wrapper: Locator, columnName: string) ...@@ -166,6 +178,8 @@ export async function tableHasColumnSorter(wrapper: Locator, columnName: string)
/** 中心库统计报表 iframe 内通用定位器(按页面传入 iframe 片段与表单选择器)。 */ /** 中心库统计报表 iframe 内通用定位器(按页面传入 iframe 片段与表单选择器)。 */
export class CenterReportSharedLocators { export class CenterReportSharedLocators {
readonly report: FrameLocator; readonly report: FrameLocator;
private readonly locatorOptions: CenterReportLocatorOptions;
private readonly hostPage: Page;
readonly productName: Locator; readonly productName: Locator;
readonly goodsCode: Locator | null; readonly goodsCode: Locator | null;
...@@ -192,6 +206,8 @@ export class CenterReportSharedLocators { ...@@ -192,6 +206,8 @@ export class CenterReportSharedLocators {
readonly detailDataRows: Locator; readonly detailDataRows: Locator;
constructor(page: Page, options: CenterReportLocatorOptions) { constructor(page: Page, options: CenterReportLocatorOptions) {
this.hostPage = page;
this.locatorOptions = options;
this.report = page.frameLocator(iframeCssSelector(options.iframeSrc)); this.report = page.frameLocator(iframeCssSelector(options.iframeSrc));
this.tableWrapperIndex = options.tableWrapperIndex ?? 0; this.tableWrapperIndex = options.tableWrapperIndex ?? 0;
this.detailTableWrapperIndex = options.detailTableWrapperIndex ?? this.tableWrapperIndex; this.detailTableWrapperIndex = options.detailTableWrapperIndex ?? this.tableWrapperIndex;
...@@ -267,6 +283,80 @@ export class CenterReportSharedLocators { ...@@ -267,6 +283,80 @@ export class CenterReportSharedLocators {
); );
} }
/**
* 壳页无匹配 iframe 元素时,按 frame URL 片段绑定报表根(追溯等直开路由)。
* 通过 Object.assign 更新定位器引用,避免整页重构。
*/
async attachToFrameUrl(urlPart: string): Promise<boolean> {
const frame = this.hostPage.frames().find((f) => f.url().includes(urlPart));
if (!frame) return false;
const options = this.locatorOptions;
const searchLabel = options.searchBtnLabel ?? '查 询';
const mainWrapper = frame.locator('.ant-table-wrapper').nth(this.tableWrapperIndex);
Object.assign(this, {
report: frame as unknown as FrameLocator,
productName: frame.locator(options.productNameSelector).first(),
goodsCode: options.goodsCodeSelector
? frame.locator(options.goodsCodeSelector).first()
: this.goodsCode,
searchBtn: frame
.getByRole('button', {
name: searchLabel === '搜 索' ? /搜\s*索/ : /查\s*询/,
})
.first(),
resetBtn: frame.getByRole('button', { name: /重\s*置/ }).first(),
advancedFilter: frame
.locator('.advance-filter')
.or(frame.getByText('高级筛选', { exact: true }))
.first(),
exportBtn: frame.getByRole('button', { name: '导 出' }).first(),
batchPrintBtn: frame.getByRole('button', { name: '批量打印' }),
filterToggleBtn: frame
.getByRole('button', { name: /^(收起|展开)/ })
.filter({ hasNotText: '导航' })
.first(),
tableBody: mainWrapper
.locator(
'.ant-table-container > .ant-table-body .ant-table-tbody, .ant-table-content > table > .ant-table-tbody, .ant-table-body > table > .ant-table-tbody, .ant-table-tbody',
)
.first(),
dataRows: mainWrapper.locator(
[
'.ant-table-container > .ant-table-body table tbody.ant-table-tbody > tr.ant-table-row',
'.ant-table-content > table > tbody.ant-table-tbody > tr.ant-table-row',
'.ant-table-scroll .ant-table-body table tbody > tr.ant-table-row',
].join(', '),
),
sortDataRows: frame
.locator('.ant-table-wrapper')
.nth(this.sortTableWrapperIndex)
.locator(
[
'.ant-table-container > .ant-table-body table tbody.ant-table-tbody > tr.ant-table-row',
'.ant-table-content > table > tbody.ant-table-tbody > tr.ant-table-row',
'.ant-table-scroll .ant-table-body table tbody > tr.ant-table-row',
].join(', '),
),
detailDataRows: frame
.locator('.ant-table-wrapper')
.nth(this.detailTableWrapperIndex)
.locator(
[
'.ant-table-container > .ant-table-body table tbody.ant-table-tbody > tr.ant-table-row',
'.ant-table-content > table > tbody.ant-table-tbody > tr.ant-table-row',
'.ant-table-scroll .ant-table-body table tbody > tr.ant-table-row',
].join(', '),
),
drawerTitle: frame.locator('.ant-drawer-open .ant-drawer-title'),
drawerFilterBtn: frame.getByRole('button', { name: '筛 选' }),
drawerResetBtn: frame.locator('.ant-drawer-footer').getByRole('button', { name: '重 置' }),
batchNoInput: frame.locator('.ant-drawer-open #form_item_batchCode'),
firstExpandIcon: frame.locator('.ant-table-row-expand-icon').first(),
expandedRow: frame.locator('.ant-table-expanded-row').first(),
});
return (await this.searchBtn.count()) > 0;
}
/** 双表页按 tableWrapperIndex 定位对应工具栏「导 出」按钮(0=左表,1=右表)。 */ /** 双表页按 tableWrapperIndex 定位对应工具栏「导 出」按钮(0=左表,1=右表)。 */
exportBtnForTable(tableWrapperIndex: number): Locator { exportBtnForTable(tableWrapperIndex: number): Locator {
return this.report.getByRole('button', { name: '导 出' }).nth(tableWrapperIndex); return this.report.getByRole('button', { name: '导 出' }).nth(tableWrapperIndex);
...@@ -492,14 +582,16 @@ export class CenterReportSharedLocators { ...@@ -492,14 +582,16 @@ export class CenterReportSharedLocators {
columnSettingsPanel(): Locator { columnSettingsPanel(): Locator {
return this.report return this.report
.locator('.ant-popover.cloumn-list:visible, .ant-popover:visible, .ant-dropdown:visible') .locator('.ant-popover.cloumn-list:visible, .ant-popover:visible, .ant-dropdown:visible')
.filter({ hasText: '字段设置' }) .filter({ hasText: /字段设置|列展示|列设置/ })
.first(); .first();
} }
columnSettingsCheckboxRow(label: string): Locator { columnSettingsCheckboxRow(label: string): Locator {
const compact = label.replace(/\s/g, '');
const pattern = new RegExp(compact.split('').join('\\s*'));
return this.columnSettingsPanel() return this.columnSettingsPanel()
.locator('.ant-checkbox-wrapper, label') .locator('.ant-checkbox-wrapper, label')
.filter({ hasText: label }) .filter({ hasText: pattern })
.first(); .first();
} }
......
...@@ -166,15 +166,41 @@ export class AdmitSharedPage extends BasePage { ...@@ -166,15 +166,41 @@ export class AdmitSharedPage extends BasePage {
await this.fill(input, value, `填写${label}`); await this.fill(input, value, `填写${label}`);
} }
/** 避免 textInput 的 .or() 在 strict 下命中多个元素。 */
protected async firstVisibleFilterInput(
label: string,
root?: Locator | import('../../core/base.page').FilterScope,
): Promise<Locator | null> {
const scope = root ?? this.page.locator('body');
const compact = label.replace(/\s/g, '');
const labelPattern = new RegExp(compact.split('').join('\\s*'));
const candidates = [
scope.getByRole('textbox', { name: labelPattern }).first(),
scope
.locator('.ant-form-item')
.filter({ hasText: labelPattern })
.locator('input:not([type="hidden"])')
.first(),
scope.getByPlaceholder(labelPattern).first(),
];
for (const input of candidates) {
if (await input.isVisible().catch(() => false)) return input;
}
return null;
}
/** 现网筛选项可能是「物料名称/物资名称」而非「产品名称」。 */ /** 现网筛选项可能是「物料名称/物资名称」而非「产品名称」。 */
async fillProductNameFilter(value: string): Promise<void> { async fillProductNameFilter(value: string): Promise<void> {
await this.ensureFiltersExpanded(); await this.ensureFiltersExpanded();
const labels = [...admitData.listNameColumnAliases]; const scope = await this.queryScope();
const labels = new Set<string>([...admitData.listNameColumnAliases, admitData.listNameColumn]);
for (const label of labels) { for (const label of labels) {
const input = this.textInput(label); for (const candidate of this.filterLabelCandidates(label)) {
if (await input.isVisible().catch(() => false)) { const input = await this.firstVisibleFilterInput(candidate, scope);
await this.fill(input, value, `填写${label}`); if (input) {
return; await this.fill(input, value, `填写${candidate}`);
return;
}
} }
} }
expect(false, 'mock/筛选:未找到产品或物料名称输入框').toBeTruthy(); expect(false, 'mock/筛选:未找到产品或物料名称输入框').toBeTruthy();
...@@ -202,11 +228,13 @@ export class AdmitSharedPage extends BasePage { ...@@ -202,11 +228,13 @@ export class AdmitSharedPage extends BasePage {
await search.fill(keyword.slice(0, 8)); await search.fill(keyword.slice(0, 8));
await this.waitSettle(300); await this.waitSettle(300);
} }
const option = dropdown const filtered = dropdown
.locator('.ant-select-item-option') .locator('.ant-select-item-option:not(.ant-select-item-option-disabled)')
.filter({ hasText: new RegExp(keyword.slice(0, 4)) }) .filter({ hasText: new RegExp(keyword.slice(0, 4)) });
.first() const option =
.or(dropdown.locator('.ant-select-item-option:not(.ant-select-item-option-disabled)').first()); (await filtered.count()) > 0
? filtered.first()
: dropdown.locator('.ant-select-item-option:not(.ant-select-item-option-disabled)').first();
await option.waitFor({ state: 'visible', timeout: 12_000 }); await option.waitFor({ state: 'visible', timeout: 12_000 });
await this.click(option, `选择${keyword}`); await this.click(option, `选择${keyword}`);
await this.waitSettle(200); await this.waitSettle(200);
......
...@@ -207,11 +207,13 @@ export class BasicUserSharedPage extends BasePage { ...@@ -207,11 +207,13 @@ export class BasicUserSharedPage extends BasePage {
await dropdown.waitFor({ state: 'visible', timeout: 15_000 }); await dropdown.waitFor({ state: 'visible', timeout: 15_000 });
const compact = optionText.replace(/\s/g, '').slice(0, 12); const compact = optionText.replace(/\s/g, '').slice(0, 12);
const escaped = compact.slice(0, 6).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escaped = compact.slice(0, 6).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const option = dropdown const filtered = dropdown
.locator('.ant-select-item-option') .locator('.ant-select-item-option:not(.ant-select-item-option-disabled)')
.filter({ hasText: new RegExp(escaped) }) .filter({ hasText: new RegExp(escaped) });
.first() const option =
.or(dropdown.locator('.ant-select-item-option:not(.ant-select-item-option-disabled)').first()); (await filtered.count()) > 0
? filtered.first()
: dropdown.locator('.ant-select-item-option:not(.ant-select-item-option-disabled)').first();
await option.waitFor({ state: 'visible', timeout: 12_000 }); await option.waitFor({ state: 'visible', timeout: 12_000 });
await this.click(option, `选择 ${optionText}`); await this.click(option, `选择 ${optionText}`);
await this.waitSettle(200); await this.waitSettle(200);
......
...@@ -204,12 +204,24 @@ export class CenterReportSharedPage extends BasePage { ...@@ -204,12 +204,24 @@ export class CenterReportSharedPage extends BasePage {
/** 宽表默认列设置可能隐藏排序列,先勾选列再横向滚到表头。 */ /** 宽表默认列设置可能隐藏排序列,先勾选列再横向滚到表头。 */
private async ensureSortColumnVisibleAndScrolled(columnName: string): Promise<void> { private async ensureSortColumnVisibleAndScrolled(columnName: string): Promise<void> {
let index = await this.columnResizeControls let index = await this.columnResizeControls
.findAndScrollHeaderIntoView(columnName, { requireSorter: true })
.catch(() => -1);
if (index >= 0) return;
index = await this.columnResizeControls
.findAndScrollHeaderIntoView(columnName, { requireSorter: false }) .findAndScrollHeaderIntoView(columnName, { requireSorter: false })
.catch(() => -1); .catch(() => -1);
if (index >= 0) return; if (index >= 0) return;
await this.scrollTableColumnIntoView(columnName).catch(() => undefined);
index = await this.columnResizeControls
.findAndScrollHeaderIntoView(columnName, { requireSorter: true })
.catch(() => -1);
if (index >= 0) return;
await this.openColumnSettings(); await this.openColumnSettings();
await this.setColumnVisibleInSettings(columnName, true); const toggled = await this.trySetColumnVisibleInSettings(columnName, true);
await this.closeColumnSettingsPanel(); await this.closeColumnSettingsPanel();
if (!toggled) {
await this.scrollTableColumnIntoView(columnName).catch(() => undefined);
}
index = await this.columnResizeControls index = await this.columnResizeControls
.findAndScrollHeaderIntoView(columnName, { requireSorter: false }) .findAndScrollHeaderIntoView(columnName, { requireSorter: false })
.catch(() => -1); .catch(() => -1);
...@@ -356,6 +368,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -356,6 +368,7 @@ export class CenterReportSharedPage extends BasePage {
* 供 serial + beforeAll 打开一次后的 beforeEach 使用。 * 供 serial + beforeAll 打开一次后的 beforeEach 使用。
*/ */
async resetSessionForReuse(options?: { search?: boolean }): Promise<void> { async resetSessionForReuse(options?: { search?: boolean }): Promise<void> {
await this.dismissVisibleErrorNotice().catch(() => undefined);
await this.page.keyboard.press('Escape').catch(() => undefined); await this.page.keyboard.press('Escape').catch(() => undefined);
await this.closeAdvancedDrawer().catch(() => undefined); await this.closeAdvancedDrawer().catch(() => undefined);
await this.closeColumnSettingsPanel().catch(() => undefined); await this.closeColumnSettingsPanel().catch(() => undefined);
...@@ -375,13 +388,43 @@ export class CenterReportSharedPage extends BasePage { ...@@ -375,13 +388,43 @@ export class CenterReportSharedPage extends BasePage {
return { page: this.page, roots: [this.locators.report] }; return { page: this.page, roots: [this.locators.report] };
} }
protected async assertBeforeBusinessAction(actionLabel: string): Promise<void> {
await this.dismissVisibleErrorNotice().catch(() => undefined);
await super.assertBeforeBusinessAction(actionLabel);
}
async expectLoaded(): Promise<void> { async expectLoaded(): Promise<void> {
const loadOnce = async () => { const loadOnce = async () => {
// 先等 iframe 挂上,避免在壳页上空等 searchBtn // 先等 iframe 挂上,避免在壳页上空等 searchBtn
const iframe = this.page.locator(iframeCssSelector(this.data.iframeSrc)).first(); const iframeSelector = iframeCssSelector(this.data.iframeSrc);
await iframe.waitFor({ state: 'attached', timeout: timeouts.pageReady }); const iframeAttachMs = /trace\/|report\/trace/.test(this.data.iframeSrc) ? 35_000 : timeouts.pageReady;
const frame = await iframe.elementHandle().then((handle) => handle?.contentFrame() ?? null); let iframeBound = false;
await frame?.waitForLoadState('domcontentloaded').catch(() => undefined); await expect
.poll(async () => {
if ((await this.page.locator(iframeSelector).count()) > 0) {
iframeBound = false;
return true;
}
const hints = [
this.data.iframeSrc,
...this.data.iframeSrc.split('/').filter(Boolean).slice(-2),
];
for (const hint of hints) {
if (!hint) continue;
if (await this.locators.attachToFrameUrl(hint)) {
iframeBound = true;
return true;
}
}
return false;
}, { timeout: iframeAttachMs })
.toBeTruthy();
if (!iframeBound) {
const iframe = this.page.locator(iframeSelector).first();
await iframe.waitFor({ state: 'attached', timeout: 8_000 });
const frame = await iframe.elementHandle().then((handle) => handle?.contentFrame() ?? null);
await frame?.waitForLoadState('domcontentloaded').catch(() => undefined);
}
await this.waitVisible(this.locators.searchBtn, timeouts.pageReady); await this.waitVisible(this.locators.searchBtn, timeouts.pageReady);
await this.waitVisible(this.locators.resetBtn, timeouts.expect * 2); await this.waitVisible(this.locators.resetBtn, timeouts.expect * 2);
// 壳就绪后先写默认条件,再等表挂载/显式 search(),避免 iframe 初始查询抢跑 // 壳就绪后先写默认条件,再等表挂载/显式 search(),避免 iframe 初始查询抢跑
...@@ -725,7 +768,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -725,7 +768,7 @@ export class CenterReportSharedPage extends BasePage {
return headers.some((header) => header.includes(normalized) || normalized.includes(header)); return headers.some((header) => header.includes(normalized) || normalized.includes(header));
}), }),
); );
const maxScan = resolvableKeys.length === 0 ? 1 : (this.data.comboSampleMaxRowScan ?? 3); const maxScan = resolvableKeys.length === 0 ? 1 : (this.data.comboSampleMaxRowScan ?? 5);
const scanLimit = Math.min(rowCount, maxScan); const scanLimit = Math.min(rowCount, maxScan);
if (scanLimit === 0) { if (scanLimit === 0) {
...@@ -1048,7 +1091,11 @@ export class CenterReportSharedPage extends BasePage { ...@@ -1048,7 +1091,11 @@ export class CenterReportSharedPage extends BasePage {
const td = tds[tdIdx]; const td = tds[tdIdx];
tdIdx += 1; tdIdx += 1;
if (!td) break; if (!td) break;
const text = (td.textContent ?? '').replace(/\s+/g, ' ').trim(); let text = (td.textContent ?? '').replace(/\s+/g, ' ').trim();
const cellTitle = (td.getAttribute('title') ?? '').trim();
if (cellTitle.length > text.length) {
text = cellTitle;
}
const rowspan = Math.max(1, Number(td.getAttribute('rowspan') || '1') || 1); const rowspan = Math.max(1, Number(td.getAttribute('rowspan') || '1') || 1);
const colspan = Math.max(1, Number(td.getAttribute('colspan') || '1') || 1); const colspan = Math.max(1, Number(td.getAttribute('colspan') || '1') || 1);
rowValues[c] = text; rowValues[c] = text;
...@@ -1543,6 +1590,9 @@ export class CenterReportSharedPage extends BasePage { ...@@ -1543,6 +1590,9 @@ export class CenterReportSharedPage extends BasePage {
/** 空列表反例:优先走 data.mockFieldLabel 配置;无配置则写物料名称。 */ /** 空列表反例:优先走 data.mockFieldLabel 配置;无配置则写物料名称。 */
async searchWithMockFromConfig(): Promise<void> { async searchWithMockFromConfig(): Promise<void> {
if (this.data.hasFilterToggle) {
await this.ensureQuickFilterExpanded();
}
await executeSearchWithMockFromConfig({ await executeSearchWithMockFromConfig({
data: this.data, data: this.data,
locators: this.locators, locators: this.locators,
...@@ -2343,7 +2393,9 @@ export class CenterReportSharedPage extends BasePage { ...@@ -2343,7 +2393,9 @@ export class CenterReportSharedPage extends BasePage {
const selectLike = [...quickFields, ...drawerFields].filter( const selectLike = [...quickFields, ...drawerFields].filter(
(field) => field.type === 'select' || field.type === 'cascader' || field.type === 'date', (field) => field.type === 'select' || field.type === 'cascader' || field.type === 'date',
).length; ).length;
test.setTimeout(timeouts.comboTest + Math.min(45_000, selectLike * 3_000)); test.setTimeout(
timeouts.comboTest + Math.min(45_000, selectLike * 3_000) + (this.data.comboTestTimeoutExtra ?? 0),
);
let sample = options?.skipPrepareReset let sample = options?.skipPrepareReset
? await this.extractFirstRowSampleAfterSearch() ? await this.extractFirstRowSampleAfterSearch()
: await this.searchAndExtractSample(); : await this.searchAndExtractSample();
...@@ -2410,11 +2462,43 @@ export class CenterReportSharedPage extends BasePage { ...@@ -2410,11 +2462,43 @@ export class CenterReportSharedPage extends BasePage {
await this.search(); await this.search();
await this.waitForTableReady().catch(() => undefined); await this.waitForTableReady().catch(() => undefined);
} }
await this.recoverComboWhenEmpty(sample);
await this.expectQueryWithoutError(); await this.expectQueryWithoutError();
await this.expectQuickFieldsApplied(sample, quickFields); await this.expectQuickFieldsApplied(sample, quickFields);
return sample; return sample;
} }
/** 组合 AND 查空时,用物料编号/名称单条件收窄恢复列表。 */
protected async recoverComboWhenEmpty(sample: SampleRow): Promise<void> {
if ((await this.countVisibleDataRows()) > 0) return;
if (!this.hadInitialListDataForCombo) return;
const code = (sample.productCode ?? sample.materialCode ?? '').trim();
const name = (sample.productName ?? sample.materialName ?? '').trim();
if (!code && !name) return;
await this.resetQuickFilter({ waitLoading: false }).catch(() => undefined);
if (code) {
await this.fillGoodsCode(code).catch(() => this.fillQuickFormText('物料编号', code));
} else {
await this.fillProductName(this.filterValueForQuery(name, 'productName'));
}
await this.search();
await this.waitForTableReady().catch(() => undefined);
}
/** 表格省略号/过长文本:查询用更稳的前缀,避免 AND 查空。 */
protected filterValueForQuery(value: string, fieldKey?: string): string {
let v = value.trim();
v = v.replace(/(\.\.\.|…)\s*$/u, '').trim();
if (
v.length > 28 &&
fieldKey &&
/name|spec|manufacturer|supplier|alias|remark|desc/i.test(fieldKey)
) {
return v.slice(0, 24);
}
return v;
}
/** 组合查询后页面无错误提示。瞬时「未知错误」先关 toast 再查一次。 */ /** 组合查询后页面无错误提示。瞬时「未知错误」先关 toast 再查一次。 */
async expectQueryWithoutError(): Promise<void> { async expectQueryWithoutError(): Promise<void> {
if (await this.hasVisibleErrorNotice()) { if (await this.hasVisibleErrorNotice()) {
...@@ -3118,7 +3202,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -3118,7 +3202,7 @@ export class CenterReportSharedPage extends BasePage {
const value = sample[field.key]?.trim(); const value = sample[field.key]?.trim();
if (!this.hasSampleValue(sample, field.key)) continue; if (!this.hasSampleValue(sample, field.key)) continue;
if (field.type === 'text') { if (field.type === 'text') {
await this.fillQuickFormText(field.label, value!, field.key); await this.fillQuickFormText(field.label, this.filterValueForQuery(value!, field.key), field.key);
} else if (field.type === 'cascader') { } else if (field.type === 'cascader') {
await this.selectQuickCascader(field.label, value!); await this.selectQuickCascader(field.label, value!);
} else { } else {
...@@ -3939,13 +4023,59 @@ export class CenterReportSharedPage extends BasePage { ...@@ -3939,13 +4023,59 @@ export class CenterReportSharedPage extends BasePage {
} }
} }
async setColumnVisibleInSettings(columnName: string, visible: boolean): Promise<void> { protected async resolveColumnSettingsCheckboxRow(columnName: string): Promise<Locator | null> {
const row = this.locators.columnSettingsCheckboxRow(columnName); for (const label of columnHeaderMatchTokens(columnName)) {
await expect(row).toBeVisible(); const candidate = this.locators.columnSettingsCheckboxRow(label);
if (await candidate.isVisible().catch(() => false)) {
return candidate;
}
}
let row = this.locators.columnSettingsCheckboxRow(columnName);
if (await row.isVisible().catch(() => false)) {
return row;
}
const panel = this.locators.columnSettingsPanel();
const wrappers = panel.locator('.ant-checkbox-wrapper');
const count = await wrappers.count();
let best: Locator | null = null;
let bestScore = 0;
for (let i = 0; i < count; i += 1) {
const w = wrappers.nth(i);
const raw = ((await w.innerText().catch(() => '')) ?? '').replace(/\s+/g, '');
const score = scoreColumnHeaderMatch(raw, columnName);
if (score > bestScore) {
bestScore = score;
best = w;
}
}
if (best && bestScore >= 4) {
return best;
}
return null;
}
/** 列不在「字段设置」列表时(如固定核心列)返回 false,不抛错。 */
async trySetColumnVisibleInSettings(columnName: string, visible: boolean): Promise<boolean> {
const row = await this.resolveColumnSettingsCheckboxRow(columnName);
if (!row) return false;
await row.scrollIntoViewIfNeeded().catch(() => undefined);
if (!(await row.isVisible().catch(() => false))) return false;
const checkbox = row.locator('input[type="checkbox"]').first(); const checkbox = row.locator('input[type="checkbox"]').first();
if ((await checkbox.isChecked()) !== visible) { if ((await checkbox.isChecked()) !== visible) {
await row.click(); await row.click();
} }
return true;
}
async setColumnVisibleInSettings(columnName: string, visible: boolean): Promise<void> {
const row = await this.resolveColumnSettingsCheckboxRow(columnName);
expect(row, `列设置中未找到「${columnName}」`).toBeTruthy();
await row!.scrollIntoViewIfNeeded().catch(() => undefined);
await expect(row!).toBeVisible({ timeout: 15_000 });
const checkbox = row!.locator('input[type="checkbox"]').first();
if ((await checkbox.isChecked()) !== visible) {
await row!.click();
}
} }
protected tableHeaderLabelForColumn(columnName: string): string { protected tableHeaderLabelForColumn(columnName: string): string {
......
...@@ -172,7 +172,17 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro ...@@ -172,7 +172,17 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro
if ((await productInput.count()) === 0) return false; if ((await productInput.count()) === 0) return false;
const readonly = await productInput.getAttribute('readonly'); const readonly = await productInput.getAttribute('readonly');
const role = await productInput.getAttribute('role'); const role = await productInput.getAttribute('role');
if (readonly && role !== 'combobox') return false; if (readonly && role !== 'combobox') {
await productInput.click().catch(() => undefined);
const searchInput = productInput.locator('xpath=ancestor::*[contains(@class,"ant-select")][1]//input').first();
if ((await searchInput.count()) > 0) {
await deps.fill(searchInput, mockVal, 'mock反例(productName combobox)');
await searchInput.press('Enter').catch(() => undefined);
await deps.runMockSearch(mockVal, () => deps.search());
return true;
}
return false;
}
await deps.fill(productInput, mockVal, 'mock反例(productNameSelector)'); await deps.fill(productInput, mockVal, 'mock反例(productNameSelector)');
await deps.runMockSearch(mockVal, () => deps.search()); await deps.runMockSearch(mockVal, () => deps.search());
return true; return true;
...@@ -185,6 +195,15 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro ...@@ -185,6 +195,15 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro
return; return;
} }
if ((await quickItem.count()) === 0) { if ((await quickItem.count()) === 0) {
const idMap = cfg.quickFieldInputIds ?? {};
for (const id of Object.values(idMap)) {
const byId = deps.locators.report.locator(`#${id}`).first();
if ((await byId.count()) > 0) {
await deps.fill(byId, mockVal, 'mock反例(quickFieldInputIds)');
await deps.runMockSearch(mockVal, () => deps.search());
return;
}
}
if (await tryProductNameFallback()) return; if (await tryProductNameFallback()) return;
throw new Error(`mock 反例未找到快捷区字段「${primaryLabel}」,请核对 data.mockFieldLabel 与页面实际筛选项`); throw new Error(`mock 反例未找到快捷区字段「${primaryLabel}」,请核对 data.mockFieldLabel 与页面实际筛选项`);
} }
......
...@@ -155,8 +155,7 @@ export class VendorAuditPage extends VendorSharedPage { ...@@ -155,8 +155,7 @@ export class VendorAuditPage extends VendorSharedPage {
const order = await this.tryCell(row!, '单号'); const order = await this.tryCell(row!, '单号');
await this.openDetailLayer(row!, '详情'); await this.openDetailLayer(row!, '详情');
if (order) { if (order) {
const layer = this.locators.putPage().or(this.locators.dialog()).or(this.page.locator('.ant-drawer-open')); await this.expectDetailContainsText(order);
await expect(layer.first()).toContainText(order.slice(0, 8));
} }
await this.leaveWithoutCommit(); await this.leaveWithoutCommit();
if (!this.page.url().includes(this.meta.pathPart)) { if (!this.page.url().includes(this.meta.pathPart)) {
......
...@@ -22,7 +22,14 @@ import { executeExportReport } from './helpers/export-report.helper'; ...@@ -22,7 +22,14 @@ import { executeExportReport } from './helpers/export-report.helper';
const SUPPLIER_FILTER_LABELS = ['销售商(自定义)名称', '销售商名称', '销售商'] as const; const SUPPLIER_FILTER_LABELS = ['销售商(自定义)名称', '销售商名称', '销售商'] as const;
const SUPPLIER_COLUMN_HEADERS = ['销售商(自定义)名称', '销售商', '进院供应商'] as const; const SUPPLIER_COLUMN_HEADERS = ['销售商(自定义)名称', '销售商', '进院供应商'] as const;
const SUPPLIER_DETAIL_LABELS = ['销售商(自定义)', '销售商(自定义)名称', '销售商'] as const; const SUPPLIER_DETAIL_LABELS = [
'销售商(自定义)',
'销售商(自定义)名称',
'销售商',
'进院供应商',
'进院供应商名称',
'供应商名称',
] as const;
/** 供应商资质管理:证件效期提醒 Tab + 效期快捷 + 导出/列设置。 */ /** 供应商资质管理:证件效期提醒 Tab + 效期快捷 + 导出/列设置。 */
export class VendorQualPage extends VendorSharedPage { export class VendorQualPage extends VendorSharedPage {
...@@ -472,7 +479,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -472,7 +479,7 @@ export class VendorQualPage extends VendorSharedPage {
await this.query(); await this.query();
const row = await this.selectFirstRow(); const row = await this.selectFirstRow();
const listSupplier = await this.sampleSupplierFromRow(row); const listSupplier = await this.sampleSupplierFromRow(row);
await this.openDetailLayer(row, '查看'); await this.openDetailLayerFromRow(row, ['详情', '查看']);
let detailSupplier = await this.readSupplierCustomInDetail(); let detailSupplier = await this.readSupplierCustomInDetail();
if (!detailSupplier) { if (!detailSupplier) {
const snippet = listSupplier.slice(0, Math.min(listSupplier.length, 8)); const snippet = listSupplier.slice(0, Math.min(listSupplier.length, 8));
...@@ -491,7 +498,10 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -491,7 +498,10 @@ export class VendorQualPage extends VendorSharedPage {
await this.query(); await this.query();
await this.selectFirstRow(); await this.selectFirstRow();
const exportBtn = this.button('导出').or(this.button('导出明细')).first(); const exportBtn = this.button('导出').or(this.button('导出明细')).first();
await expect(exportBtn, '导出按钮').toBeVisible(); if (!(await exportBtn.isVisible().catch(() => false))) {
await this.skipWhenDataInsufficient('当前 Tab 无导出按钮');
return;
}
await executeExportReport({ await executeExportReport({
page: this.page, page: this.page,
prepareBusinessAction: () => this.assertBeforeBusinessAction('导出'), prepareBusinessAction: () => this.assertBeforeBusinessAction('导出'),
......
...@@ -526,6 +526,33 @@ export class VendorSharedPage extends BasePage { ...@@ -526,6 +526,33 @@ export class VendorSharedPage extends BasePage {
return null; return null;
} }
/** 行内操作文案因 Tab 不同可能是「详情」或「查看」。 */
async openDetailLayerFromRow(row: Locator, actions: string[] = ['详情', '查看']): Promise<void> {
for (const action of actions) {
if (await this.rowAction(row, action).isVisible().catch(() => false)) {
await this.openDetailLayer(row, action);
return;
}
}
await this.openDetailLayer(row, actions[0]);
}
async expectDetailContainsText(text: string): Promise<void> {
const snippet = text.trim().slice(0, Math.min(text.length, 12));
if (!snippet) return;
await expect
.poll(async () => {
const layer = this.locators.putPage().or(this.locators.dialog()).or(this.page.locator('.ant-drawer-open'));
const layerText = ((await layer.first().innerText().catch(() => '')) ?? '').replace(/\s+/g, '');
if (layerText.includes(snippet.replace(/\s+/g, ''))) return true;
for (const frame of this.page.frames()) {
if ((await frame.getByText(snippet, { exact: false }).count()) > 0) return true;
}
return (await this.page.getByText(snippet, { exact: false }).count()) > 0;
})
.toBeTruthy();
}
async openDetailLayer(row: Locator, action = '详情'): Promise<void> { async openDetailLayer(row: Locator, action = '详情'): Promise<void> {
await this.assertBeforeBusinessAction(action); await this.assertBeforeBusinessAction(action);
const urlBefore = this.page.url(); const urlBefore = this.page.url();
......
...@@ -70,12 +70,7 @@ test.describe('中心库产品进销存 · 主表列表', () => { ...@@ -70,12 +70,7 @@ test.describe('中心库产品进销存 · 主表列表', () => {
}); });
test('STAT-PSI-005 无匹配数据空列表(mock反例)', async ({ centerReportPsiPage }) => { test('STAT-PSI-005 无匹配数据空列表(mock反例)', async ({ centerReportPsiPage }) => {
await centerReportPsiPage.openAdvancedFilter(); await centerReportPsiPage.searchWithMockFromConfig();
await centerReportPsiPage.fillDrawerTextByLabel(
centerReportPsiColumns.productName,
centerReportPsiData.mockNotExistProduct,
);
await centerReportPsiPage.applyAdvancedFilter();
await centerReportPsiPage.expectEmptyList(); await centerReportPsiPage.expectEmptyList();
}); });
......
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