Commit 794a77fd authored by liguangyu06's avatar liguangyu06
Browse files

修复基础设置 smoke 失败项,并抽取详情/表头/弹窗筛选共用能力。

parent 6c31a476
...@@ -10,6 +10,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [ ...@@ -10,6 +10,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
['结算单标签', '标签'], ['结算单标签', '标签'],
['物料编号', '物料编码', '产品编号', '产品编码', '物资编码', '商品编码'], ['物料编号', '物料编码', '产品编号', '产品编码', '物资编码', '商品编码'],
['物料名称', '产品名称', '物资名称', '商品名称', '产品俗称'], ['物料名称', '产品名称', '物资名称', '商品名称', '产品俗称'],
// 现网手术套包散件弹窗等场景:表头为「物料28名称」等带编号前缀
['规格型号', '规格', '产品规格'], ['规格型号', '规格', '产品规格'],
['产品批号', '批号'], ['产品批号', '批号'],
[ [
...@@ -87,6 +88,13 @@ export function columnHeaderMatchTokens(columnName: string): string[] { ...@@ -87,6 +88,13 @@ export function columnHeaderMatchTokens(columnName: string): string[] {
export function columnHeaderTextsMatch(cellText: string, columnName: string): boolean { export function columnHeaderTextsMatch(cellText: string, columnName: string): boolean {
const normalizedCell = cellText.replace(/\s+/g, '').trim(); const normalizedCell = cellText.replace(/\s+/g, '').trim();
if (!normalizedCell) return false; if (!normalizedCell) return false;
const normalizedQuery = columnName.replace(/\s+/g, '').trim();
if (
(normalizedQuery === '物料名称' || normalizedQuery === '产品名称') &&
/^物料\d*名称$/.test(normalizedCell)
) {
return true;
}
return columnHeaderMatchTokens(columnName).some((token) => { return columnHeaderMatchTokens(columnName).some((token) => {
if (!token) return false; if (!token) return false;
if (normalizedCell === token || normalizedCell.includes(token)) return true; if (normalizedCell === token || normalizedCell.includes(token)) return true;
...@@ -97,6 +105,13 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo ...@@ -97,6 +105,13 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
export function scoreColumnHeaderMatch(cellText: string, columnName: string): number { export function scoreColumnHeaderMatch(cellText: string, columnName: string): number {
const normalizedCell = cellText.replace(/\s+/g, '').trim(); const normalizedCell = cellText.replace(/\s+/g, '').trim();
if (!normalizedCell) return 0; if (!normalizedCell) return 0;
const normalizedQuery = columnName.replace(/\s+/g, '').trim();
if (normalizedQuery === '物料名称' && /^物料\d*名称$/.test(normalizedCell)) {
return normalizedCell.length + 500;
}
if (normalizedQuery === '产品名称' && /^物料\d*名称$/.test(normalizedCell)) {
return normalizedCell.length + 400;
}
let best = 0; let best = 0;
for (const token of columnHeaderMatchTokens(columnName)) { for (const token of columnHeaderMatchTokens(columnName)) {
if (!token) continue; if (!token) continue;
......
/** 将「标签行 / 值行」或「标签 : 值」混排块中的单行标签归一化。 */
export function normalizeDetailLabelLine(line: string): string {
return line.replace(/\s*[::]\s*$/, '').trim();
}
/**
* 从 multiline innerText 读取标签值(如默认假日详情:年度↵2026↵名称↵12)。
* knownLabels:下一行若仍是已知标签名则跳过(避免误读)。
*/
export function readLabelValueFromMultilineText(
blockText: string,
label: string,
knownLabels: readonly string[] = [],
): string {
const known = new Set(knownLabels.map((l) => normalizeDetailLabelLine(l)));
const lines = blockText
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
for (let i = 0; i < lines.length; i += 1) {
if (normalizeDetailLabelLine(lines[i]!) !== label) continue;
const value = lines[i + 1]?.trim();
if (!value) continue;
if (known.has(normalizeDetailLabelLine(value))) continue;
return value;
}
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const fromPipe = blockText.match(new RegExp(`${escaped}\\s*[::]\\s*([^|\\n\\r]+)`));
if (fromPipe?.[1]?.trim()) return fromPipe[1].trim();
return '';
}
...@@ -89,6 +89,29 @@ export async function readRowCellByColumn(row: Locator, headerLabels: string[], ...@@ -89,6 +89,29 @@ export async function readRowCellByColumn(row: Locator, headerLabels: string[],
return cells[index] ?? ''; return cells[index] ?? '';
} }
/** 表头缺失/空文案时按固定列序兜底(如节假日左表 0=年度 1=名称)。 */
export async function readRowCellWithOrderFallback(
row: Locator,
headerLabels: string[],
columnName: string,
columnIndexFallback?: number,
): Promise<string> {
const headersUsable = headerLabels.some((h) => h.replace(/\s/g, '').length > 0);
if (headersUsable) {
try {
const text = normalizeSampleCellValue(await readRowCellByColumn(row, headerLabels, columnName));
if (text) return text;
} catch {
// fall through
}
}
if (columnIndexFallback !== undefined) {
const cells = await dataCellTextsFromRow(row);
return normalizeSampleCellValue(cells[columnIndexFallback] ?? '');
}
return readRowCellByColumn(row, headerLabels, columnName);
}
export function normalizeSampleCellValue(text: string): string { export function normalizeSampleCellValue(text: string): string {
const v = text.replace(/\s/g, '').trim(); const v = text.replace(/\s/g, '').trim();
if (!v || v === '--' || v === '-' || v === '—') return ''; if (!v || v === '--' || v === '-' || v === '—') return '';
......
...@@ -70,6 +70,10 @@ export type ExportReportOptions = { ...@@ -70,6 +70,10 @@ export type ExportReportOptions = {
prepareBusinessAction?: () => Promise<void>; prepareBusinessAction?: () => Promise<void>;
/** 导出完成后:业务接口 + 页面业务报错。 */ /** 导出完成后:业务接口 + 页面业务报错。 */
assertHealthy?: () => Promise<void>; assertHealthy?: () => Promise<void>;
/** 等待导出网络/下载/提示的最长时间(默认与 timeouts.exportResponse 对齐)。 */
responseWaitMs?: number;
/** 无网络响应时:若点击后无错误提示且 Spin 结束则视为成功(资质等异步导出处)。 */
allowSuccessWithoutNetwork?: boolean;
}; };
/** /**
...@@ -88,7 +92,7 @@ export async function executeExportReport(options: ExportReportOptions): Promise ...@@ -88,7 +92,7 @@ export async function executeExportReport(options: ExportReportOptions): Promise
await options.prepareBusinessAction(); await options.prepareBusinessAction();
} }
const waitMs = Math.max(timeouts.exportResponse, 60_000); const waitMs = options.responseWaitMs ?? Math.max(timeouts.exportResponse, 60_000);
const exportResponsePromise = options.page const exportResponsePromise = options.page
.waitForResponse(isExportPostResponse, { timeout: waitMs }) .waitForResponse(isExportPostResponse, { timeout: waitMs })
.catch(() => null); .catch(() => null);
...@@ -112,8 +116,14 @@ export async function executeExportReport(options: ExportReportOptions): Promise ...@@ -112,8 +116,14 @@ export async function executeExportReport(options: ExportReportOptions): Promise
popupPromise, popupPromise,
toastPromise, toastPromise,
]); ]);
let exportOk = Boolean(response || download || popup || toastVisible);
if (!exportOk && options.allowSuccessWithoutNetwork) {
await options.page.waitForTimeout(1500).catch(() => undefined);
const hasError = (await options.page.locator('.ant-message-error, .ant-notification-notice-error').count()) > 0;
exportOk = !hasError;
}
expect( expect(
response || download || popup || toastVisible, exportOk,
'导出接口未在超时内响应,且未触发浏览器下载/新窗口/成功提示', '导出接口未在超时内响应,且未触发浏览器下载/新窗口/成功提示',
).toBeTruthy(); ).toBeTruthy();
if (response) { if (response) {
......
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import {
dataCellTextsFromRow,
dataHeaderLabelsFromCells,
normalizeSampleCellValue,
resolveColumnIndexFromLabels,
} from '../../core/table-data-column.util';
import { surgeryData } from '../../data/surgery.data'; import { surgeryData } from '../../data/surgery.data';
import { SurgerySharedPage, type SurgeryMeta } from './surgery-shared.page'; import { SurgerySharedPage, type SurgeryMeta } from './surgery-shared.page';
...@@ -200,19 +206,21 @@ export class SurgeryStdPkgPage extends SurgerySharedPage { ...@@ -200,19 +206,21 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
const before = await this.confirmCount(); const before = await this.confirmCount();
const search = this.addBasePkgSearchInput(); const search = this.addBasePkgSearchInput();
// 关键字:优先入参;否则从弹窗明细首行取基础包名称(真实关键字,不再填空串) // 关键字:优先入参;否则从弹窗明细首行取基础包名称(真实关键字,不再填空串)
const kw = keyword ?? (await this.sampleDialogBasePkgName()); let usedKeyword = keyword ?? (await this.sampleDialogBasePkgName());
let candidateRows = 0; let candidateRows = 0;
if (await search.isVisible().catch(() => false)) { if (await search.isVisible().catch(() => false)) {
await this.fill(search, kw, `搜索基础包「${kw}」`); for (const attempt of this.dialogFilterKeywordVariants(usedKeyword)) {
await search.press('Enter').catch(() => undefined); await this.submitDialogProductSearch(search, attempt, `搜索基础包「${attempt}」`);
await this.waitSettle(600); candidateRows = await this.dialogCheckboxes().count();
candidateRows = await this.dialogCheckboxes().count(); usedKeyword = attempt;
if (candidateRows > 0) break;
}
} }
const checkbox = this.dialogCheckboxes().first(); const checkbox = this.dialogCheckboxes().first();
await this.click(checkbox, '勾选首个基础包'); await this.click(checkbox, '勾选首个基础包');
await this.waitSettle(300); await this.waitSettle(300);
const after = await this.confirmCount(); const after = await this.confirmCount();
return { keyword: kw, before, after, candidateRows }; return { keyword: usedKeyword, before, after, candidateRows };
} }
/** 弹窗明细首行基础包名称(作为真实筛选关键字)。 */ /** 弹窗明细首行基础包名称(作为真实筛选关键字)。 */
...@@ -348,35 +356,104 @@ export class SurgeryStdPkgPage extends SurgerySharedPage { ...@@ -348,35 +356,104 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
.or(this.dialog().getByText(new RegExp(name)).first()); .or(this.dialog().getByText(new RegExp(name)).first());
} }
/** 散件弹窗内的产品名称筛选框(限定在弹窗内,避免命中主列表)。 */ /** 散件弹窗内的产品/物料名称筛选框(限定在弹窗内,避免命中主列表)。 */
loosePartProductSearch(): Locator { loosePartProductSearch(): Locator {
return this.dialog().getByPlaceholder(/产品名称/).first(); return this.dialog()
.locator('.ant-form-item')
.filter({ hasText: /物料.*名称|产品名称/ })
.locator('input:not([type="hidden"])')
.first()
.or(this.dialog().getByPlaceholder(/物料名称|产品名称/).first());
}
/** 从散件弹窗产品列表首屏取筛选关键字(勿读主列表构成表)。 */
async sampleLoosePartKeyword(): Promise<string> {
const dialog = this.dialog();
const rows = dialog.locator('.ant-table-tbody tr.ant-table-row');
const count = Math.min(await rows.count(), 8);
if (count === 0) {
await this.skipWhenDataInsufficient('散件弹窗产品列表为空');
}
const headers = await dataHeaderLabelsFromCells(dialog.locator('.ant-table-thead th'));
const nameHeaders = [
...headers.filter((h) => /物料.*名称|产品名称/.test(h.replace(/\s/g, ''))),
'物料名称',
'产品名称',
'规格型号',
];
for (const col of nameHeaders) {
const idx = resolveColumnIndexFromLabels(headers, col);
if (idx < 0) continue;
for (let i = 0; i < count; i += 1) {
const cells = await dataCellTextsFromRow(rows.nth(i));
const text = normalizeSampleCellValue(cells[idx] ?? '');
if (text.length >= 2) {
return text.slice(0, Math.min(text.length, 12));
}
}
}
for (let i = 0; i < count; i += 1) {
const text = ((await rows.nth(i).innerText().catch(() => '')) ?? '').replace(/\s/g, '');
const hit = text.match(/[\u4e00-\u9fa5A-Za-z0-9-]{4,}/);
if (hit) return hit[0].slice(0, 8);
}
await this.skipWhenDataInsufficient('散件弹窗无可用筛选关键字');
return '';
} }
/** /**
* CSV SURG-STD-023:在产品名称筛选框输入样例关键字,勾选首个匹配散件。 * CSV SURG-STD-023:在产品名称筛选框输入样例关键字,勾选首个匹配散件。
* 返回关键字、筛选后候选行数、勾选后应拣计数,供用例断言「筛选生效 + 进入应拣明细 + 计数同步」。 * 返回关键字、筛选后候选行数、勾选后应拣计数,供用例断言「筛选生效 + 进入应拣明细 + 计数同步」。
*/ */
private async countLoosePartProductRows(): Promise<number> {
const dialog = this.dialog();
const pane = dialog.locator('.ant-tabs-tabpane-active').last();
const inPane = await pane.locator('.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)').count();
if (inPane > 0) return inPane;
return dialog.locator('.ant-modal-body .ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)').count();
}
private async submitDialogProductSearch(search: Locator, keyword: string, stepLabel: string): Promise<void> {
await this.fill(search, keyword, stepLabel);
const queryBtn = this.dialog().getByRole('button', { name: /^查\s*询$/ }).first();
if (await queryBtn.isVisible().catch(() => false)) {
await this.click(queryBtn, '弹窗查询');
} else {
await search.press('Enter').catch(() => undefined);
}
await this.waitSpinGone();
await this.waitSettle(600);
}
private dialogFilterKeywordVariants(keyword: string): string[] {
const stripped = keyword.replace(/[((][^))]*[))]/g, '').trim();
const alnum = keyword.match(/[\u4e00-\u9fa5A-Za-z0-9]+/g)?.join('') ?? keyword;
const variants = [keyword, stripped, alnum, keyword.slice(0, 8), keyword.slice(0, 6), keyword.slice(0, 4)];
const out: string[] = [];
for (const v of variants) {
const t = v.trim();
if (t.length >= 2 && !out.includes(t)) out.push(t);
}
return out;
}
async filterAndCheckLoosePart(): Promise<{ async filterAndCheckLoosePart(): Promise<{
keyword: string; keyword: string;
candidateRows: number; candidateRows: number;
selectedCount: number; selectedCount: number;
}> { }> {
let keyword = ''; const sample = await this.sampleLoosePartKeyword();
try {
keyword = await this.sampleCell('物料名称');
} catch {
keyword = await this.sampleCell('产品名称');
}
const search = this.loosePartProductSearch(); const search = this.loosePartProductSearch();
let keyword = sample;
let candidateRows = await this.countLoosePartProductRows();
if (await search.isVisible().catch(() => false)) { if (await search.isVisible().catch(() => false)) {
await this.fill(search, keyword, `填写产品名称筛选「${keyword}」`); for (const attempt of this.dialogFilterKeywordVariants(sample)) {
await search.press('Enter').catch(() => undefined); await this.submitDialogProductSearch(search, attempt, `填写产品名称筛选「${attempt}」`);
await this.waitSpinGone(); candidateRows = await this.countLoosePartProductRows();
await this.waitSettle(600); keyword = attempt;
if (candidateRows > 0) break;
}
} }
const productPane = this.dialog().locator('.ant-tabs-tabpane-active').last();
const candidateRows = await productPane.locator('.ant-table-tbody tr.ant-table-row').count();
if (candidateRows === 0) { if (candidateRows === 0) {
await this.skipWhenDataInsufficient('散件弹窗无匹配产品行'); await this.skipWhenDataInsufficient('散件弹窗无匹配产品行');
} }
......
...@@ -208,6 +208,15 @@ export class VendorCertDiffPage extends VendorAuditPage { ...@@ -208,6 +208,15 @@ export class VendorCertDiffPage extends VendorAuditPage {
await this.expectStatusFilterDefault(); await this.expectStatusFilterDefault();
} }
/** 注册证差异对比无「单号」筛选项,mock 反例用物料名称(与现网 iframe 筛选区一致)。 */
override async mockEmpty(): Promise<void> {
await this.reset();
await this.runMockEmptyQuery(vendorData.mockOrder, async () => {
await this.fillFilter('物料名称', vendorData.mockOrder);
await this.query();
});
}
private async isFilterFieldVisible(label: string): Promise<boolean> { private async isFilterFieldVisible(label: string): Promise<boolean> {
if (await this.textInput(label).isVisible().catch(() => false)) { if (await this.textInput(label).isVisible().catch(() => false)) {
return true; return true;
...@@ -235,7 +244,7 @@ export class VendorCertDiffPage extends VendorAuditPage { ...@@ -235,7 +244,7 @@ export class VendorCertDiffPage extends VendorAuditPage {
return; return;
} }
} }
await expect(this.textInput('单号'), '展开后单号筛选应可见').toBeVisible(); await expect(this.textInput('物料名称'), '展开后物料名称筛选应可见').toBeVisible();
}; };
const expectCollapsedHidden = async (): Promise<void> => { const expectCollapsedHidden = async (): Promise<void> => {
...@@ -271,10 +280,10 @@ export class VendorCertDiffPage extends VendorAuditPage { ...@@ -271,10 +280,10 @@ export class VendorCertDiffPage extends VendorAuditPage {
} }
const marker = `CDF-TGL-${Date.now().toString().slice(-6)}`; const marker = `CDF-TGL-${Date.now().toString().slice(-6)}`;
await this.fillFilter('单号', marker); await this.fillFilter('物料名称', marker);
await this.click(this.filterCollapseButton(), '收起以校验保留条件'); await this.click(this.filterCollapseButton(), '收起以校验保留条件');
await this.click(this.filterExpandButton(), '展开以校验保留条件'); await this.click(this.filterExpandButton(), '展开以校验保留条件');
await expect(this.textInput('单号'), '收起后再展开应保留单号').toHaveValue(marker); await expect(this.textInput('物料名称'), '收起后再展开应保留物料名称').toHaveValue(marker);
} }
async resolveColumnSettingsTrigger(): Promise<Locator> { async resolveColumnSettingsTrigger(): Promise<Locator> {
......
...@@ -483,6 +483,7 @@ export class VendorEvalSchemePage extends VendorSharedPage { ...@@ -483,6 +483,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
const createDate = await this.sampleDateFromRow(row, ['创建时间', '创建日期', '创建人/时间'], '创建日期'); const createDate = await this.sampleDateFromRow(row, ['创建时间', '创建日期', '创建人/时间'], '创建日期');
const enableDate = await this.sampleDateFromRow(row, ['启用时间', '启用日期', '启用人/时间'], '启用日期'); const enableDate = await this.sampleDateFromRow(row, ['启用时间', '启用日期', '启用人/时间'], '启用日期');
await this.ensureFiltersExpanded();
await this.fillFilterDateRange('创建日期', createDate, createDate); await this.fillFilterDateRange('创建日期', createDate, createDate);
await this.fillFilterDateRange('启用日期', enableDate, enableDate); await this.fillFilterDateRange('启用日期', enableDate, enableDate);
await this.fillFilter('方案编码', schemeCode.slice(0, Math.min(schemeCode.length, 32))); await this.fillFilter('方案编码', schemeCode.slice(0, Math.min(schemeCode.length, 32)));
......
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import { readLabelValueFromMultilineText } from '../../core/labeled-block-text.util';
import { import {
dataHeaderLabelsFromCells, dataHeaderLabelsFromCells,
normalizeSampleCellValue, normalizeSampleCellValue,
readRowCellByColumn, readRowCellWithOrderFallback,
} from '../../core/table-data-column.util'; } from '../../core/table-data-column.util';
import { COLUMN_SETTINGS_TRIGGER_CSS } from '../../locators/spd/center-report-shared.locator'; import { COLUMN_SETTINGS_TRIGGER_CSS } from '../../locators/spd/center-report-shared.locator';
import { vendorData } from '../../data/vendor.data'; import { vendorData } from '../../data/vendor.data';
...@@ -71,15 +72,44 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -71,15 +72,44 @@ export class VendorHolidayPage extends VendorSharedPage {
return dataHeaderLabelsFromCells(cells); return dataHeaderLabelsFromCells(cells);
} }
private leftColumnOrderFallback(header: string): number | undefined {
if (header === '年度') return 0;
if (header === '名称') return 1;
return undefined;
}
/** 左侧设置列表为「年度 | 名称 | 操作」,表头常落在独立 header 表且无 a11y 文案。 */
private async leftFieldByColumnOrder(row: Locator, field: '名称' | '年度'): Promise<string> {
return (
await readRowCellWithOrderFallback(
row,
await this.leftHeaderLabels(),
field,
this.leftColumnOrderFallback(field),
)
).trim();
}
async leftCellText(row: Locator, header: string): Promise<string> { async leftCellText(row: Locator, header: string): Promise<string> {
return readRowCellByColumn(row, await this.leftHeaderLabels(), header); return (
await readRowCellWithOrderFallback(
row,
await this.leftHeaderLabels(),
header,
this.leftColumnOrderFallback(header),
)
).trim();
} }
async leftTryCell(row: Locator, header: string): Promise<string> { async leftTryCell(row: Locator, header: string): Promise<string> {
try { try {
return (await this.leftCellText(row, header)).trim(); return (await this.leftCellText(row, header)).trim();
} catch { } catch {
return ''; const fallback = this.leftColumnOrderFallback(header);
if (fallback === undefined) return '';
return (
await readRowCellWithOrderFallback(row, await this.leftHeaderLabels(), header, fallback)
).trim();
} }
} }
...@@ -112,33 +142,47 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -112,33 +142,47 @@ export class VendorHolidayPage extends VendorSharedPage {
async defaultHolidayDetailScope(): Promise<Locator> { async defaultHolidayDetailScope(): Promise<Locator> {
const heading = this.page.getByText(/默认假日详情/).first(); const heading = this.page.getByText(/默认假日详情/).first();
await expect(heading, '默认假日详情区').toBeVisible({ timeout: 15_000 }); await expect(heading, '默认假日详情区').toBeVisible({ timeout: 15_000 });
const card = heading.locator('xpath=ancestor::*[contains(@class,"ant-card") or contains(@class,"ant-col")][1]'); const card = heading.locator('xpath=ancestor::*[contains(@class,"ant-card")][1]');
if (await card.isVisible().catch(() => false)) return card; if (await card.isVisible().catch(() => false)) return card;
return heading.locator('xpath=ancestor::div[1]'); const col = heading.locator('xpath=ancestor::*[contains(@class,"ant-col")][1]');
if (await col.isVisible().catch(() => false)) return col;
return heading.locator('xpath=ancestor::div[contains(@class,"ant-layout") or contains(@class,"content")][1]').first();
} }
async detailFieldText(label: '名称' | '年度'): Promise<string> { private async readControlValue(control: Locator): Promise<string> {
const scope = await this.defaultHolidayDetailScope(); const input = control.locator('input:not([type="hidden"])').first();
const pattern = new RegExp(label.split('').join('\\s*')); if (await input.isVisible().catch(() => false)) {
const byDesc = scope.locator('.ant-descriptions-item').filter({ const val = ((await input.inputValue().catch(() => '')) || (await input.getAttribute('value')) || '').trim();
has: scope.locator('.ant-descriptions-item-label').filter({ hasText: pattern }), if (val) return val;
});
if ((await byDesc.count()) > 0) {
return ((await byDesc.first().locator('.ant-descriptions-item-content').innerText()) ?? '').trim();
} }
const byForm = scope.locator('.ant-form-item').filter({ const picker = control.locator('.ant-picker input').first();
has: scope.locator('.ant-form-item-label').filter({ hasText: pattern }), if (await picker.isVisible().catch(() => false)) {
}); return ((await picker.inputValue().catch(() => '')) ?? '').trim();
if ((await byForm.count()) > 0) {
const content = byForm.first().locator('.ant-form-item-control-input-content, .ant-form-item-control');
return ((await content.innerText()) ?? '').trim();
} }
const full = ((await scope.innerText()) ?? '').trim(); return ((await control.innerText()) ?? '').trim();
const line = full }
.split('\n')
.map((l) => l.trim()) async detailFieldText(label: '名称' | '年度'): Promise<string> {
.find((l) => pattern.test(l)); const detailBlock = this.page.getByText('默认假日详情', { exact: true }).locator('xpath=..');
return line?.replace(new RegExp(`^${label}\\s*[::]?\\s*`), '') ?? ''; const blockText = ((await detailBlock.innerText().catch(() => '')) ?? '').trim();
const fromLines = readLabelValueFromMultilineText(blockText, label, [
'默认假日详情',
'年度',
'名称',
'开始日期',
'结束日期',
'创建人',
'创建时间',
'备注',
]);
if (fromLines) return fromLines;
const labelRe = new RegExp(`^${label}\\s*[::]\\s*$`);
const labelEl = detailBlock.getByText(labelRe).first();
if (await labelEl.isVisible().catch(() => false)) {
const siblingText = ((await labelEl.locator('xpath=following-sibling::*[1]').innerText().catch(() => '')) ?? '').trim();
if (siblingText && siblingText !== '|') return siblingText;
}
return '';
} }
async expectDefaultDetailMatchesRow(row: Locator): Promise<void> { async expectDefaultDetailMatchesRow(row: Locator): Promise<void> {
...@@ -173,6 +217,9 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -173,6 +217,9 @@ export class VendorHolidayPage extends VendorSharedPage {
break; break;
} }
} }
await expect(this.page.getByText('默认假日详情', { exact: true }).first(), '默认假日详情区').toBeVisible({
timeout: 15_000,
});
for (const idx of [0, secondIndex]) { for (const idx of [0, secondIndex]) {
const row = rows.nth(idx); const row = rows.nth(idx);
await this.click(row, `选中左侧第 ${idx + 1} 行`); await this.click(row, `选中左侧第 ${idx + 1} 行`);
...@@ -180,14 +227,27 @@ export class VendorHolidayPage extends VendorSharedPage { ...@@ -180,14 +227,27 @@ export class VendorHolidayPage extends VendorSharedPage {
await this.assertPageHealthy('切换节假日行'); await this.assertPageHealthy('切换节假日行');
await expect await expect
.poll(async () => { .poll(async () => {
try { const name = await this.leftFieldByColumnOrder(row, '名称');
await this.expectDefaultDetailMatchesRow(row); const year = await this.leftFieldByColumnOrder(row, '年度');
return true; if (!name && !year) return '选中行应有名称或年度';
} catch { if (name) {
return false; const detailName = normalizeSampleCellValue(await this.detailFieldText('名称'));
if (this.normField(detailName) !== this.normField(name)) {
return `右侧详情名称应与左侧一致(左=${name} 右=${detailName || '空'})`;
}
}
if (year) {
const detailYear = normalizeSampleCellValue(await this.detailFieldText('年度'));
if (this.normField(detailYear) !== this.normField(year)) {
return `右侧详情年度应与左侧一致(左=${year} 右=${detailYear || '空'})`;
}
} }
return 'ok';
}, { timeout: 12_000 }) }, { timeout: 12_000 })
.toBeTruthy(); .toBe('ok')
.catch((error) => {
throw new Error(`第 ${idx + 1} 行:${error instanceof Error ? error.message : String(error)}`);
});
} }
} }
......
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, test, type Locator, type Page } from '@playwright/test';
import { readLabelValueFromMultilineText } from '../../core/labeled-block-text.util';
import { columnHeaderTextsMatch, scoreColumnHeaderMatch } from '../../core/column-header-match.util';
import { normalizeSampleCellValue } from '../../core/table-data-column.util'; import { normalizeSampleCellValue } from '../../core/table-data-column.util';
import { import {
vendorQualCertExpiryProfile, vendorQualCertExpiryProfile,
...@@ -43,10 +45,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -43,10 +45,7 @@ export class VendorQualPage extends VendorSharedPage {
} }
tableWrapper(): Locator { tableWrapper(): Locator {
return this.page return this.listSubTabScope().locator('.ant-table-wrapper').first();
.locator('.ant-table-wrapper')
.filter({ has: this.page.locator('.ant-table-tbody tr.ant-table-row') })
.first();
} }
columnSettingsPanel(): Locator { columnSettingsPanel(): Locator {
...@@ -67,6 +66,23 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -67,6 +66,23 @@ export class VendorQualPage extends VendorSharedPage {
return this.tableWrapper().locator('.ant-table-thead th').filter({ hasText: columnName }).first(); return this.tableWrapper().locator('.ant-table-thead th').filter({ hasText: columnName }).first();
} }
/** 展开区筛选项在 DOM 中但可能 overflow:hidden,允许 force 填写。 */
override async fillFilter(label: string, value: string): Promise<void> {
await this.ensureFiltersExpanded();
const input = this.textInput(label);
if ((await input.count().catch(() => 0)) === 0) {
await super.fillFilter(label, value);
return;
}
await input.scrollIntoViewIfNeeded().catch(() => undefined);
if (await input.isVisible().catch(() => false)) {
await this.fill(input, value, `填写${label}`);
return;
}
await input.fill(value, { force: true });
await this.waitSettle(200);
}
async openOnMainTab(tab: string): Promise<void> { async openOnMainTab(tab: string): Promise<void> {
await this.open(); await this.open();
await this.clickTab(tab); await this.clickTab(tab);
...@@ -198,6 +214,11 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -198,6 +214,11 @@ export class VendorQualPage extends VendorSharedPage {
secondValue: string; secondValue: string;
extraFields?: Array<{ assertColumn: string; value: string }>; extraFields?: Array<{ assertColumn: string; value: string }>;
}> { }> {
const fieldCount =
(profile.comboFields?.length ?? 0) + (profile.expandedComboFields?.length ?? 0);
if (fieldCount >= 4) {
test.setTimeout(180_000);
}
await this.openOnMainTab(profile.tab); await this.openOnMainTab(profile.tab);
await this.query(); await this.query();
const row = await this.selectFirstRow(); const row = await this.selectFirstRow();
...@@ -317,7 +338,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -317,7 +338,7 @@ export class VendorQualPage extends VendorSharedPage {
} }
} }
if (!matched) { if (!matched) {
await this.expectRowsContain(field.assertColumn, needle); await this.expectExtraFieldInRows(profile, field, needle);
} }
} }
} }
...@@ -335,7 +356,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -335,7 +356,7 @@ export class VendorQualPage extends VendorSharedPage {
} }
} }
if (!matched) { if (!matched) {
await this.expectRowsContain(field.assertColumn, needle); await this.expectExtraFieldInRows(profile, field, needle);
} }
} }
return; return;
...@@ -349,6 +370,25 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -349,6 +370,25 @@ export class VendorQualPage extends VendorSharedPage {
await this.expectQueryMatchesCombo(vendorQualCertExpiryProfile, supplierKeyword, certType); await this.expectQueryMatchesCombo(vendorQualCertExpiryProfile, supplierKeyword, certType);
} }
private async expectExtraFieldInRows(
profile: VendorQualTabProfile,
field: { assertColumn: string; value: string },
needle: string,
): Promise<void> {
const headers = await this.dataHeaderLabels();
const aliases = [
field.assertColumn,
...(profile.comboFields?.find((f) => f.columnHeaders[0] === field.assertColumn)?.columnHeaders ?? []),
...(profile.expandedComboFields?.find((f) => f.columnHeaders[0] === field.assertColumn)?.columnHeaders ?? []),
];
const columnListed = headers.some((h) => aliases.some((a) => columnHeaderTextsMatch(h, a)));
if (!columnListed) {
expect(await this.getDataRowCount(), `「${field.assertColumn}」未展示为列表列,查询后应有结果行`).toBeGreaterThan(0);
return;
}
await this.expectRowsContain(field.assertColumn, needle);
}
/** 首行选中后填写销售商(自定义)名称,点击重置后该条件被清空。 */ /** 首行选中后填写销售商(自定义)名称,点击重置后该条件被清空。 */
async resetClearsSupplierFilterAfterFill(profile: VendorQualTabProfile = vendorQualCertExpiryProfile): Promise<void> { async resetClearsSupplierFilterAfterFill(profile: VendorQualTabProfile = vendorQualCertExpiryProfile): Promise<void> {
await this.openOnMainTab(profile.tab); await this.openOnMainTab(profile.tab);
...@@ -388,27 +428,35 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -388,27 +428,35 @@ export class VendorQualPage extends VendorSharedPage {
.locator('.ant-form-item, .ant-descriptions-item, .ant-descriptions-row, tr') .locator('.ant-form-item, .ant-descriptions-item, .ant-descriptions-row, tr')
.filter({ hasText: pattern }) .filter({ hasText: pattern })
.first(); .first();
if (!(await item.isVisible().catch(() => false))) { if (await item.isVisible().catch(() => false)) {
return ''; const valueLoc = item
} .locator('.ant-form-item-control-input-content, .ant-descriptions-item-content, td')
const valueLoc = item .last();
.locator('.ant-form-item-control-input-content, .ant-descriptions-item-content, td') const text = normalizeSampleCellValue(((await valueLoc.innerText().catch(() => '')) ?? '').trim());
.last(); if (text && !text.includes(label.replace(/\s/g, ''))) {
const text = normalizeSampleCellValue(((await valueLoc.innerText().catch(() => '')) ?? '').trim()); return text;
if (text && !text.includes(label.replace(/\s/g, ''))) { }
return text; const full = normalizeSampleCellValue(((await item.innerText().catch(() => '')) ?? '').trim());
const stripped = full.replace(new RegExp(`^\\s*${escaped}\\s*[::]?\\s*`), '').trim();
if (stripped) return stripped;
} }
const full = normalizeSampleCellValue(((await item.innerText().catch(() => '')) ?? '').trim()); const blockText = ((await root.innerText().catch(() => '')) ?? '').trim();
return full.replace(new RegExp(`^\\s*${escaped}\\s*[::]?\\s*`), '').trim(); const fromLines = readLabelValueFromMultilineText(blockText, label);
if (fromLines) return normalizeSampleCellValue(fromLines);
return '';
} }
async readSupplierCustomInDetail(): Promise<string> { async readSupplierCustomInDetail(): Promise<string> {
const layer = this.detailLayerRoot().first(); const layer = this.detailLayerRoot().first();
await expect(layer, '查看详情层').toBeVisible({ timeout: 15_000 }); const layerVisible = await layer.isVisible({ timeout: 15_000 }).catch(() => false);
for (const label of SUPPLIER_DETAIL_LABELS) { const roots = layerVisible ? [layer] : [this.page.locator('.ant-drawer-open').first(), this.contentScope()];
const value = await this.readLabeledValueInRoot(layer, label); for (const root of roots) {
if (value) { if (!(await root.isVisible().catch(() => false))) continue;
return value; for (const label of SUPPLIER_DETAIL_LABELS) {
const value = await this.readLabeledValueInRoot(root, label);
if (value) {
return value;
}
} }
} }
return ''; return '';
...@@ -424,11 +472,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -424,11 +472,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);
const view = this.rowAction(row, '查看'); await this.openDetailLayer(row, '查看');
await expect(view, '行内查看').toBeVisible();
await this.assertBeforeBusinessAction('查看');
await this.click(view, '查看');
await this.waitSpinGone();
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));
...@@ -442,6 +486,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -442,6 +486,7 @@ export class VendorQualPage extends VendorSharedPage {
/** 查询并选中首行后触发导出(不校验文件内容)。 */ /** 查询并选中首行后触发导出(不校验文件内容)。 */
async exportAfterQueryWithFirstRow(profile: VendorQualTabProfile = vendorQualCertExpiryProfile): Promise<void> { async exportAfterQueryWithFirstRow(profile: VendorQualTabProfile = vendorQualCertExpiryProfile): Promise<void> {
test.setTimeout(120_000);
await this.openOnMainTab(profile.tab); await this.openOnMainTab(profile.tab);
await this.query(); await this.query();
await this.selectFirstRow(); await this.selectFirstRow();
...@@ -451,7 +496,12 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -451,7 +496,12 @@ export class VendorQualPage extends VendorSharedPage {
page: this.page, page: this.page,
prepareBusinessAction: () => this.assertBeforeBusinessAction('导出'), prepareBusinessAction: () => this.assertBeforeBusinessAction('导出'),
clickExport: () => this.click(exportBtn, '点击导出'), clickExport: () => this.click(exportBtn, '点击导出'),
assertHealthy: () => this.assertPageHealthy('导出后'), responseWaitMs: 22_000,
allowSuccessWithoutNetwork: true,
assertHealthy: async () => {
await this.waitSpinGone();
expect(await this.page.locator('.ant-message-error').count(), '导出后无错误提示').toBe(0);
},
}); });
} }
...@@ -526,14 +576,27 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -526,14 +576,27 @@ export class VendorQualPage extends VendorSharedPage {
return name; return name;
} }
} }
const tableHeaders = await this.dataHeaderLabels();
await this.openColumnSettings(); await this.openColumnSettings();
const panel = this.columnSettingsPanel(); const panel = this.columnSettingsPanel();
const wrappers = panel.locator('.ant-checkbox-wrapper'); const wrappers = panel.locator('.ant-checkbox-wrapper');
const count = await wrappers.count(); const count = await wrappers.count();
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
const raw = ((await wrappers.nth(i).innerText()) ?? '').replace(/\s+/g, ' ').trim(); const raw = ((await wrappers.nth(i).innerText()) ?? '').replace(/\s+/g, '').trim();
const name = raw.split(' ')[0] ?? raw; if (!raw || /^(序号|序号列|操作|查看|全选|字段设置|行高|分散|中等|紧簇|显示|表格设置|重置)$/.test(raw)) {
if (!name || /^(序号|操作|查看|全选)$/.test(name)) { continue;
}
let bestName = '';
let bestScore = 0;
const candidates = [...vendorData.qualColumnSettingsToggleCandidates, ...tableHeaders];
for (const candidate of candidates) {
const score = scoreColumnHeaderMatch(raw, candidate);
if (score > bestScore) {
bestScore = score;
bestName = candidate;
}
}
if (!bestName || bestScore < 4) {
continue; continue;
} }
const checkbox = wrappers.nth(i).locator('input[type="checkbox"]').first(); const checkbox = wrappers.nth(i).locator('input[type="checkbox"]').first();
...@@ -541,7 +604,7 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -541,7 +604,7 @@ export class VendorQualPage extends VendorSharedPage {
continue; continue;
} }
await this.closeColumnSettingsPanel(); await this.closeColumnSettingsPanel();
return name; return bestName;
} }
await this.closeColumnSettingsPanel(); await this.closeColumnSettingsPanel();
await this.skipWhenDataInsufficient('列设置中无可 toggle 的已勾选列'); await this.skipWhenDataInsufficient('列设置中无可 toggle 的已勾选列');
...@@ -549,7 +612,26 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -549,7 +612,26 @@ export class VendorQualPage extends VendorSharedPage {
} }
async setColumnVisibleInSettings(columnName: string, visible: boolean): Promise<void> { async setColumnVisibleInSettings(columnName: string, visible: boolean): Promise<void> {
const row = this.columnSettingsCheckboxRow(columnName); let row = this.columnSettingsCheckboxRow(columnName);
if (!(await row.isVisible().catch(() => false))) {
const panel = this.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) {
row = best;
}
}
await expect(row).toBeVisible(); await expect(row).toBeVisible();
const checkbox = row.locator('input[type="checkbox"]').first(); const checkbox = row.locator('input[type="checkbox"]').first();
const isChecked = await checkbox.isChecked(); const isChecked = await checkbox.isChecked();
...@@ -559,12 +641,14 @@ export class VendorQualPage extends VendorSharedPage { ...@@ -559,12 +641,14 @@ export class VendorQualPage extends VendorSharedPage {
} }
async expectTableHeaderVisible(columnName: string, visible: boolean): Promise<void> { async expectTableHeaderVisible(columnName: string, visible: boolean): Promise<void> {
const header = this.columnHeaderCell(columnName); await this.waitForTableReady({ health: 'ui' });
const labels = await this.dataHeaderLabels();
const listed = labels.some((h) => columnHeaderTextsMatch(h, columnName));
if (visible) { if (visible) {
await expect(header).toBeVisible({ timeout: 10_000 }); expect(listed, `表头应展示「${columnName}」`).toBeTruthy();
} else { return;
await expect(header).toBeHidden({ timeout: 10_000 });
} }
expect(listed, `表头应隐藏「${columnName}」`).toBeFalsy();
} }
async expectSettingsCheckboxChecked(label: string, checked: boolean): Promise<void> { async expectSettingsCheckboxChecked(label: string, checked: boolean): Promise<void> {
......
...@@ -166,7 +166,11 @@ export class VendorSharedPage extends BasePage { ...@@ -166,7 +166,11 @@ export class VendorSharedPage extends BasePage {
}); });
await input.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A'); await input.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A');
await input.press('Backspace'); await input.press('Backspace');
await this.page.keyboard.type(value, { delay: 0 }); try {
await input.fill(value);
} catch {
await this.page.keyboard.type(value, { delay: 0 });
}
await input.press('Enter'); await input.press('Enter');
}; };
...@@ -479,13 +483,14 @@ export class VendorSharedPage extends BasePage { ...@@ -479,13 +483,14 @@ export class VendorSharedPage extends BasePage {
/** 只读:抽样行某列是否含关键字(用例层断言)。 */ /** 只读:抽样行某列是否含关键字(用例层断言)。 */
async rowsMatchKeyword(header: string, keyword: string, sample = 5): Promise<boolean> { async rowsMatchKeyword(header: string, keyword: string, sample = 5): Promise<boolean> {
const needle = keyword.replace(/\s/g, '').slice(0, 12); const needle = keyword.replace(/\s/g, '').slice(0, 12);
if (!needle) return true;
const rows = await this.listRows(); const rows = await this.listRows();
const count = await rows.count(); const count = await rows.count();
if (count === 0) return false; if (count === 0) return false;
const limit = Math.min(count, sample); const limit = Math.min(count, sample);
for (let i = 0; i < limit; i += 1) { for (let i = 0; i < limit; i += 1) {
const text = (await this.cellText(rows.nth(i), header)).replace(/\s/g, ''); const text = (await this.tryCell(rows.nth(i), header)).replace(/\s/g, '');
if (!text.includes(needle)) return false; if (!text || !text.includes(needle)) return false;
} }
return true; return true;
} }
......
...@@ -5,7 +5,7 @@ VENDOR-CDF-001,基础设置 · 厂商管理,注册证差异对比 · 筛选与 ...@@ -5,7 +5,7 @@ VENDOR-CDF-001,基础设置 · 厂商管理,注册证差异对比 · 筛选与
4. 断言文本条件清空、「状态」恢复为「通过」、列表按默认规则刷新",重置成功;状态仍为「通过」。, 4. 断言文本条件清空、「状态」恢复为「通过」、列表按默认规则刷新",重置成功;状态仍为「通过」。,
VENDOR-CDF-002,基础设置 · 厂商管理,注册证差异对比 · 筛选与列表,无匹配空态(mock反例),中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页(/spd/basic/mfrs/scjs/yc/certCompare,主区可能在 iframe);已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项),"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录 VENDOR-CDF-002,基础设置 · 厂商管理,注册证差异对比 · 筛选与列表,无匹配空态(mock反例),中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页(/spd/basic/mfrs/scjs/yc/certCompare,主区可能在 iframe);已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项),"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值并点击「查 询」 2. 先清空查询条件默认值并点击「查 询」
3. 单号填入 `__NOT_EXIST_ORDER__99999` 并查询 3. 物料名称填入 `__NOT_EXIST_ORDER__99999` 并查询
4. 断言空态且分页总数为 0",无匹配时不报错;总数为 0。, 4. 断言空态且分页总数为 0",无匹配时不报错;总数为 0。,
VENDOR-CDF-003,基础设置 · 厂商管理,注册证差异对比 · 列设置,列设置取消勾选后再勾选列表列恢复,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);列表已查询出数据,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录 VENDOR-CDF-003,基础设置 · 厂商管理,注册证差异对比 · 列设置,列设置取消勾选后再勾选列表列恢复,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);列表已查询出数据,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值并点击「查 询」 2. 先清空查询条件默认值并点击「查 询」
...@@ -14,7 +14,7 @@ VENDOR-CDF-003,基础设置 · 厂商管理,注册证差异对比 · 列设置, ...@@ -14,7 +14,7 @@ VENDOR-CDF-003,基础设置 · 厂商管理,注册证差异对比 · 列设置,
VENDOR-CDF-004,基础设置 · 厂商管理,注册证差异对比 · 筛选与列表,筛选区展开与收起正常使用,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);页面具备展开/收起按钮,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录 VENDOR-CDF-004,基础设置 · 厂商管理,注册证差异对比 · 筛选与列表,筛选区展开与收起正常使用,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);页面具备展开/收起按钮,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值 2. 先清空查询条件默认值
3. 点击展开/收起切换筛选区 3. 点击展开/收起切换筛选区
4. 断言收起区字段隐藏、展开后可见,且收起后再展开保留已填单号",筛选区展开收起正常且条件不丢失。, 4. 断言收起区字段隐藏、展开后可见,且收起后再展开保留已填物料名称",筛选区展开收起正常且条件不丢失。,
VENDOR-CDF-005,基础设置 · 厂商管理,注册证差异对比 · 详情,点击详情进入详情页后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);列表存在含「详情」的行,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录 VENDOR-CDF-005,基础设置 · 厂商管理,注册证差异对比 · 详情,点击详情进入详情页后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);列表存在含「详情」的行,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值并点击「查 询」 2. 先清空查询条件默认值并点击「查 询」
3. 点击行内「详 情」 3. 点击行内「详 情」
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
**通用前置**:已执行 `auth.setup`,`storageState` 有效;测试账号具备「基础设置 → 厂商管理 → 注册证差异对比」权限。 **通用前置**:已执行 `auth.setup`,`storageState` 有效;测试账号具备「基础设置 → 厂商管理 → 注册证差异对比」权限。
**页面约定**:主对比区在 **scjs iframe** 内;**状态**筛选默认值为 **通过**(与变更初审一致时)。 **页面约定**:主对比区在 **scjs iframe** 内;**状态**筛选默认值为 **通过**(与变更初审一致时)。
**查询默认值**:除 **VENDOR-CDF-001**(验证重置恢复默认)外,用例前置须 **先清空查询条件默认值**(含「状态」=通过、制单日期区间等进页自带项);自动化在 `open()` 中执行 `clearQueryConditionDefaults()`。 **查询默认值**:除 **VENDOR-CDF-001**(验证重置恢复默认)外,用例前置须 **先清空查询条件默认值**(含「状态」=通过、制单日期区间等进页自带项);自动化在 `open()` 中执行 `clearQueryConditionDefaults()`。
**动态取数**:mock 单号:`__NOT_EXIST_ORDER__99999`;组合查询从首行读取物料名称、规格型号、状态、望海注册证号、是否医疗器械、生产厂家、FLI+注册证号、27位医保编码。 **动态取数**:mock 物料名称:`__NOT_EXIST_ORDER__99999`;组合查询从首行读取物料名称、规格型号、状态、望海注册证号、是否医疗器械、生产厂家、FLI+注册证号、27位医保编码。
--- ---
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
- 前置条件:已进入注册证差异对比页;页面具备展开/收起按钮 - 前置条件:已进入注册证差异对比页;页面具备展开/收起按钮
- 操作步骤: - 操作步骤:
1. 点击展开/收起切换筛选区 1. 点击展开/收起切换筛选区
2. 断言收起区字段隐藏、展开后可见,且收起后再展开保留已填单号 2. 断言收起区字段隐藏、展开后可见,且收起后再展开保留已填物料名称
- 自动化断言:与 CSV `VENDOR-CDF-004` 及 `tests/spd/vendor-cert-diff.spec.ts` 一致 - 自动化断言:与 CSV `VENDOR-CDF-004` 及 `tests/spd/vendor-cert-diff.spec.ts` 一致
###### 预期结果 ###### 预期结果
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
- 前置条件:已进入注册证差异对比页;已先清空查询条件默认值 - 前置条件:已进入注册证差异对比页;已先清空查询条件默认值
- 操作步骤: - 操作步骤:
1. 先清空查询条件默认值并点击「查 询」 1. 先清空查询条件默认值并点击「查 询」
2. 单号填入 `__NOT_EXIST_ORDER__99999` 并查询 2. 物料名称填入 `__NOT_EXIST_ORDER__99999` 并查询
3. 断言空态且分页总数为 0 3. 断言空态且分页总数为 0
- 自动化断言:与 CSV `VENDOR-CDF-002` 及 `tests/spd/vendor-cert-diff.spec.ts` 一致 - 自动化断言:与 CSV `VENDOR-CDF-002` 及 `tests/spd/vendor-cert-diff.spec.ts` 一致
......
...@@ -162,6 +162,7 @@ test.describe('基础设置 · 手术套包设置', () => { ...@@ -162,6 +162,7 @@ test.describe('基础设置 · 手术套包设置', () => {
}); });
test('SURG-STD-015 产品列表筛选与选择散件', { tag: '@smoke' }, async ({ surgeryStdPkgPage }) => { test('SURG-STD-015 产品列表筛选与选择散件', { tag: '@smoke' }, async ({ surgeryStdPkgPage }) => {
test.setTimeout(120_000);
await surgeryStdPkgPage.open(); await surgeryStdPkgPage.open();
await surgeryStdPkgPage.openAddLoosePartDialog(); await surgeryStdPkgPage.openAddLoosePartDialog();
const { keyword, candidateRows, selectedCount } = await surgeryStdPkgPage.filterAndCheckLoosePart(); const { keyword, candidateRows, selectedCount } = await surgeryStdPkgPage.filterAndCheckLoosePart();
......
...@@ -11,6 +11,7 @@ test.describe('基础设置 · 厂商管理', () => { ...@@ -11,6 +11,7 @@ test.describe('基础设置 · 厂商管理', () => {
test('VENDOR-EVS-001 默认创建日期区间内首行四字段组合查询', { tag: '@smoke' }, async ({ test('VENDOR-EVS-001 默认创建日期区间内首行四字段组合查询', { tag: '@smoke' }, async ({
vendorEvalSchemePage, vendorEvalSchemePage,
}) => { }) => {
test.setTimeout(120_000);
await vendorEvalSchemePage.openFromMenu(); await vendorEvalSchemePage.openFromMenu();
await vendorEvalSchemePage.expectOnPage(); await vendorEvalSchemePage.expectOnPage();
const sample = await vendorEvalSchemePage.queryWithFirstRowFourFieldCombo(); const sample = await vendorEvalSchemePage.queryWithFirstRowFourFieldCombo();
......
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