Commit 2d26844c authored by liguangyu06's avatar liguangyu06
Browse files

同步基础管理/报表用例与登录态稳定性修复

修复 auth 写入锁 stale PID 导致 setup 空等超时;主辅账号 setup 串行并延长超时,login 依赖 setup。同步用户/厂商/准入等 CSV·MD·spec·PO,移除 stat 调试 spec,补充离线全量脚本与 gate-stamp 目录哈希修复。
parent 45178f79
...@@ -21,6 +21,7 @@ export class BasicNoticePage extends BasicHospitalSharedPage { ...@@ -21,6 +21,7 @@ export class BasicNoticePage extends BasicHospitalSharedPage {
} }
async search(): Promise<void> { async search(): Promise<void> {
await this.assertBeforeBusinessAction('查询');
await this.click(this.locators.searchBtn, '查 询'); await this.click(this.locators.searchBtn, '查 询');
await this.waitForTableReady(); await this.waitForTableReady();
} }
...@@ -28,6 +29,7 @@ export class BasicNoticePage extends BasicHospitalSharedPage { ...@@ -28,6 +29,7 @@ export class BasicNoticePage extends BasicHospitalSharedPage {
async resetSessionForReuse(options?: { search?: boolean }): Promise<void> { async resetSessionForReuse(options?: { search?: boolean }): Promise<void> {
await this.closeDialogIfOpen(); await this.closeDialogIfOpen();
if (await this.locators.resetBtn.isVisible().catch(() => false)) { if (await this.locators.resetBtn.isVisible().catch(() => false)) {
await this.assertBeforeBusinessAction('重置');
await this.click(this.locators.resetBtn, '重 置'); await this.click(this.locators.resetBtn, '重 置');
await this.waitForTableReady(); await this.waitForTableReady();
} }
......
...@@ -120,7 +120,7 @@ export class BasicRolePage extends BasicUserSharedPage { ...@@ -120,7 +120,7 @@ export class BasicRolePage extends BasicUserSharedPage {
await host.evaluate((el) => { await host.evaluate((el) => {
el.scrollTop = 0; el.scrollTop = 0;
}); });
await this.assertPageHealthy(healthContext); await this.assertUiReady(healthContext);
return; return;
} }
...@@ -141,10 +141,11 @@ export class BasicRolePage extends BasicUserSharedPage { ...@@ -141,10 +141,11 @@ export class BasicRolePage extends BasicUserSharedPage {
}); });
if (!walked.ok) { if (!walked.ok) {
await this.skipWhenDataInsufficient(skipDetail); await this.skipWhenDataInsufficient(skipDetail);
return;
} }
expect(walked.topDown, '向下滚动后 scrollTop 应增加').toBeGreaterThan(walked.top0); expect(walked.topDown, '向下滚动后 scrollTop 应增加').toBeGreaterThan(walked.top0);
expect(walked.topUp, '向上滚回后应回到顶部').toBeLessThanOrEqual(4); expect(walked.topUp, '向上滚回后应回到顶部').toBeLessThanOrEqual(4);
await this.assertPageHealthy(healthContext); await this.assertUiReady(healthContext);
} }
async assertRoleTreeVerticalScroll(): Promise<void> { async assertRoleTreeVerticalScroll(): Promise<void> {
...@@ -352,6 +353,7 @@ export class BasicRolePage extends BasicUserSharedPage { ...@@ -352,6 +353,7 @@ export class BasicRolePage extends BasicUserSharedPage {
const dialog = this.editRoleDialog(); const dialog = this.editRoleDialog();
const save = dialog.getByRole('button', { name: /保\s*存/ }).first(); const save = dialog.getByRole('button', { name: /保\s*存/ }).first();
await expect(save, '弹框保存按钮').toBeVisible(); await expect(save, '弹框保存按钮').toBeVisible();
await this.assertBeforeBusinessAction('保存编辑角色');
await this.click(save, '保存编辑角色', { operation: 'write' }); await this.click(save, '保存编辑角色', { operation: 'write' });
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
...@@ -404,12 +406,13 @@ export class BasicRolePage extends BasicUserSharedPage { ...@@ -404,12 +406,13 @@ export class BasicRolePage extends BasicUserSharedPage {
await this.skipWhenDataInsufficient('「角色」按钮不可见'); await this.skipWhenDataInsufficient('「角色」按钮不可见');
return this.addRoleDialog(); return this.addRoleDialog();
} }
await this.assertBeforeBusinessAction('打开新增角色');
await this.click(add, '角 色'); await this.click(add, '角 色');
await this.waitSettle(400); await this.waitSettle(400);
const dialog = this.addRoleDialog(); const dialog = this.addRoleDialog();
await expect(dialog, '新增角色弹窗').toBeVisible({ timeout: 12_000 }); await expect(dialog, '新增角色弹窗').toBeVisible({ timeout: 12_000 });
expect(await this.page.locator('.ant-message-error').count(), '不应出现错误提示').toBe(0); expect(await this.page.locator('.ant-message-error').count(), '不应出现错误提示').toBe(0);
await this.assertPageHealthy('打开新增角色弹窗'); await this.assertUiReady('打开新增角色弹窗');
return dialog; return dialog;
} }
...@@ -437,7 +440,7 @@ export class BasicRolePage extends BasicUserSharedPage { ...@@ -437,7 +440,7 @@ export class BasicRolePage extends BasicUserSharedPage {
await this.click(close, '点击新增弹框 X 关闭'); await this.click(close, '点击新增弹框 X 关闭');
await expect(dialog, 'X 关闭后弹框应隐藏').toBeHidden({ timeout: 8_000 }); await expect(dialog, 'X 关闭后弹框应隐藏').toBeHidden({ timeout: 8_000 });
expect(await this.page.locator('.ant-message-error').count(), '不应出现错误提示').toBe(0); expect(await this.page.locator('.ant-message-error').count(), '不应出现错误提示').toBe(0);
await this.assertPageHealthy('X 关闭新增角色弹框后'); await this.assertUiReady('X 关闭新增角色弹框后');
} }
async cancelAddRoleDialog(): Promise<void> { async cancelAddRoleDialog(): Promise<void> {
...@@ -448,7 +451,7 @@ export class BasicRolePage extends BasicUserSharedPage { ...@@ -448,7 +451,7 @@ export class BasicRolePage extends BasicUserSharedPage {
await this.click(cancel, '取消新增角色'); await this.click(cancel, '取消新增角色');
await expect(dialog, '取消后弹框应关闭').toBeHidden({ timeout: 8_000 }); await expect(dialog, '取消后弹框应关闭').toBeHidden({ timeout: 8_000 });
expect(await this.page.locator('.ant-message-error').count(), '不应出现错误提示').toBe(0); expect(await this.page.locator('.ant-message-error').count(), '不应出现错误提示').toBe(0);
await this.assertPageHealthy('取消新增角色后'); await this.assertUiReady('取消新增角色后');
} }
async cancelAddRole(): Promise<void> { async cancelAddRole(): Promise<void> {
......
...@@ -18,7 +18,7 @@ export class BasicHospitalSharedPage extends BasePage { ...@@ -18,7 +18,7 @@ export class BasicHospitalSharedPage extends BasePage {
await this.goto(path); await this.goto(path);
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
await this.assertPageHealthy(`打开 ${path}`); await this.assertUiReady(`打开 ${path}`);
expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/); expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/);
} }
...@@ -34,6 +34,7 @@ export class BasicHospitalSharedPage extends BasePage { ...@@ -34,6 +34,7 @@ export class BasicHospitalSharedPage extends BasePage {
await this.page.waitForURL((url) => url.pathname.includes(pathPart), { timeout: 20_000 }); await this.page.waitForURL((url) => url.pathname.includes(pathPart), { timeout: 20_000 });
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
await this.assertUiReady(`菜单进入${menuName}`);
} }
async expectUrlContains(part: string): Promise<void> { async expectUrlContains(part: string): Promise<void> {
...@@ -61,10 +62,10 @@ export class BasicHospitalSharedPage extends BasePage { ...@@ -61,10 +62,10 @@ export class BasicHospitalSharedPage extends BasePage {
return match ? Number(match[1]) : 0; return match ? Number(match[1]) : 0;
} }
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(200); await this.waitSettle(200);
await this.assertPageHealthy('列表加载'); await this.assertAfterTableWait('列表加载', options?.health ?? 'business');
} }
async expectListOrEmpty(): Promise<void> { async expectListOrEmpty(): Promise<void> {
......
...@@ -40,7 +40,7 @@ export class BasicUserSharedPage extends BasePage { ...@@ -40,7 +40,7 @@ export class BasicUserSharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`打开 ${this.meta.path}`); await this.assertUiReady(`打开 ${this.meta.path}`);
expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/); expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/);
} }
...@@ -63,7 +63,7 @@ export class BasicUserSharedPage extends BasePage { ...@@ -63,7 +63,7 @@ export class BasicUserSharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(600); await this.waitSettle(600);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`菜单进入${this.meta.menu}`); await this.assertUiReady(`菜单进入${this.meta.menu}`);
} }
async waitContentReady(): Promise<void> { async waitContentReady(): Promise<void> {
...@@ -71,13 +71,13 @@ export class BasicUserSharedPage extends BasePage { ...@@ -71,13 +71,13 @@ export class BasicUserSharedPage extends BasePage {
.locator('.ant-table-thead th, .ant-empty, .menu-role-tree, .ant-tree, [placeholder="姓名、登录账号"]') .locator('.ant-table-thead th, .ant-empty, .menu-role-tree, .ant-tree, [placeholder="姓名、登录账号"]')
.first(); .first();
await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined); await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined);
await this.waitForTableReady(); await this.waitForTableReady({ health: 'ui' });
} }
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(200); await this.waitSettle(200);
await this.assertPageHealthy('列表加载'); await this.assertAfterTableWait('列表加载', options?.health ?? 'business');
} }
button(name: string, root?: Locator): Locator { button(name: string, root?: Locator): Locator {
...@@ -89,11 +89,13 @@ export class BasicUserSharedPage extends BasePage { ...@@ -89,11 +89,13 @@ export class BasicUserSharedPage extends BasePage {
} }
async query(): Promise<void> { async query(): Promise<void> {
await this.assertBeforeBusinessAction('查询');
await this.clickButton('查询'); await this.clickButton('查询');
await this.waitForTableReady(); await this.waitForTableReady();
} }
async reset(): Promise<void> { async reset(): Promise<void> {
await this.assertBeforeBusinessAction('重置');
await this.clickButton('重置'); await this.clickButton('重置');
await this.waitForTableReady(); await this.waitForTableReady();
} }
......
...@@ -30,11 +30,13 @@ export class BasicUserUnionPage extends BasicUserPage { ...@@ -30,11 +30,13 @@ export class BasicUserUnionPage extends BasicUserPage {
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
const row = rows.nth(i); const row = rows.nth(i);
const sampleDept = await this.tryCell(row, '所属科室'); const sampleDept = await this.tryCell(row, '所属科室');
const samplePhone = await this.tryCell(row, '联系方式');
const phoneNorm = samplePhone.replace(/\s/g, '');
if (!sampleDept || sampleDept === '--') continue; if (!sampleDept || sampleDept === '--') continue;
if (!phoneNorm || samplePhone === '--' || phoneNorm.length < 4) continue;
const sampleUserName = await this.cellText(row, '姓名'); const sampleUserName = await this.cellText(row, '姓名');
const sampleLoginName = await this.cellText(row, '登录账号'); const sampleLoginName = await this.cellText(row, '登录账号');
const sampleHospital = await this.cellText(row, '所属医院'); const sampleHospital = await this.cellText(row, '所属医院');
const samplePhone = await this.tryCell(row, '联系方式');
const rawRole = await this.tryCell(row, '角色'); const rawRole = await this.tryCell(row, '角色');
const sampleRole = rawRole.replace(/[((].*$/, '').replace(/…|\.{3}.*/, '').trim(); const sampleRole = rawRole.replace(/[((].*$/, '').replace(/…|\.{3}.*/, '').trim();
const sampleStatus = await this.inferRowStatus(row); const sampleStatus = await this.inferRowStatus(row);
...@@ -98,16 +100,36 @@ export class BasicUserUnionPage extends BasicUserPage { ...@@ -98,16 +100,36 @@ export class BasicUserUnionPage extends BasicUserPage {
if (sample.sampleHospital) { if (sample.sampleHospital) {
await this.selectFormOption('所属医院', sample.sampleHospital).catch(() => undefined); await this.selectFormOption('所属医院', sample.sampleHospital).catch(() => undefined);
} }
await this.expandAdvanced();
const panel = this.page.locator('.ant-drawer-open, .ant-pro-table-search-query-filter, .advance-filter-panel').first();
const root = (await panel.isVisible().catch(() => false)) ? panel : this.page;
const phone = sample.samplePhone.replace(/\s/g, '');
if (phone && phone !== '--') {
const telInput = root.locator('#form_item_tel').first();
if (await telInput.isVisible().catch(() => false)) {
await this.fill(telInput, sample.samplePhone.trim(), '联系方式');
}
}
if (sample.sampleDept && sample.sampleDept !== '--') { if (sample.sampleDept && sample.sampleDept !== '--') {
await this.selectFormOption('科室', sample.sampleDept).catch(() => undefined); await this.selectFormOption('科室', sample.sampleDept).catch(() => undefined);
} }
if (sample.sampleRole) { if (sample.sampleRole) {
await this.selectFormOption('角色', sample.sampleRole).catch(() => undefined); await this.selectFormOption('角色', sample.sampleRole).catch(() => undefined);
} }
const filterBtn = this.locators.button('筛选', root).first();
if (await filterBtn.isVisible().catch(() => false)) {
await this.click(filterBtn, '筛 选');
} else {
await this.query();
}
await this.waitForTableReady();
}
async filterByDept(dept: string): Promise<void> {
await this.expandAdvanced(); await this.expandAdvanced();
const panel = this.page.locator('.ant-drawer-open, .ant-pro-table-search-query-filter, .advance-filter-panel').first(); const panel = this.page.locator('.ant-drawer-open, .ant-pro-table-search-query-filter, .advance-filter-panel').first();
const root = (await panel.isVisible().catch(() => false)) ? panel : this.page; const root = (await panel.isVisible().catch(() => false)) ? panel : this.page;
await this.selectFormOption('状态', sample.sampleStatus); await this.selectFormOption('科室', dept);
const filterBtn = this.locators.button('筛选', root).first(); const filterBtn = this.locators.button('筛选', root).first();
if (await filterBtn.isVisible().catch(() => false)) { if (await filterBtn.isVisible().catch(() => false)) {
await this.click(filterBtn, '筛 选'); await this.click(filterBtn, '筛 选');
......
This diff is collapsed.
...@@ -340,10 +340,10 @@ export class CenterReportSharedPage extends BasePage { ...@@ -340,10 +340,10 @@ export class CenterReportSharedPage extends BasePage {
const loadOnce = async () => { const loadOnce = async () => {
// 先等 iframe 挂上,避免在壳页上空等 searchBtn // 先等 iframe 挂上,避免在壳页上空等 searchBtn
const iframe = this.page.locator(iframeCssSelector(this.data.iframeSrc)).first(); const iframe = this.page.locator(iframeCssSelector(this.data.iframeSrc)).first();
await iframe.waitFor({ state: 'attached', timeout: Math.max(timeouts.pageReady, 25_000) }); await iframe.waitFor({ state: 'attached', timeout: timeouts.pageReady });
const frame = await iframe.elementHandle().then((handle) => handle?.contentFrame() ?? null); const frame = await iframe.elementHandle().then((handle) => handle?.contentFrame() ?? null);
await frame?.waitForLoadState('domcontentloaded').catch(() => undefined); await frame?.waitForLoadState('domcontentloaded').catch(() => undefined);
await this.waitVisible(this.locators.searchBtn, Math.max(timeouts.pageReady, 25_000)); 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);
if (this.data.hasAdvancedFilter) { if (this.data.hasAdvancedFilter) {
await this.waitVisible(this.locators.advancedFilter, timeouts.expect).catch(() => undefined); await this.waitVisible(this.locators.advancedFilter, timeouts.expect).catch(() => undefined);
...@@ -354,10 +354,9 @@ export class CenterReportSharedPage extends BasePage { ...@@ -354,10 +354,9 @@ export class CenterReportSharedPage extends BasePage {
.catch(() => undefined); .catch(() => undefined);
// 默认不等表就绪:显式 search() 再等,避免 open+search 双轮询 // 默认不等表就绪:显式 search() 再等,避免 open+search 双轮询
if (this.data.waitTableOnOpen) { if (this.data.waitTableOnOpen) {
await this.waitForTableReady().catch(() => undefined); await this.waitForTableReady({ health: 'ui' }).catch(() => undefined);
} }
} }
await this.assertPageHealthy('页面加载完成');
}; };
await retryReportLoad( await retryReportLoad(
...@@ -366,19 +365,24 @@ export class CenterReportSharedPage extends BasePage { ...@@ -366,19 +365,24 @@ export class CenterReportSharedPage extends BasePage {
await this.page.reload({ waitUntil: 'domcontentloaded' }).catch(() => undefined); await this.page.reload({ waitUntil: 'domcontentloaded' }).catch(() => undefined);
await this.goto(this.data.reportPath); await this.goto(this.data.reportPath);
}, },
(ms) => this.page.waitForTimeout(ms), (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
); );
} }
async search(): Promise<void> { async search(): Promise<void> {
await this.waitForSearchLoadingDone(); if (await isSearchButtonLoading(this.locators.searchBtn)) {
await waitAntSpinGone(this.locators.report); await this.waitForSearchLoadingDone();
}
if (await isAntSpinVisible(this.locators.report)) {
await waitAntSpinGone(this.locators.report);
}
if (!(await this.locators.searchBtn.isVisible().catch(() => false))) { if (!(await this.locators.searchBtn.isVisible().catch(() => false))) {
await this.ensureQuickFilterExpanded().catch(() => undefined); await this.ensureQuickFilterExpanded().catch(() => undefined);
} }
await this.assertBeforeBusinessAction('查询');
await this.click(this.locators.searchBtn, '点击查询'); await this.click(this.locators.searchBtn, '点击查询');
if (this.data.isChart) { if (this.data.isChart) {
await this.waitForSearchLoadingDone(); await waitSearchButtonIdle(this.locators.searchBtn, timeouts.searchLoading, timeouts.tableIdleGrace);
return; return;
} }
await this.waitForTableReady(); await this.waitForTableReady();
...@@ -390,7 +394,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -390,7 +394,7 @@ export class CenterReportSharedPage extends BasePage {
} }
/** 等待列表查询结束:loading 结束且有数据行或空态。 */ /** 等待列表查询结束:loading 结束且有数据行或空态。 */
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
if (this.page.isClosed()) return; if (this.page.isClosed()) return;
await this.locators.tableBody await this.locators.tableBody
.waitFor({ state: 'attached', timeout: timeouts.tableAttached }) .waitFor({ state: 'attached', timeout: timeouts.tableAttached })
...@@ -415,12 +419,13 @@ export class CenterReportSharedPage extends BasePage { ...@@ -415,12 +419,13 @@ export class CenterReportSharedPage extends BasePage {
if (await isAntSpinVisible(this.locators.report.locator('.ant-table-wrapper'))) return 'loading'; if (await isAntSpinVisible(this.locators.report.locator('.ant-table-wrapper'))) return 'loading';
const anyRow = await this.locators.tableBody.locator('tr').count().catch(() => 0); const anyRow = await this.locators.tableBody.locator('tr').count().catch(() => 0);
if (anyRow > 0) return 'ready'; if (anyRow > 0) return 'ready';
// 行或空态晚一拍渲染时不要立刻判空;超过 8 秒仍无 spin 再放行,避免无占位符的空表卡满超时 // 无 spin、按钮也不在 loading 时只留短窗口,避免空表卡满超时
if (Date.now() - started < 8_000) return 'loading'; if (Date.now() - started < timeouts.tableIdleGrace) return 'loading';
return 'idle'; return 'idle';
}, Math.max(timeouts.tableReady, 35_000)); }, timeouts.tableReady);
if (!this.page.isClosed()) { if (!this.page.isClosed()) {
await this.assertPageHealthy('查询完成后'); const mode = options?.health ?? 'business';
await this.assertAfterTableWait(mode === 'ui' ? '列表就绪' : '查询完成后', mode);
} }
} }
...@@ -1603,6 +1608,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -1603,6 +1608,7 @@ export class CenterReportSharedPage extends BasePage {
await executeExportReport({ await executeExportReport({
page: this.page, page: this.page,
prepareRowSelection: options?.prepareRowSelection, prepareRowSelection: options?.prepareRowSelection,
prepareBusinessAction: () => this.assertBeforeBusinessAction('导出'),
clickExport: () => clickExport: () =>
this.click(exportBtn, `点击${tableLabel}导出`, { operation: 'write' }), this.click(exportBtn, `点击${tableLabel}导出`, { operation: 'write' }),
hasVisibleErrorNotice: () => this.hasVisibleErrorNotice(), hasVisibleErrorNotice: () => this.hasVisibleErrorNotice(),
...@@ -2755,11 +2761,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -2755,11 +2761,7 @@ export class CenterReportSharedPage extends BasePage {
await this.selectQuickFormSelect(label, value); await this.selectQuickFormSelect(label, value);
return; return;
} }
if (await input.isVisible().catch(() => false)) { await this.fill(input, value, `填写${label}`);
await this.fill(input, value, `填写${label}`);
return;
}
await input.fill(value, { force: true, timeout: 8_000 }).catch(() => undefined);
return; return;
} }
if ( if (
...@@ -2792,11 +2794,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -2792,11 +2794,7 @@ export class CenterReportSharedPage extends BasePage {
await this.selectQuickFormSelect(label, value); await this.selectQuickFormSelect(label, value);
return; return;
} }
if (await byId.isVisible().catch(() => false)) { await this.fill(byId, value, `填写${label}`);
await this.fill(byId, value, `填写${label}`);
} else {
await byId.fill(value, { force: true, timeout: 8_000 }).catch(() => undefined);
}
} }
} }
} }
...@@ -3932,7 +3930,7 @@ export class CenterReportSharedPage extends BasePage { ...@@ -3932,7 +3930,7 @@ export class CenterReportSharedPage extends BasePage {
if (!found) { if (!found) {
await this.toggleFilterArea(); await this.toggleFilterArea();
await this.toggleFilterArea(); await this.toggleFilterArea();
await this.assertPageHealthy('收起展开筛选区后'); await this.assertUiReady('收起展开筛选区后');
return; return;
} }
......
This diff is collapsed.
...@@ -66,7 +66,9 @@ export type ExportReportOptions = { ...@@ -66,7 +66,9 @@ export type ExportReportOptions = {
prepareRowSelection?: () => Promise<void>; prepareRowSelection?: () => Promise<void>;
clickExport: () => Promise<void>; clickExport: () => Promise<void>;
hasVisibleErrorNotice?: () => Promise<boolean>; hasVisibleErrorNotice?: () => Promise<boolean>;
/** 页面/系统健康检查(报错文案、接口失败)。 */ /** 点击导出前:清空接口窗口并确认可交互(与 query 前置一致)。 */
prepareBusinessAction?: () => Promise<void>;
/** 导出完成后:业务接口 + 页面业务报错。 */
assertHealthy?: () => Promise<void>; assertHealthy?: () => Promise<void>;
}; };
...@@ -82,6 +84,10 @@ export async function executeExportReport(options: ExportReportOptions): Promise ...@@ -82,6 +84,10 @@ export async function executeExportReport(options: ExportReportOptions): Promise
await options.page.keyboard.press('Escape').catch(() => undefined); await options.page.keyboard.press('Escape').catch(() => undefined);
if (options.prepareBusinessAction) {
await options.prepareBusinessAction();
}
const waitMs = Math.max(timeouts.exportResponse, 60_000); const waitMs = Math.max(timeouts.exportResponse, 60_000);
const exportResponsePromise = options.page const exportResponsePromise = options.page
.waitForResponse(isExportPostResponse, { timeout: waitMs }) .waitForResponse(isExportPostResponse, { timeout: waitMs })
......
/** FineReport iframe 首次打开常超时或 503,加载失败后重新进页再等。 */ /** 仅服务不可用时重试;普通超时立即失败。 */
const RETRYABLE_LOAD = const RETRYABLE_LOAD = /503|herpService|SERVICE_UNAVAILABLE/i;
/503|herpService|SERVICE_UNAVAILABLE|Timeout|iframe\[src|waiting for locator|not attached/i;
export async function retryReportLoad( export async function retryReportLoad(
loadOnce: () => Promise<void>, loadOnce: () => Promise<void>,
reload: () => Promise<void>, reload: () => Promise<void>,
wait: (ms: number) => Promise<void>, wait: (ms: number) => Promise<void>,
maxAttempts = 3, maxAttempts = 2,
): Promise<void> { ): Promise<void> {
let lastError: unknown; let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
......
...@@ -4,8 +4,8 @@ import { env } from '../../../config/env.config'; ...@@ -4,8 +4,8 @@ import { env } from '../../../config/env.config';
import { applyCaseMeta } from '../../../core/case-meta'; import { applyCaseMeta } from '../../../core/case-meta';
import { attachFailureDiagnostics } from '../../../core/diagnostics'; import { attachFailureDiagnostics } from '../../../core/diagnostics';
import { attachRuntimeHealthGuard, getRuntimeHealthGuard } from '../../../core/runtime-health.util'; import { attachRuntimeHealthGuard, getRuntimeHealthGuard } from '../../../core/runtime-health.util';
import { slotForParallelIndex, storageStatePathForSlot } from '../../../utils/auth-pool.util';
import { ensureAuthStorageFresh, isPageOnLogin } from '../../../utils/auth-session'; import { ensureAuthStorageFresh, isPageOnLogin } from '../../../utils/auth-session';
import { AUTH_STATE_PATH } from '../../../utils/path.util';
// 必须与用例侧同一 test 实例,describe 嵌套才生效 // 必须与用例侧同一 test 实例,describe 嵌套才生效
import { test } from '../../../fixtures'; import { test } from '../../../fixtures';
...@@ -47,17 +47,21 @@ export function describeWithReuseOpen<T extends ReuseOpenReport>( ...@@ -47,17 +47,21 @@ export function describeWithReuseOpen<T extends ReuseOpenReport>(
let page: Page; let page: Page;
let report: T; let report: T;
let parallelIndex = 0;
const replaceSession = async (browser: Browser): Promise<void> => { const replaceSession = async (browser: Browser): Promise<void> => {
await page?.context().close().catch(() => undefined); await page?.context().close().catch(() => undefined);
({ page, report } = await createReusedReportSession(browser, factory, options)); ({ page, report } = await createReusedReportSession(browser, factory, options, parallelIndex));
}; };
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }, testInfo) => {
test.setTimeout(120_000); test.setTimeout(120_000);
({ page, report } = await createReusedReportSession(browser, factory, options)); parallelIndex = testInfo.parallelIndex;
({ page, report } = await createReusedReportSession(browser, factory, options, parallelIndex));
}); });
test.beforeEach(async ({ browser }, testInfo) => { test.beforeEach(async ({ browser }, testInfo) => {
parallelIndex = testInfo.parallelIndex;
applyCaseMeta(testInfo); applyCaseMeta(testInfo);
if (!page || page.isClosed() || (await isPageOnLogin(page))) { if (!page || page.isClosed() || (await isPageOnLogin(page))) {
await replaceSession(browser); await replaceSession(browser);
...@@ -110,11 +114,13 @@ export async function createReusedReportSession<T extends ReuseOpenReport>( ...@@ -110,11 +114,13 @@ export async function createReusedReportSession<T extends ReuseOpenReport>(
browser: Browser, browser: Browser,
factory: (page: Page) => T, factory: (page: Page) => T,
options: ReuseOpenOptions = {}, options: ReuseOpenOptions = {},
parallelIndex = 0,
): Promise<{ page: Page; report: T }> { ): Promise<{ page: Page; report: T }> {
await ensureAuthStorageFresh(browser, { honorForce: false }); const slot = slotForParallelIndex(parallelIndex);
await ensureAuthStorageFresh(browser, { honorForce: false, slot });
const context = await browser.newContext({ const context = await browser.newContext({
baseURL: env.baseURL, baseURL: env.baseURL,
storageState: AUTH_STATE_PATH, storageState: storageStatePathForSlot(slot),
}); });
const page = await context.newPage(); const page = await context.newPage();
attachRuntimeHealthGuard(page); attachRuntimeHealthGuard(page);
......
...@@ -120,7 +120,7 @@ export class ReagEntryPage extends ReconCostReportSharedPage { ...@@ -120,7 +120,7 @@ export class ReagEntryPage extends ReconCostReportSharedPage {
return this.listDataRows().count(); return this.listDataRows().count();
} }
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
await this.locators.tableBody.waitFor({ state: 'visible', timeout: timeouts.pageReady }).catch(() => undefined); await this.locators.tableBody.waitFor({ state: 'visible', timeout: timeouts.pageReady }).catch(() => undefined);
await this.waitForSearchLoadingDone(); await this.waitForSearchLoadingDone();
await waitUntilNotLoading(async () => { await waitUntilNotLoading(async () => {
...@@ -129,7 +129,8 @@ export class ReagEntryPage extends ReconCostReportSharedPage { ...@@ -129,7 +129,8 @@ export class ReagEntryPage extends ReconCostReportSharedPage {
if (await isTableEmptyVisible(this.locators.report)) return 'empty'; if (await isTableEmptyVisible(this.locators.report)) return 'empty';
return 'idle'; return 'idle';
}, timeouts.pageReady); }, timeouts.pageReady);
await this.assertPageHealthy('查询完成后'); const mode = options?.health ?? 'business';
await this.assertAfterTableWait(mode === 'ui' ? '列表就绪' : '查询完成后', mode);
} }
async searchAndExtractSample(rowIndex = this.data.sampleRowIndex): Promise<SampleRow> { async searchAndExtractSample(rowIndex = this.data.sampleRowIndex): Promise<SampleRow> {
...@@ -322,6 +323,7 @@ export class ReagEntryPage extends ReconCostReportSharedPage { ...@@ -322,6 +323,7 @@ export class ReagEntryPage extends ReconCostReportSharedPage {
async exportReagEntryReport(): Promise<void> { async exportReagEntryReport(): Promise<void> {
await executeExportReport({ await executeExportReport({
page: this.page, page: this.page,
prepareBusinessAction: () => this.assertBeforeBusinessAction('导出'),
clickExport: () => this.click(this.locators.exportBtn, '点击导出'), clickExport: () => this.click(this.locators.exportBtn, '点击导出'),
hasVisibleErrorNotice: () => this.hasVisibleErrorNotice(), hasVisibleErrorNotice: () => this.hasVisibleErrorNotice(),
assertHealthy: () => this.assertPageHealthy('导出后'), assertHealthy: () => this.assertPageHealthy('导出后'),
......
...@@ -492,6 +492,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage { ...@@ -492,6 +492,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
await expect(btn).toBeVisible({ timeout: 10_000 }); await expect(btn).toBeVisible({ timeout: 10_000 });
await executeExportReport({ await executeExportReport({
page: this.page, page: this.page,
prepareBusinessAction: () => this.assertBeforeBusinessAction('导出条码明细'),
clickExport: () => this.click(btn, '点击导出条码明细'), clickExport: () => this.click(btn, '点击导出条码明细'),
hasVisibleErrorNotice: () => this.hasVisibleErrorNotice(), hasVisibleErrorNotice: () => this.hasVisibleErrorNotice(),
assertHealthy: () => this.assertPageHealthy('导出条码明细后'), assertHealthy: () => this.assertPageHealthy('导出条码明细后'),
......
...@@ -47,7 +47,7 @@ export class SurgerySharedPage extends BasePage { ...@@ -47,7 +47,7 @@ export class SurgerySharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`打开 ${this.meta.path}`); await this.assertUiReady(`打开 ${this.meta.path}`);
expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/); expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/);
} }
...@@ -65,7 +65,7 @@ export class SurgerySharedPage extends BasePage { ...@@ -65,7 +65,7 @@ export class SurgerySharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(600); await this.waitSettle(600);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`菜单进入${this.meta.menu}`); await this.assertUiReady(`菜单进入${this.meta.menu}`);
} }
/** 等左树或主表挂载,避免菜单切入后尚未渲染。 */ /** 等左树或主表挂载,避免菜单切入后尚未渲染。 */
...@@ -74,7 +74,7 @@ export class SurgerySharedPage extends BasePage { ...@@ -74,7 +74,7 @@ export class SurgerySharedPage extends BasePage {
.locator('.ant-tree, .ant-table-thead th, .ant-table-header th, thead th, .ant-empty') .locator('.ant-tree, .ant-table-thead th, .ant-table-header th, thead th, .ant-empty')
.first(); .first();
await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined); await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined);
await this.waitForTableReady(); await this.waitForTableReady({ health: 'ui' });
} }
/** 断言已在目标页,且未误入同组其它页。 */ /** 断言已在目标页,且未误入同组其它页。 */
...@@ -93,6 +93,7 @@ export class SurgerySharedPage extends BasePage { ...@@ -93,6 +93,7 @@ export class SurgerySharedPage extends BasePage {
// ---------- 同组页签 ---------- // ---------- 同组页签 ----------
async switchTab(name: string, expectPathPart?: string): Promise<void> { async switchTab(name: string, expectPathPart?: string): Promise<void> {
await this.assertBeforeBusinessAction(`切换 Tab「${name}」`);
await this.click(this.locators.tab(name), `点击 Tab「${name}」`); await this.click(this.locators.tab(name), `点击 Tab「${name}」`);
if (expectPathPart) { if (expectPathPart) {
await this.page.waitForURL((url) => url.pathname.includes(expectPathPart), { timeout: 20_000 }).catch(() => undefined); await this.page.waitForURL((url) => url.pathname.includes(expectPathPart), { timeout: 20_000 }).catch(() => undefined);
...@@ -100,7 +101,6 @@ export class SurgerySharedPage extends BasePage { ...@@ -100,7 +101,6 @@ export class SurgerySharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(500); await this.waitSettle(500);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`切换 Tab「${name}」`);
} }
async expectTabPath(pathPart: string): Promise<void> { async expectTabPath(pathPart: string): Promise<void> {
...@@ -224,11 +224,13 @@ export class SurgerySharedPage extends BasePage { ...@@ -224,11 +224,13 @@ export class SurgerySharedPage extends BasePage {
} }
async query(): Promise<void> { async query(): Promise<void> {
await this.assertBeforeBusinessAction('查询');
await this.click(this.locators.button('查 询').first(), '查 询'); await this.click(this.locators.button('查 询').first(), '查 询');
await this.waitForTableReady(); await this.waitForTableReady();
} }
async reset(): Promise<void> { async reset(): Promise<void> {
await this.assertBeforeBusinessAction('重置');
await this.click(this.locators.button('重 置').first(), '重 置'); await this.click(this.locators.button('重 置').first(), '重 置');
await this.waitForTableReady(); await this.waitForTableReady();
} }
...@@ -257,10 +259,10 @@ export class SurgerySharedPage extends BasePage { ...@@ -257,10 +259,10 @@ export class SurgerySharedPage extends BasePage {
return match ? Number(match[1]) : 0; return match ? Number(match[1]) : 0;
} }
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(200); await this.waitSettle(200);
await this.assertPageHealthy('列表加载'); await this.assertAfterTableWait('列表加载', options?.health ?? 'business');
} }
async headerCells(): Promise<Locator> { async headerCells(): Promise<Locator> {
......
...@@ -178,7 +178,10 @@ export class VendorCertDiffPage extends VendorSharedPage { ...@@ -178,7 +178,10 @@ export class VendorCertDiffPage extends VendorSharedPage {
} }
await input.fill(' '); await input.fill(' ');
const queryBtn = root.getByRole('button', { name: /查\s*询/ }).first(); const queryBtn = root.getByRole('button', { name: /查\s*询/ }).first();
if (await queryBtn.isVisible().catch(() => false)) await queryBtn.click(); if (await queryBtn.isVisible().catch(() => false)) {
await this.assertBeforeBusinessAction('查询');
await queryBtn.click();
}
await this.waitSettle(800); await this.waitSettle(800);
await this.assertPageHealthy('空格物料名称搜索'); await this.assertPageHealthy('空格物料名称搜索');
await this.expectListOrEmptyInFrame(); await this.expectListOrEmptyInFrame();
...@@ -195,7 +198,7 @@ export class VendorCertDiffPage extends VendorSharedPage { ...@@ -195,7 +198,7 @@ export class VendorCertDiffPage extends VendorSharedPage {
} }
const text = ((await rows.first().innerText().catch(() => '')) ?? '').replace(/\s/g, ''); const text = ((await rows.first().innerText().catch(() => '')) ?? '').replace(/\s/g, '');
expect(text.length, '一致行应有内容').toBeGreaterThan(0); expect(text.length, '一致行应有内容').toBeGreaterThan(0);
await this.assertPageHealthy('一致行抽样'); await this.assertUiReady('一致行抽样');
} }
/** CSV VENDOR-CDF-011:不一致行至少一侧差异。 */ /** CSV VENDOR-CDF-011:不一致行至少一侧差异。 */
...@@ -209,6 +212,6 @@ export class VendorCertDiffPage extends VendorSharedPage { ...@@ -209,6 +212,6 @@ export class VendorCertDiffPage extends VendorSharedPage {
} }
const text = ((await rows.first().innerText().catch(() => '')) ?? '').replace(/\s/g, ''); const text = ((await rows.first().innerText().catch(() => '')) ?? '').replace(/\s/g, '');
expect(text.length, '不一致行应有内容').toBeGreaterThan(0); expect(text.length, '不一致行应有内容').toBeGreaterThan(0);
await this.assertPageHealthy('不一致行抽样'); await this.assertUiReady('不一致行抽样');
} }
} }
...@@ -115,7 +115,7 @@ export class VendorEvalSchemePage extends VendorSharedPage { ...@@ -115,7 +115,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
const formErr = await this.locators.formError.first().isVisible().catch(() => false); const formErr = await this.locators.formError.first().isVisible().catch(() => false);
expect(blocked || formErr || (await layer.first().isVisible()), '超长应被约束或仍停留编辑层').toBeTruthy(); expect(blocked || formErr || (await layer.first().isVisible()), '超长应被约束或仍停留编辑层').toBeTruthy();
} }
await this.assertPageHealthy('方案名称超长'); await this.assertUiReady('方案名称超长');
await this.leaveWithoutCommit(); await this.leaveWithoutCommit();
} }
......
...@@ -104,7 +104,7 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -104,7 +104,7 @@ export class VendorHolidayPage extends VendorSharedPage {
await this.click(submit, '非法日期提交'); await this.click(submit, '非法日期提交');
await this.expectBlockedAndStay(); await this.expectBlockedAndStay();
} else { } else {
await this.assertPageHealthy('结束早于开始(提交禁用或未点)'); await this.assertUiReady('结束早于开始(提交禁用或未点)');
} }
await this.leaveWithoutCommit(); await this.leaveWithoutCommit();
} }
...@@ -126,7 +126,7 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -126,7 +126,7 @@ export class VendorHolidayPage extends VendorSharedPage {
await inputs.nth(0).fill(day); await inputs.nth(0).fill(day);
await inputs.nth(1).fill(day); await inputs.nth(1).fill(day);
await this.page.keyboard.press('Enter').catch(() => undefined); await this.page.keyboard.press('Enter').catch(() => undefined);
await this.assertPageHealthy('同日区间'); await this.assertUiReady('同日区间');
await this.leaveWithoutCommit(); await this.leaveWithoutCommit();
} }
...@@ -143,6 +143,6 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -143,6 +143,6 @@ export class VendorHolidayPage extends VendorSharedPage {
} }
const name = ((await supplierCell.innerText()) ?? '').trim(); const name = ((await supplierCell.innerText()) ?? '').trim();
expect(name.length, '进院供应商名称非空').toBeGreaterThan(0); expect(name.length, '进院供应商名称非空').toBeGreaterThan(0);
await this.assertPageHealthy('进院供应商主数据抽样'); await this.assertUiReady('进院供应商主数据抽样');
} }
} }
...@@ -46,6 +46,7 @@ export class VendorQueryPage extends VendorSharedPage { ...@@ -46,6 +46,7 @@ export class VendorQueryPage extends VendorSharedPage {
} }
async searchByName(name: string): Promise<void> { async searchByName(name: string): Promise<void> {
await this.assertBeforeBusinessAction('查询');
await this.fill(this.nameSearch(), name, '填写厂商名称'); await this.fill(this.nameSearch(), name, '填写厂商名称');
await this.page.keyboard.press('Enter'); await this.page.keyboard.press('Enter');
await this.waitForTableReady(); await this.waitForTableReady();
...@@ -75,7 +76,6 @@ export class VendorQueryPage extends VendorSharedPage { ...@@ -75,7 +76,6 @@ export class VendorQueryPage extends VendorSharedPage {
async searchSpacesOnly(): Promise<void> { async searchSpacesOnly(): Promise<void> {
await this.searchByName(' '); await this.searchByName(' ');
await this.assertPageHealthy('空格关键字搜索');
} }
/** 打开资质管理并用同源关键字检索;返回列表快照供用例断言。 */ /** 打开资质管理并用同源关键字检索;返回列表快照供用例断言。 */
......
...@@ -37,7 +37,7 @@ export class VendorSharedPage extends BasePage { ...@@ -37,7 +37,7 @@ export class VendorSharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`打开 ${this.meta.path}`); await this.assertUiReady(`打开 ${this.meta.path}`);
expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/); expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/);
} }
...@@ -56,7 +56,7 @@ export class VendorSharedPage extends BasePage { ...@@ -56,7 +56,7 @@ export class VendorSharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(600); await this.waitSettle(600);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`菜单进入${this.meta.menu}`); await this.assertUiReady(`菜单进入${this.meta.menu}`);
} }
/** 等主表或关键控件出现,避免菜单切入后表头尚未挂载。 */ /** 等主表或关键控件出现,避免菜单切入后表头尚未挂载。 */
...@@ -65,7 +65,7 @@ export class VendorSharedPage extends BasePage { ...@@ -65,7 +65,7 @@ export class VendorSharedPage extends BasePage {
.locator('.ant-table-thead th, .ant-table-header th, thead th, .ant-empty, .ant-tree, [placeholder="厂商名称"]') .locator('.ant-table-thead th, .ant-table-header th, thead th, .ant-empty, .ant-tree, [placeholder="厂商名称"]')
.first(); .first();
await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined); await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined);
await this.waitForTableReady(); await this.waitForTableReady({ health: 'ui' });
} }
async expectOnPage(): Promise<void> { async expectOnPage(): Promise<void> {
...@@ -90,11 +90,13 @@ export class VendorSharedPage extends BasePage { ...@@ -90,11 +90,13 @@ export class VendorSharedPage extends BasePage {
} }
async query(): Promise<void> { async query(): Promise<void> {
await this.assertBeforeBusinessAction('查询');
await this.clickButton('查询'); await this.clickButton('查询');
await this.waitForTableReady(); await this.waitForTableReady();
} }
async reset(): Promise<void> { async reset(): Promise<void> {
await this.assertBeforeBusinessAction('重置');
await this.clickButton('重置'); await this.clickButton('重置');
await this.waitForTableReady(); await this.waitForTableReady();
} }
...@@ -137,6 +139,7 @@ export class VendorSharedPage extends BasePage { ...@@ -137,6 +139,7 @@ export class VendorSharedPage extends BasePage {
async clickTab(name: string): Promise<void> { async clickTab(name: string): Promise<void> {
const tab = this.tab(name); const tab = this.tab(name);
await expect(tab, `Tab「${name}」`).toBeVisible({ timeout: 15_000 }); await expect(tab, `Tab「${name}」`).toBeVisible({ timeout: 15_000 });
await this.assertBeforeBusinessAction(`切换${name}`);
await this.click(tab, `切换${name}`); await this.click(tab, `切换${name}`);
await this.waitForTableReady(); await this.waitForTableReady();
} }
...@@ -216,10 +219,10 @@ export class VendorSharedPage extends BasePage { ...@@ -216,10 +219,10 @@ export class VendorSharedPage extends BasePage {
return match ? Number(match[1]) : 0; return match ? Number(match[1]) : 0;
} }
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(200); await this.waitSettle(200);
await this.assertPageHealthy('列表加载'); await this.assertAfterTableWait('列表加载', options?.health ?? 'business');
} }
async cellText(row: Locator, header: string): Promise<string> { async cellText(row: Locator, header: string): Promise<string> {
...@@ -322,6 +325,7 @@ export class VendorSharedPage extends BasePage { ...@@ -322,6 +325,7 @@ export class VendorSharedPage extends BasePage {
} }
async openDetailLayer(row: Locator, action = '详情'): Promise<void> { async openDetailLayer(row: Locator, action = '详情'): Promise<void> {
await this.assertBeforeBusinessAction(action);
await this.click(this.rowAction(row, action), action); await this.click(this.rowAction(row, action), action);
const layer = this.locators.putPage().or(this.locators.dialog()).or(this.page.locator('.ant-drawer-open')); const layer = this.locators.putPage().or(this.locators.dialog()).or(this.page.locator('.ant-drawer-open'));
await expect(layer.first(), `${action}应打开详情层`).toBeVisible({ timeout: 15_000 }); await expect(layer.first(), `${action}应打开详情层`).toBeVisible({ timeout: 15_000 });
...@@ -367,7 +371,8 @@ export class VendorSharedPage extends BasePage { ...@@ -367,7 +371,8 @@ export class VendorSharedPage extends BasePage {
await this.click(this.locators.nextPage, '下一页'); await this.click(this.locators.nextPage, '下一页');
} }
await this.waitForTableReady(); await this.waitForTableReady();
expect(await this.getDataRowCount(), '第 2 页应有数据或可空').toBeGreaterThanOrEqual(0); await expect(this.page.locator('.ant-pagination-item-active').first(), '当前页应为第 2 页').toHaveText('2');
expect(await this.getDataRowCount(), '第 2 页应有数据').toBeGreaterThan(0);
} }
async closeOverlay(): Promise<void> { async closeOverlay(): Promise<void> {
......
...@@ -38,7 +38,7 @@ export class WhFlowSharedPage extends BasePage { ...@@ -38,7 +38,7 @@ export class WhFlowSharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(400); await this.waitSettle(400);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`打开 ${this.meta.path}`); await this.assertUiReady(`打开 ${this.meta.path}`);
expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/); expect(this.page.url(), 'storageState 失效回到登录页').not.toMatch(/\/login/);
} }
...@@ -61,7 +61,7 @@ export class WhFlowSharedPage extends BasePage { ...@@ -61,7 +61,7 @@ export class WhFlowSharedPage extends BasePage {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(600); await this.waitSettle(600);
await this.waitContentReady(); await this.waitContentReady();
await this.assertPageHealthy(`菜单进入${this.meta.menu}`); await this.assertUiReady(`菜单进入${this.meta.menu}`);
} }
async waitContentReady(): Promise<void> { async waitContentReady(): Promise<void> {
...@@ -71,7 +71,7 @@ export class WhFlowSharedPage extends BasePage { ...@@ -71,7 +71,7 @@ export class WhFlowSharedPage extends BasePage {
) )
.first(); .first();
await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined); await markers.waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined);
await this.waitForTableReady(); await this.waitForTableReady({ health: 'ui' });
} }
async expectOnPage(): Promise<void> { async expectOnPage(): Promise<void> {
...@@ -91,11 +91,13 @@ export class WhFlowSharedPage extends BasePage { ...@@ -91,11 +91,13 @@ export class WhFlowSharedPage extends BasePage {
} }
async query(): Promise<void> { async query(): Promise<void> {
await this.assertBeforeBusinessAction('查询');
await this.clickButton('查询'); await this.clickButton('查询');
await this.waitForTableReady(); await this.waitForTableReady();
} }
async reset(): Promise<void> { async reset(): Promise<void> {
await this.assertBeforeBusinessAction('重置');
await this.clickButton('重置'); await this.clickButton('重置');
await this.waitForTableReady(); await this.waitForTableReady();
} }
...@@ -197,10 +199,10 @@ export class WhFlowSharedPage extends BasePage { ...@@ -197,10 +199,10 @@ export class WhFlowSharedPage extends BasePage {
return match ? Number(match[1]) : 0; return match ? Number(match[1]) : 0;
} }
async waitForTableReady(): Promise<void> { async waitForTableReady(options?: { health?: 'ui' | 'business' }): Promise<void> {
await this.waitSpinGone(); await this.waitSpinGone();
await this.waitSettle(200); await this.waitSettle(200);
await this.assertPageHealthy('列表加载'); await this.assertAfterTableWait('列表加载', options?.health ?? 'business');
} }
async cellText(row: Locator, header: string): Promise<string> { async cellText(row: Locator, header: string): Promise<string> {
......
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