Commit 6c31a476 authored by liguangyu06's avatar liguangyu06
Browse files

同步基础管理与统计报表用例文档,并更新 Playwright 脚本与 PO。

对齐 CSV/MD/测试点与现网列名及组合查询;补充厂商资质子 Tab 与基础模块复跑脚本。
parent aa8987ce
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import { env } from '../../config/env.config'; import { env } from '../../config/env.config';
import { vendorData } from '../../data/vendor.data';
import { BasePage } from '../../core/base.page'; import { BasePage } from '../../core/base.page';
import { columnHeaderTextsMatch } from '../../core/column-header-match.util'; import { columnHeaderTextsMatch } from '../../core/column-header-match.util';
import { import {
...@@ -11,6 +12,7 @@ import { ...@@ -11,6 +12,7 @@ import {
} from '../../core/table-data-column.util'; } from '../../core/table-data-column.util';
import { VendorSharedLocators } from '../../locators/spd/vendor-shared.locator'; import { VendorSharedLocators } from '../../locators/spd/vendor-shared.locator';
import { skipIfInsufficientForPage2 } from './helpers/pagination-page2.helper'; import { skipIfInsufficientForPage2 } from './helpers/pagination-page2.helper';
import { skipIfInsufficientForPageSizeChange } from './helpers/pagination-pagesize.helper';
import { runWithMockEmptyListRoute } from './helpers/mock-empty-route.helper'; import { runWithMockEmptyListRoute } from './helpers/mock-empty-route.helper';
export type VendorMeta = { export type VendorMeta = {
...@@ -143,6 +145,50 @@ export class VendorSharedPage extends BasePage { ...@@ -143,6 +145,50 @@ export class VendorSharedPage extends BasePage {
await this.fill(input, value, `填写${label}`); await this.fill(input, value, `填写${label}`);
} }
/** 搜索条 ant-picker-range:键盘录入起止日期(YYYY-MM-DD)。 */
async fillFilterDateRange(label: string, start: string, end: string): Promise<void> {
await this.ensureFiltersExpanded();
const scope = await this.queryScope();
const item = await this.resolveFilterItem(scope, label);
expect(item, `筛选「${label}」日期区间`).toBeTruthy();
const picker = item!.locator('.ant-picker').first();
await expect(picker, `「${label}」日期控件`).toBeVisible();
const inputs = picker.locator('input');
const count = await inputs.count();
expect(count, `「${label}」应有日期输入`).toBeGreaterThanOrEqual(1);
const typeInto = async (input: Locator, value: string) => {
await input.click({ force: true });
await input.evaluate((el) => {
const node = el as HTMLInputElement;
node.removeAttribute('readonly');
node.readOnly = false;
});
await input.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A');
await input.press('Backspace');
await this.page.keyboard.type(value, { delay: 0 });
await input.press('Enter');
};
if (count >= 2) {
await typeInto(inputs.nth(0), start);
await typeInto(inputs.nth(1), end);
} else {
await typeInto(inputs.first(), `${start} ~ ${end}`);
}
await this.page.keyboard.press('Escape').catch(() => undefined);
await this.waitSettle(300);
}
async selectFirstRow(): Promise<Locator> {
const row = await this.firstRow();
await this.assertBeforeBusinessAction('选择首行');
await this.click(row, '选择首行');
await this.waitSettle(300);
await expect(row, '首行应处于选中态').toHaveClass(/basic-selected-row|ant-table-row-selected/);
return row;
}
/** 列表列「销售商」与筛选项「进院供应商名称」等别名。 */ /** 列表列「销售商」与筛选项「进院供应商名称」等别名。 */
async fillSupplierFilter(value: string): Promise<void> { async fillSupplierFilter(value: string): Promise<void> {
await this.ensureFiltersExpanded(); await this.ensureFiltersExpanded();
...@@ -162,14 +208,33 @@ export class VendorSharedPage extends BasePage { ...@@ -162,14 +208,33 @@ export class VendorSharedPage extends BasePage {
await runWithMockEmptyListRoute(this.page, mockNeedle, fillAndQuery); await runWithMockEmptyListRoute(this.page, mockNeedle, fillAndQuery);
} }
/** 业务内容区(排除顶栏多页签「首页 / xxx」)。 */
protected contentScope(): Locator {
return this.page.locator('.ant-layout-content').last();
}
tab(name: string): Locator { tab(name: string): Locator {
const pattern = new RegExp(name); const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return this.page const pattern = new RegExp(escaped);
.locator('.ant-tabs-tab, [role="tab"], .ant-radio-button-wrapper, .ant-segmented-item') const exact = new RegExp(`^\\s*${escaped}\\s*$`);
const scope = this.contentScope();
const customNav = scope
.locator('div, span, a, button')
.filter({ hasText: exact })
.first();
return scope
.locator('.ant-tabs-tab:not(.ant-tabs-tab-with-remove) .ant-tabs-tab-btn, .ant-tabs-tab:not(.ant-tabs-tab-with-remove)')
.filter({ hasText: pattern }) .filter({ hasText: pattern })
.first() .first()
.or(this.page.getByRole('tab', { name: pattern }).first()) .or(
.or(this.page.locator('div,span,button').filter({ hasText: new RegExp(`^\\s*${name}\\s*$`) }).first()); scope
.locator('.ant-radio-group, .ant-segmented')
.locator('.ant-radio-button-wrapper, .ant-segmented-item')
.filter({ hasText: pattern })
.first(),
)
.or(scope.getByRole('tab', { name: pattern }).first())
.or(customNav);
} }
async readTabCount(name: string): Promise<number | null> { async readTabCount(name: string): Promise<number | null> {
...@@ -193,20 +258,107 @@ export class VendorSharedPage extends BasePage { ...@@ -193,20 +258,107 @@ export class VendorSharedPage extends BasePage {
expect(await this.getTotalCount(), `${name} Tab 计数应等于分页总数`).toBe(count); expect(await this.getTotalCount(), `${name} Tab 计数应等于分页总数`).toBe(count);
} }
/** 断言列表区 Tab/Radio 当前为选中态(进页默认 Tab 等)。 */
async expectTabActive(name: string): Promise<void> {
const tab = this.tab(name);
await expect(tab, `Tab「${name}」`).toBeVisible({ timeout: 15_000 });
await expect
.poll(
async () => {
return tab.evaluate((el) => {
let node: HTMLElement | null = el as HTMLElement;
for (let depth = 0; depth < 6 && node; depth += 1) {
const cls = node.className?.toString() ?? '';
if (/active|checked|selected|on/i.test(cls)) return true;
if (node.getAttribute('aria-selected') === 'true') return true;
const style = window.getComputedStyle(node);
const fw = Number.parseInt(style.fontWeight, 10);
if (Number.isFinite(fw) && fw >= 600) return true;
const border = parseFloat(style.borderBottomWidth || '0');
if (border >= 2) return true;
const color = style.color;
const parent = node.parentElement;
if (parent) {
const siblings = [...parent.children].filter(
(child) => (child.textContent ?? '').replace(/\s/g, '').length > 0,
);
if (siblings.length >= 2) {
const colors = siblings.map((s) => window.getComputedStyle(s).color);
const uniq = new Set(colors);
if (uniq.size > 1 && !colors.slice(1).every((c) => c === colors[0])) {
if (color !== colors[0]) return true;
}
}
}
node = node.parentElement;
}
return false;
});
},
{ timeout: 8_000 },
)
.toBeTruthy();
}
/** 资质等页会挂 width/height=0 的镜像表,只认可见主表。 */
protected async visibleTableWrapper(): Promise<Locator> {
const wrappers = this.page.locator('.ant-table-wrapper');
const count = await wrappers.count();
let best = this.page.locator('.ant-table-wrapper').first();
let bestScore = -1;
for (let i = 0; i < count; i += 1) {
const wrapper = wrappers.nth(i);
const usable = await wrapper
.evaluate((el) => {
if (el.closest('.put-page, .ant-modal-wrap, .ant-drawer-open')) return false;
const rect = el.getBoundingClientRect();
return rect.width > 8 && rect.height > 8;
})
.catch(() => false);
if (!usable) continue;
const score = await wrapper
.evaluate((el) => {
const rect = el.getBoundingClientRect();
return rect.width * rect.height;
})
.catch(() => 0);
if (score > bestScore) {
bestScore = score;
best = wrapper;
}
}
return best;
}
async headerCells(): Promise<Locator> { async headerCells(): Promise<Locator> {
await this.page const mainWrapper = await this.visibleTableWrapper();
await mainWrapper
.locator('.ant-table-thead th, .ant-table-header th, thead th') .locator('.ant-table-thead th, .ant-table-header th, thead th')
.first() .first()
.waitFor({ state: 'attached', timeout: 12_000 }) .waitFor({ state: 'attached', timeout: 12_000 })
.catch(() => undefined); .catch(() => undefined);
const groups = this.page.locator('.ant-table-thead, .ant-table-header thead, thead'); const groups = mainWrapper.locator('.ant-table-thead, .ant-table-header thead, thead');
const count = await groups.count(); const count = await groups.count();
let best = this.page.locator('.ant-table-thead th, .ant-table-header th, thead th'); let best = mainWrapper.locator('.ant-table-thead th, .ant-table-header th, thead th');
let bestLen = 0; let bestLen = 0;
let bestScore = -1;
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
const cells = groups.nth(i).locator('th'); const cells = groups.nth(i).locator('th');
const len = await cells.count(); const len = await cells.count();
if (len > bestLen) { let titled = 0;
for (let j = 0; j < Math.min(len, 20); j += 1) {
const th = cells.nth(j);
const titleEl = th.locator('.ant-table-column-title').first();
let raw = '';
if ((await titleEl.count()) > 0) {
raw = (await titleEl.innerText({ timeout: 800 }).catch(() => '')).trim();
}
if (!raw) raw = ((await th.innerText({ timeout: 800 }).catch(() => '')) || '').trim();
if (raw.replace(/\s/g, '')) titled += 1;
}
const score = titled * 100 + len;
if (score > bestScore) {
bestScore = score;
bestLen = len; bestLen = len;
best = cells; best = cells;
} }
...@@ -238,22 +390,8 @@ export class VendorSharedPage extends BasePage { ...@@ -238,22 +390,8 @@ export class VendorSharedPage extends BasePage {
} }
async listRows(): Promise<Locator> { async listRows(): Promise<Locator> {
const bodies = this.page.locator('.ant-table-tbody'); const wrapper = await this.visibleTableWrapper();
const count = await bodies.count(); return wrapper.locator('tbody.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)');
let best = this.page.locator('.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)');
let bestWidth = -1;
for (let i = 0; i < count; i += 1) {
const body = bodies.nth(i);
const insidePut = await body.evaluate((el) => Boolean(el.closest('.put-page,.ant-modal'))).catch(() => false);
if (insidePut) continue;
const row = body.locator('> tr.ant-table-row:not(.ant-table-measure-row)').first();
const width = await row.locator('td').count().catch(() => 0);
if (width > bestWidth) {
bestWidth = width;
best = body.locator('> tr.ant-table-row:not(.ant-table-measure-row)');
}
}
return best;
} }
async getDataRowCount(): Promise<number> { async getDataRowCount(): Promise<number> {
...@@ -286,7 +424,15 @@ export class VendorSharedPage extends BasePage { ...@@ -286,7 +424,15 @@ export class VendorSharedPage extends BasePage {
} }
async firstRow(): Promise<Locator> { async firstRow(): Promise<Locator> {
const row = (await this.listRows()).first(); const rows = await this.listRows();
const count = Math.min(await rows.count(), 40);
for (let i = 0; i < count; i += 1) {
const row = rows.nth(i);
if (await row.isVisible().catch(() => false)) {
return row;
}
}
const row = rows.first();
await expect(row, '列表至少应有 1 条数据').toBeVisible(); await expect(row, '列表至少应有 1 条数据').toBeVisible();
return row; return row;
} }
...@@ -377,8 +523,21 @@ export class VendorSharedPage extends BasePage { ...@@ -377,8 +523,21 @@ 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.assertBeforeBusinessAction(action);
const urlBefore = this.page.url();
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'));
const layerVisible = await layer
.first()
.isVisible({ timeout: 8_000 })
.catch(() => false);
if (!layerVisible) {
await this.page
.waitForURL((url) => url.href !== urlBefore, { timeout: 15_000 })
.catch(() => undefined);
await this.waitSpinGone();
await this.assertPageHealthy(action);
return;
}
await expect(layer.first(), `${action}应打开详情层`).toBeVisible({ timeout: 15_000 }); await expect(layer.first(), `${action}应打开详情层`).toBeVisible({ timeout: 15_000 });
await this.waitSpinGone(); await this.waitSpinGone();
await this.assertPageHealthy(action); await this.assertPageHealthy(action);
...@@ -407,6 +566,62 @@ export class VendorSharedPage extends BasePage { ...@@ -407,6 +566,62 @@ export class VendorSharedPage extends BasePage {
return null; return null;
} }
pageSizeChanger(): Locator {
return this.page.locator('.ant-pagination-options-size-changer').first();
}
async pageSizeChangerText(): Promise<string> {
return ((await this.pageSizeChanger().innerText().catch(() => '')) ?? '').replace(/\s+/g, ' ');
}
async changePageSizeTo(toLabel: string): Promise<void> {
const changer = this.pageSizeChanger();
if (!(await changer.isVisible().catch(() => false))) {
await this.skipWhenDataInsufficient('当前页面无每页条数切换');
}
const sizeNum = (toLabel.match(/\d+/) ?? ['10'])[0];
const current = await this.pageSizeChangerText();
if (current.includes(sizeNum) && current.includes('条')) {
return;
}
await this.assertBeforeBusinessAction('切换每页条数');
await this.click(changer, '打开每页条数');
const option = this.page
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option')
.filter({ hasText: new RegExp(`${sizeNum}\\s*条`) })
.first();
await this.click(option, `选择 ${toLabel}`);
await this.waitForTableReady();
const after = await this.pageSizeChangerText();
if (!after.includes(sizeNum)) {
throw new Error(`切换每页条数未生效:期望含 ${sizeNum},当前「${after || '(空)'}」`);
}
}
/**
* 切换为更小每页条数(厂商查询默认 50,目标见 vendorData.paginationPageSizeTo)。
* 返回切换前后可见行数与总条数(总条数应不变)。
*/
async switchToSmallerPageSize(): Promise<{ total: number; rowsBefore: number; rowsAfter: number; pageSizeLabel: string }> {
const pageSizeLabel = vendorData.paginationPageSizeTo;
const sizeNum = Number((pageSizeLabel.match(/\d+/) ?? ['20'])[0]);
await skipIfInsufficientForPageSizeChange({
ensureHealthyBeforeSkip: (reason) => this.ensureHealthyBeforeSkip(reason),
waitForTableReady: () => this.waitForTableReady(),
getDataRowCount: () => this.getDataRowCount(),
});
const total = await this.getTotalCount();
const rowsBefore = await this.getDataRowCount();
if (total > sizeNum && rowsBefore <= sizeNum) {
await this.skipWhenDataInsufficient(
`当前页可见行数已不大于 ${sizeNum},无法验证切换 ${pageSizeLabel} 后列表变化`,
);
}
await this.changePageSizeTo(pageSizeLabel);
const rowsAfter = await this.getDataRowCount();
return { total, rowsBefore, rowsAfter, pageSizeLabel };
}
async goToPage2(): Promise<void> { async goToPage2(): Promise<void> {
await skipIfInsufficientForPage2({ await skipIfInsufficientForPage2({
ensureHealthyBeforeSkip: (reason) => this.ensureHealthyBeforeSkip(reason), ensureHealthyBeforeSkip: (reason) => this.ensureHealthyBeforeSkip(reason),
......
...@@ -4,6 +4,7 @@ import { env } from '../../config/env.config'; ...@@ -4,6 +4,7 @@ import { env } from '../../config/env.config';
import { BasePage } from '../../core/base.page'; import { BasePage } from '../../core/base.page';
import { WhFlowSharedLocators } from '../../locators/spd/wh-flow-shared.locator'; import { WhFlowSharedLocators } from '../../locators/spd/wh-flow-shared.locator';
import { skipIfInsufficientForPage2 } from './helpers/pagination-page2.helper'; import { skipIfInsufficientForPage2 } from './helpers/pagination-page2.helper';
import { skipIfInsufficientForPageSizeChange } from './helpers/pagination-pagesize.helper';
export type WhFlowMeta = { export type WhFlowMeta = {
path: string; path: string;
...@@ -125,13 +126,28 @@ export class WhFlowSharedPage extends BasePage { ...@@ -125,13 +126,28 @@ export class WhFlowSharedPage extends BasePage {
const select = this.formSelect(label); const select = this.formSelect(label);
await expect(select, `筛选「${label}」`).toBeVisible(); await expect(select, `筛选「${label}」`).toBeVisible();
await this.click(select, `打开${label}`); await this.click(select, `打开${label}`);
const compact = optionText.replace(/\s/g, '').slice(0, 12); const dropdown = await this.activeSelectDropdown(this.page);
const escaped = compact.slice(0, 8).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const options = dropdown.locator('.ant-select-item-option:not(.ant-select-item-option-disabled)');
const option = this.page await expect
.locator('.ant-select-dropdown:visible .ant-select-item-option') .poll(async () => (await options.count()) > 0, { timeout: 15_000 })
.filter({ hasText: new RegExp(escaped) }) .toBeTruthy();
.first(); const count = await options.count();
await this.click(option, `选择 ${optionText}`); const compact = optionText.replace(/\s/g, '');
let target: Locator | null = null;
for (let i = 0; i < count; i += 1) {
const raw = ((await options.nth(i).innerText()) ?? '').replace(/\s/g, '');
if (!raw) continue;
if (raw === compact || raw.includes(compact) || compact.includes(raw)) {
target = options.nth(i);
break;
}
}
if (!target) {
await this.skipWhenDataInsufficient(`下拉「${label}」无匹配项「${optionText}」`);
}
target = target!;
await target.waitFor({ state: 'visible', timeout: 12_000 });
await this.click(target, `选择 ${optionText}`);
await this.waitSettle(200); await this.waitSettle(200);
} }
...@@ -310,7 +326,17 @@ export class WhFlowSharedPage extends BasePage { ...@@ -310,7 +326,17 @@ export class WhFlowSharedPage extends BasePage {
async interceptState(): Promise<{ hintVisible: boolean; overlayOpen: boolean }> { async interceptState(): Promise<{ hintVisible: boolean; overlayOpen: boolean }> {
const hint = this.page.getByText(/必填|请输入|请选择|不能为空|不可为空|请先|不合法/).first(); const hint = this.page.getByText(/必填|请输入|请选择|不能为空|不可为空|请先|不合法/).first();
const hintVisible = await this.locators.formError.first().or(hint).isVisible().catch(() => false); const formError = this.page.locator('.ant-form-item-explain-error:visible, .ant-form-item-has-error').first();
const invalidInput = this.page
.locator('input[aria-invalid="true"], .ant-input-status-error, .ant-select-status-error')
.first();
const toast = this.page.locator('.ant-message-error, .ant-notification-notice-error').first();
const hintVisible =
(await this.locators.formError.first().isVisible().catch(() => false)) ||
(await formError.isVisible().catch(() => false)) ||
(await invalidInput.isVisible().catch(() => false)) ||
(await toast.isVisible().catch(() => false)) ||
(await hint.isVisible().catch(() => false));
const overlayOpen = const overlayOpen =
(await this.page.locator('.put-page:visible').count()) > 0 || (await this.page.locator('.put-page:visible').count()) > 0 ||
(await this.page.locator('.ant-modal:visible').count()) > 0 || (await this.page.locator('.ant-modal:visible').count()) > 0 ||
...@@ -331,6 +357,60 @@ export class WhFlowSharedPage extends BasePage { ...@@ -331,6 +357,60 @@ export class WhFlowSharedPage extends BasePage {
return { onLogin, hasBusinessRow }; return { onLogin, hasBusinessRow };
} }
pageSizeChanger(): Locator {
return this.page.locator('.ant-pagination-options-size-changer').first();
}
async pageSizeChangerText(): Promise<string> {
return ((await this.pageSizeChanger().innerText().catch(() => '')) ?? '').replace(/\s+/g, ' ');
}
async changePageSizeTo(toLabel: string): Promise<void> {
const changer = this.pageSizeChanger();
if (!(await changer.isVisible().catch(() => false))) {
await this.skipWhenDataInsufficient('当前页面无每页条数切换');
}
const sizeNum = (toLabel.match(/\d+/) ?? ['20'])[0];
const current = await this.pageSizeChangerText();
if (current.includes(sizeNum) && current.includes('条')) {
return;
}
await this.assertBeforeBusinessAction('切换每页条数');
await this.click(changer, '打开每页条数');
const option = this.page
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option')
.filter({ hasText: new RegExp(`${sizeNum}\\s*条`) })
.first();
await this.click(option, `选择 ${toLabel}`);
await this.waitForTableReady();
const after = await this.pageSizeChangerText();
if (!after.includes(sizeNum)) {
throw new Error(`切换每页条数未生效:期望含 ${sizeNum},当前「${after || '(空)'}」`);
}
}
/** 切换为更小每页条数(如 20 条/页),返回切换前后可见行数与总条数。 */
async switchToPageSize(
pageSizeLabel: string,
): Promise<{ total: number; rowsBefore: number; rowsAfter: number; pageSizeLabel: string }> {
const sizeNum = Number((pageSizeLabel.match(/\d+/) ?? ['20'])[0]);
await skipIfInsufficientForPageSizeChange({
ensureHealthyBeforeSkip: (reason) => this.ensureHealthyBeforeSkip(reason),
waitForTableReady: () => this.waitForTableReady(),
getDataRowCount: () => this.getDataRowCount(),
});
const total = await this.getTotalCount();
const rowsBefore = await this.getDataRowCount();
if (total > sizeNum && rowsBefore <= sizeNum) {
await this.skipWhenDataInsufficient(
`当前页可见行数已不大于 ${sizeNum},无法验证切换 ${pageSizeLabel} 后列表变化`,
);
}
await this.changePageSizeTo(pageSizeLabel);
const rowsAfter = await this.getDataRowCount();
return { total, rowsBefore, rowsAfter, pageSizeLabel };
}
async visibleSelectOptions(label: string): Promise<string[]> { async visibleSelectOptions(label: string): Promise<string[]> {
const select = this.formSelect(label); const select = this.formSelect(label);
if (!(await select.isVisible().catch(() => false))) return []; if (!(await select.isVisible().catch(() => false))) return [];
......
...@@ -80,23 +80,6 @@ PAGES = { ...@@ -80,23 +80,6 @@ PAGES = {
], ],
value="样本", value="样本",
), ),
"vendor-auth-first.page.ts": dict(
cls="VendorAuthFirstPage",
method="filterCombination",
before=None,
fields=[
("制单日期", "dateRange"),
("单号", "text"),
("进院供应商", "select"),
("预转供应商", "text"),
("状态", "select"),
("变更类型", "select"),
("原供应商", "text"),
("医院ID", "text"),
("医院名称", "text"),
],
value="样本",
),
"vendor-auth-final.page.ts": dict( "vendor-auth-final.page.ts": dict(
cls="VendorAuthFinalPage", cls="VendorAuthFinalPage",
method="filterCombination", method="filterCombination",
...@@ -155,28 +138,6 @@ PAGES = { ...@@ -155,28 +138,6 @@ PAGES = {
], ],
value="样本", value="样本",
), ),
"vendor-eval-scheme.page.ts": dict(
cls="VendorEvalSchemePage",
method="filterCombination",
before=None,
fields=[
("创建日期", "dateRange"),
("启用日期", "dateRange"),
("方案编码", "text"),
("方案名称", "text"),
],
value="样本",
),
"vendor-eval-indicator.page.ts": dict(
cls="VendorEvalIndicatorPage",
method="filterCombination",
before=None,
fields=[
("指标编码", "text"),
("指标名称", "text"),
],
value="样本",
),
"surgery-base-pkg.page.ts": dict( "surgery-base-pkg.page.ts": dict(
cls="SurgeryBasePkgPage", cls="SurgeryBasePkgPage",
method="filterCombination", method="filterCombination",
......
...@@ -19,13 +19,10 @@ TARGETS = [ ...@@ -19,13 +19,10 @@ TARGETS = [
("基础包维护", "SURG-BASE", "surgery-base-pkg.page.ts", "surgery-base-pkg.spec.ts", "surgeryBasePkgPage"), ("基础包维护", "SURG-BASE", "surgery-base-pkg.page.ts", "surgery-base-pkg.spec.ts", "surgeryBasePkgPage"),
("标准套包维护", "SURG-STD", "surgery-std-pkg.page.ts", "surgery-std-pkg.spec.ts", "surgeryStdPkgPage"), ("标准套包维护", "SURG-STD", "surgery-std-pkg.page.ts", "surgery-std-pkg.spec.ts", "surgeryStdPkgPage"),
("供应商资质管理", "VENDOR-QUL", "vendor-qual.page.ts", "vendor-qual.spec.ts", "vendorQualPage"), ("供应商资质管理", "VENDOR-QUL", "vendor-qual.page.ts", "vendor-qual.spec.ts", "vendorQualPage"),
("授权变更初审", "VENDOR-AF1", "vendor-auth-first.page.ts", "vendor-audit.spec.ts", "vendorAuthFirstPage"),
("授权变更终审", "VENDOR-AF2", "vendor-auth-final.page.ts", "vendor-audit.spec.ts", "vendorAuthFinalPage"), ("授权变更终审", "VENDOR-AF2", "vendor-auth-final.page.ts", "vendor-audit.spec.ts", "vendorAuthFinalPage"),
("注册证变更初审", "VENDOR-CF1", "vendor-cert-first.page.ts", "vendor-audit.spec.ts", "vendorCertFirstPage"), ("注册证变更初审", "VENDOR-CF1", "vendor-cert-first.page.ts", "vendor-audit.spec.ts", "vendorCertFirstPage"),
("注册证变更终审", "VENDOR-CF2", "vendor-cert-final.page.ts", "vendor-audit.spec.ts", "vendorCertFinalPage"), ("注册证变更终审", "VENDOR-CF2", "vendor-cert-final.page.ts", "vendor-audit.spec.ts", "vendorCertFinalPage"),
("注册证差异对比", "VENDOR-CDF", "vendor-cert-diff.page.ts", "vendor-cert-diff.spec.ts", "vendorCertDiffPage"), ("注册证差异对比", "VENDOR-CDF", "vendor-cert-diff.page.ts", "vendor-cert-diff.spec.ts", "vendorCertDiffPage"),
("评价指标维护", "VENDOR-EVI", "vendor-eval-indicator.page.ts", "vendor-eval-indicator.spec.ts", "vendorEvalIndicatorPage"),
("评价方案维护", "VENDOR-EVS", "vendor-eval-scheme.page.ts", "vendor-eval-scheme.spec.ts", "vendorEvalSchemePage"),
] ]
......
...@@ -16,13 +16,10 @@ TARGETS = { ...@@ -16,13 +16,10 @@ TARGETS = {
"基础包维护": ("surgery-base-pkg.spec.ts", "基础包维护 · 右侧筛选与列表", "surgeryBasePkgPage"), "基础包维护": ("surgery-base-pkg.spec.ts", "基础包维护 · 右侧筛选与列表", "surgeryBasePkgPage"),
"标准套包维护": ("surgery-std-pkg.spec.ts", "标准套包维护 · 筛选与构成列表", "surgeryStdPkgPage"), "标准套包维护": ("surgery-std-pkg.spec.ts", "标准套包维护 · 筛选与构成列表", "surgeryStdPkgPage"),
"供应商资质管理": ("vendor-qual.spec.ts", "供应商资质管理 · 筛选与 Tab", "vendorQualPage"), "供应商资质管理": ("vendor-qual.spec.ts", "供应商资质管理 · 筛选与 Tab", "vendorQualPage"),
"授权变更初审": ("vendor-audit.spec.ts", "授权变更初审 · 筛选与列表", "vendorAuthFirstPage"),
"授权变更终审": ("vendor-audit.spec.ts", "授权变更终审 · 筛选与列表", "vendorAuthFinalPage"), "授权变更终审": ("vendor-audit.spec.ts", "授权变更终审 · 筛选与列表", "vendorAuthFinalPage"),
"注册证变更初审": ("vendor-audit.spec.ts", "注册证变更初审 · 筛选与列表", "vendorCertFirstPage"), "注册证变更初审": ("vendor-audit.spec.ts", "注册证变更初审 · 筛选与列表", "vendorCertFirstPage"),
"注册证变更终审": ("vendor-audit.spec.ts", "注册证变更终审 · 筛选与列表", "vendorCertFinalPage"), "注册证变更终审": ("vendor-audit.spec.ts", "注册证变更终审 · 筛选与列表", "vendorCertFinalPage"),
"注册证差异对比": ("vendor-cert-diff.spec.ts", "注册证差异对比 · 筛选与对比", "vendorCertDiffPage"), "注册证差异对比": ("vendor-cert-diff.spec.ts", "注册证差异对比 · 筛选与对比", "vendorCertDiffPage"),
"评价指标维护": ("vendor-eval-indicator.spec.ts", "评价指标维护 · 树与列表", "vendorEvalIndicatorPage"),
"评价方案维护": ("vendor-eval-scheme.spec.ts", "评价方案维护 · 筛选与维护", "vendorEvalSchemePage"),
} }
......
...@@ -77,6 +77,19 @@ function parseCsv(text) { ...@@ -77,6 +77,19 @@ function parseCsv(text) {
return rows; return rows;
} }
function evsSortTitlesFromData() {
const dataPath = path.join(ROOT, 'data/vendor-eval-scheme.data.ts');
const text = readFileSync(dataPath, 'utf8');
const block = text.match(/vendorEvalSchemeSortColumns\s*=\s*\[([\s\S]*?)\]\s*as const/);
if (!block) return [];
const columns = [...block[1].matchAll(/'([^']+)'/g)].map((m) => m[1]);
return columns.map(
(column, index) => `VENDOR-EVS-${String(4 + index).padStart(3, '0')} ${column}列升序降序切换`,
);
}
const evsSortTitleSet = new Set(evsSortTitlesFromData());
const missing = []; const missing = [];
let total = 0; let total = 0;
for (const [csvRel, specRel] of PAIRS) { for (const [csvRel, specRel] of PAIRS) {
...@@ -92,7 +105,13 @@ for (const [csvRel, specRel] of PAIRS) { ...@@ -92,7 +105,13 @@ for (const [csvRel, specRel] of PAIRS) {
if (!cols[idIdx]) continue; if (!cols[idIdx]) continue;
total += 1; total += 1;
const title = `${cols[idIdx]} ${cols[titleIdx]}`; const title = `${cols[idIdx]} ${cols[titleIdx]}`;
if (!spec.includes(title)) missing.push(`${specRel}: ${title}`); const isEvsSort =
specRel === 'tests/spd/vendor-eval-scheme.spec.ts' &&
/^VENDOR-EVS-0(0[4-9]|1[0-3])$/.test(cols[idIdx]);
const titleOk =
spec.includes(title) ||
(isEvsSort && evsSortTitleSet.has(title) && spec.includes('sortColumnCases'));
if (!titleOk) missing.push(`${specRel}: ${title}`);
if (cols[pointIdx] && !spec.includes(cols[pointIdx])) { if (cols[pointIdx] && !spec.includes(cols[pointIdx])) {
missing.push(`${specRel}: describe ${cols[pointIdx]}`); missing.push(`${specRel}: describe ${cols[pointIdx]}`);
} }
......
#!/usr/bin/env node
/**
* 本地执行产物清理(不删源码)。保留 reports/README.md、reports/latest、reports/flake。
* 用法: node scripts/cleanup-local-artifacts.mjs
*/
import { rmSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const KEEP_IN_REPORTS = new Set(['README.md', 'latest', 'flake']);
const REMOVE_DIRS = [
'playwright-report',
'allure-results',
'allure-report',
'.pw-home',
'test-results',
];
function rmPath(target) {
try {
rmSync(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
return true;
} catch (e) {
console.warn(`SKIP ${target}: ${e.message}`);
return false;
}
}
let freed = 0;
function sizeOf(p) {
try {
return statSync(p).size;
} catch {
return 0;
}
}
for (const rel of REMOVE_DIRS) {
const abs = path.join(ROOT, rel);
try {
statSync(abs);
rmPath(abs);
console.log(`removed ${rel}`);
} catch {
/* absent */
}
}
const logsDir = path.join(ROOT, 'logs');
try {
for (const name of readdirSync(logsDir)) {
if (/\.(log|pid)$/i.test(name)) rmPath(path.join(logsDir, name));
}
} catch {
/* no logs */
}
const reportsDir = path.join(ROOT, 'reports');
for (const name of readdirSync(reportsDir)) {
if (KEEP_IN_REPORTS.has(name)) continue;
const abs = path.join(reportsDir, name);
rmPath(abs);
console.log(`removed reports/${name}`);
}
console.log('done');
...@@ -214,13 +214,14 @@ def main() -> int: ...@@ -214,13 +214,14 @@ def main() -> int:
delete = { delete = {
x.strip() x.strip()
for x in """ for x in """
ADMIT-MAT-012 ADMIT-MLS-012 ADMIT-PAP-013 ADMIT-PAP-014 ADMIT-PLS-012 ADMIT-SUP-008 ADMIT-SUP-013 ADMIT-SUP-014 ADMIT-SUP-017 ADMIT-TSK-011 ADMIT-MLS-012 ADMIT-PAP-011 ADMIT-PAP-012 ADMIT-PLS-012 ADMIT-SUP-004 ADMIT-SUP-009 ADMIT-SUP-010 ADMIT-SUP-017 ADMIT-TSK-011
BASIC-BRN-004 BASIC-BRN-009 BASIC-BRN-013 BASIC-FMN-010 BASIC-FMN-012 BASIC-FMN-013 BASIC-FMN-014 BASIC-NTC-013 BASIC-NTC-014 BASIC-NTC-015 BASIC-BRN-004 BASIC-BRN-009 BASIC-BRN-013 BASIC-FMN-010 BASIC-FMN-012 BASIC-FMN-013 BASIC-FMN-014 BASIC-NTC-013 BASIC-NTC-014 BASIC-NTC-015
FLOW-DEF-004 FLOW-DEF-006 FLOW-DEF-007 FLOW-DEF-013 FLOW-INS-001 FLOW-INS-005 FLOW-INS-009 FLOW-DEF-004 FLOW-DEF-006 FLOW-DEF-007 FLOW-DEF-013 FLOW-INS-001 FLOW-INS-005 FLOW-INS-009
SURG-BASE-001 SURG-BASE-009 SURG-BASE-016 SURG-BASE-019 SURG-BASE-022 SURG-STD-004 SURG-STD-005 SURG-STD-007 SURG-STD-011 SURG-STD-014 SURG-STD-020 SURG-TYPE-011 SURG-BASE-001 SURG-BASE-009 SURG-BASE-016 SURG-BASE-019 SURG-BASE-022 SURG-STD-004 SURG-STD-005 SURG-STD-005 SURG-STD-009 SURG-STD-010 SURG-STD-020 SURG-TYPE-011
USER-ROL-011 USER-UNI-002 USER-UNI-004 USER-UNI-013 USER-UNI-015 USER-UNI-016 USER-USR-001 USER-USR-003 USER-USR-017 USER-USR-018 USER-ROL-011 USER-UNI-002 USER-UNI-004 USER-UNI-011 USER-UNI-015 USER-UNI-016 USER-USR-001 USER-USR-003 USER-USR-017 USER-USR-018
VENDOR-AF1-007 VENDOR-AF2-007 VENDOR-CF1-007 VENDOR-CF2-007 VENDOR-EVI-008 VENDOR-EVS-009 VENDOR-HLD-006 VENDOR-HLD-007 VENDOR-QUL-008 VENDOR-QUL-009 VENDOR-QUL-012 VENDOR-AF1-007 VENDOR-AF2-007 VENDOR-CF1-007 VENDOR-CF2-007 VENDOR-EVI-008
WH-PRB-006 WH-ZON-004 WH-ZON-007 WH-ZON-019 WH-ZON-020 VENDOR-EVS-002 VENDOR-EVS-003 VENDOR-EVS-004 VENDOR-EVS-005 VENDOR-EVS-006 VENDOR-EVS-007 VENDOR-EVS-008 VENDOR-EVS-009 VENDOR-EVS-010
WH-PRB-006 WH-ZON-003 WH-ZON-005 WH-ZON-019 WH-ZON-020
""".split() """.split()
if x.strip() if x.strip()
} }
...@@ -274,8 +275,6 @@ WH-PRB-006 WH-ZON-004 WH-ZON-007 WH-ZON-019 WH-ZON-020 ...@@ -274,8 +275,6 @@ WH-PRB-006 WH-ZON-004 WH-ZON-007 WH-ZON-019 WH-ZON-020
continue continue
updated = remove_md_sections(text, delete) if fp.suffix == ".md" else text updated = remove_md_sections(text, delete) if fp.suffix == ".md" else text
updated = apply_remap(updated, global_remap) updated = apply_remap(updated, global_remap)
if fp.suffix == ".ts" and "tests/spd" in str(fp):
updated = remove_tests_from_spec(updated, delete)
if updated != text: if updated != text:
fp.write_text(updated, encoding="utf-8") fp.write_text(updated, encoding="utf-8")
......
...@@ -43,8 +43,8 @@ const TARGETS = [ ...@@ -43,8 +43,8 @@ const TARGETS = [
} }
}, },
checks: [ checks: [
{ caseId: 'STAT-RPE-020', labels: ['产品批号'], note: '需勾选维度「区分批号」' }, { caseId: 'STAT-RPE-019', labels: ['产品批号'], note: '需勾选维度「区分批号」' },
{ caseId: 'STAT-RPE-022', labels: ['产品类型'] }, { caseId: 'STAT-RPE-021', labels: ['产品类型'] },
], ],
}, },
]; ];
......
...@@ -170,7 +170,7 @@ bp( ...@@ -170,7 +170,7 @@ bp(
"编辑弹窗回填正确;取消不影响数据。", "编辑弹窗回填正确;取消不影响数据。",
) )
bp( bp(
"SURG-BASE-017", "基础包维护 · 行内操作(编辑 / 移除)", "移除二次确认防误删", "高", B_SAMPLE, "SURG-BASE-016", "基础包维护 · 行内操作(编辑 / 移除)", "移除二次确认防误删", "高", B_SAMPLE,
f"{LOGIN_NOTE}\n2. 记录当前总数 `§sampleTotal§`\n3. 点击首行「移 除」\n4. 若出现确认框则点「取 消」\n5. 断言总数仍为 `§sampleTotal§`", f"{LOGIN_NOTE}\n2. 记录当前总数 `§sampleTotal§`\n3. 点击首行「移 除」\n4. 若出现确认框则点「取 消」\n5. 断言总数仍为 `§sampleTotal§`",
"移除有二次确认;取消不删除。", "移除有二次确认;取消不删除。",
) )
...@@ -273,56 +273,56 @@ tp( ...@@ -273,56 +273,56 @@ tp(
"核心列均可见,操作列含「编 辑」「停用/启用」。", "核心列均可见,操作列含「编 辑」「停用/启用」。",
) )
tp( tp(
"SURG-TYPE-015", "术种维护 · 列表与编码展示", "ICD-10 诊断编码展示完整", "中", "SURG-TYPE-014", "术种维护 · 列表与编码展示", "ICD-10 诊断编码展示完整", "中",
f"{T_SAMPLE};至少 1 条含诊断编码的数据", f"{T_SAMPLE};至少 1 条含诊断编码的数据",
f"{LOGIN_NOTE}\n2. 读取含 ICD-10 编码的行 `§sampleIcd10Code§`\n3. 断言编码文本完整(如 A02.000x010)不截断、无乱码", f"{LOGIN_NOTE}\n2. 读取含 ICD-10 编码的行 `§sampleIcd10Code§`\n3. 断言编码文本完整(如 A02.000x010)不截断、无乱码",
"ICD-10 编码完整展示。", "ICD-10 编码完整展示。",
) )
tp( tp(
"SURG-TYPE-016", "术种维护 · 列表与编码展示", "ICD-9-CM-3 操作编码展示完整", "低", "SURG-TYPE-014", "术种维护 · 列表与编码展示", "ICD-9-CM-3 操作编码展示完整", "低",
f"{T_SAMPLE};至少 1 条含操作编码的数据", f"{T_SAMPLE};至少 1 条含操作编码的数据",
f"{LOGIN_NOTE}\n2. 读取含操作编码的行 `§sampleIcd9Code§`\n3. 断言操作编码(如 00.0200)与操作名称(如 心脏治疗性超声)对应展示无乱码", f"{LOGIN_NOTE}\n2. 读取含操作编码的行 `§sampleIcd9Code§`\n3. 断言操作编码(如 00.0200)与操作名称(如 心脏治疗性超声)对应展示无乱码",
"ICD-9-CM-3 编码与操作名称对应正确。", "ICD-9-CM-3 编码与操作名称对应正确。",
) )
tp( tp(
"SURG-TYPE-017", "术种维护 · 列表与编码展示", "分页与页大小切换", "中", "SURG-TYPE-015", "术种维护 · 列表与编码展示", "分页与页大小切换", "中",
f"{T_SAMPLE};列表总数大于单页行数上限", f"{T_SAMPLE};列表总数大于单页行数上限",
f"{LOGIN_NOTE}\n2. 切换每页条数(如 50 → 100)\n3. 断言单页行数不超过所选值,分页总数不变", f"{LOGIN_NOTE}\n2. 切换每页条数(如 50 → 100)\n3. 断言单页行数不超过所选值,分页总数不变",
"页大小切换生效;总数保持稳定。", "页大小切换生效;总数保持稳定。",
) )
tp( tp(
"SURG-TYPE-018", "术种维护 · 列表与编码展示", "首页上一页不可用", "低", T_PAGE, "SURG-TYPE-016", "术种维护 · 列表与编码展示", "首页上一页不可用", "低", T_PAGE,
f"{LOGIN_NOTE}\n2. 处于第 1 页\n3. 断言「上一页」置灰或点击无效果", f"{LOGIN_NOTE}\n2. 处于第 1 页\n3. 断言「上一页」置灰或点击无效果",
"第 1 页无法回到上一页。", "第 1 页无法回到上一页。",
) )
tp( tp(
"SURG-TYPE-019", "术种维护 · 新增术种", "打开新增术种入口", "高", T_PAGE, "SURG-TYPE-017", "术种维护 · 新增术种", "打开新增术种入口", "高", T_PAGE,
f"{LOGIN_NOTE}\n2. 点击「新增术种」\n3. 断言新增弹窗/页面可见,含术种名称、病种名称、分类与编码相关字段\n4. 断言含「取 消」「保 存」", f"{LOGIN_NOTE}\n2. 点击「新增术种」\n3. 断言新增弹窗/页面可见,含术种名称、病种名称、分类与编码相关字段\n4. 断言含「取 消」「保 存」",
"新增术种入口字段完整。", "新增术种入口字段完整。",
) )
tp( tp(
"SURG-TYPE-020", "术种维护 · 新增术种", "新增取消不落库", "高", T_PAGE, "SURG-TYPE-018", "术种维护 · 新增术种", "新增取消不落库", "高", T_PAGE,
f"{LOGIN_NOTE}\n2. 记录当前分页总数 `§sampleTotal§`\n3. 打开新增,填写名称 `§uniqOperName§` 后点「取 消」\n4. 断言列表总数不变,且无该名称行", f"{LOGIN_NOTE}\n2. 记录当前分页总数 `§sampleTotal§`\n3. 打开新增,填写名称 `§uniqOperName§` 后点「取 消」\n4. 断言列表总数不变,且无该名称行",
"取消后不新增术种。", "取消后不新增术种。",
) )
tp( tp(
"SURG-TYPE-021", "术种维护 · 新增术种", "术种名称为空拦截(mock反例)", "高", T_PAGE, "SURG-TYPE-019", "术种维护 · 新增术种", "术种名称为空拦截(mock反例)", "高", T_PAGE,
f"{MOCK_NOTE}\n2. 打开新增术种弹窗,名称留空\n3. 点击「保 存」\n4. 断言必填提示,弹窗仍在,列表无新行", f"{MOCK_NOTE}\n2. 打开新增术种弹窗,名称留空\n3. 点击「保 存」\n4. 断言必填提示,弹窗仍在,列表无新行",
"保存被拦截;不落库。", "保存被拦截;不落库。",
) )
tp( tp(
"SURG-TYPE-022", "术种维护 · 行内操作(编辑 / 停用)", "行内编辑回填", "高", T_SAMPLE, "SURG-TYPE-020", "术种维护 · 行内操作(编辑 / 停用)", "行内编辑回填", "高", T_SAMPLE,
f"{LOGIN_NOTE}\n2. 点击首行「编 辑」\n3. 断言编辑弹窗可见且回填当前术种数据\n4. 点击「取 消」关闭", f"{LOGIN_NOTE}\n2. 点击首行「编 辑」\n3. 断言编辑弹窗可见且回填当前术种数据\n4. 点击「取 消」关闭",
"编辑弹窗回填正确;取消不影响数据。", "编辑弹窗回填正确;取消不影响数据。",
) )
tp( tp(
"SURG-TYPE-023", "术种维护 · 行内操作(编辑 / 停用)", "停用状态流转", "中", "SURG-TYPE-021", "术种维护 · 行内操作(编辑 / 停用)", "停用状态流转", "中",
f"{T_PAGE};存在启用状态术种(优先选 `§uniqOperName§` 自建数据);允许写术种", f"{T_PAGE};存在启用状态术种(优先选 `§uniqOperName§` 自建数据);允许写术种",
f"{LOGIN_NOTE}\n2. 记录「停用」Tab 当前计数 `§disabledCount§`\n3. 点击目标行「停用」,若有确认框则点「确 定」\n4. 断言「停用」Tab 计数 +1\n5. 后置:对该术种执行「启用」恢复", f"{LOGIN_NOTE}\n2. 记录「停用」Tab 当前计数 `§disabledCount§`\n3. 点击目标行「停用」,若有确认框则点「确 定」\n4. 断言「停用」Tab 计数 +1\n5. 后置:对该术种执行「启用」恢复",
"术种状态变为停用;「停用」计数 +1;结束恢复原状态。", "术种状态变为停用;「停用」计数 +1;结束恢复原状态。",
) )
tp( tp(
"SURG-TYPE-024", "术种维护 · 行内操作(编辑 / 停用)", "停用二次确认防误操作", "高", "SURG-TYPE-022", "术种维护 · 行内操作(编辑 / 停用)", "停用二次确认防误操作", "高",
f"{T_PAGE};存在启用状态术种", f"{T_PAGE};存在启用状态术种",
f"{LOGIN_NOTE}\n2. 记录「停用」Tab 当前计数\n3. 点击首行「停用」\n4. 若出现确认框则点「取 消」\n5. 断言计数未变化", f"{LOGIN_NOTE}\n2. 记录「停用」Tab 当前计数\n3. 点击首行「停用」\n4. 若出现确认框则点「取 消」\n5. 断言计数未变化",
"停用有二次确认;取消不改状态。", "停用有二次确认;取消不改状态。",
...@@ -374,7 +374,7 @@ sp( ...@@ -374,7 +374,7 @@ sp(
"清空后分类树恢复默认集合。", "清空后分类树恢复默认集合。",
) )
sp( sp(
"SURG-STD-007", "标准套包维护 · 筛选与构成列表", "默认展示标准套包构成", "高", S_PAGE, "SURG-STD-005", "标准套包维护 · 筛选与构成列表", "默认展示标准套包构成", "高", S_PAGE,
f"{LOGIN_NOTE}\n2. 等待列表稳定\n3. 断言按「包名称」分组出现数据行,或「暂无数据」且分页总数为 0", f"{LOGIN_NOTE}\n2. 等待列表稳定\n3. 断言按「包名称」分组出现数据行,或「暂无数据」且分页总数为 0",
"加载无报错;有数据时「共 N 条」N≥1;无数据时空态明确。", "加载无报错;有数据时「共 N 条」N≥1;无数据时空态明确。",
) )
...@@ -384,23 +384,23 @@ sp( ...@@ -384,23 +384,23 @@ sp(
"过滤结果与关键字匹配;「共 N 条」随之更新。", "过滤结果与关键字匹配;「共 N 条」随之更新。",
) )
sp( sp(
"SURG-STD-006", "标准套包维护 · 筛选与构成列表", "规格型号筛选过滤", "中", S_SAMPLE, "SURG-STD-005", "标准套包维护 · 筛选与构成列表", "规格型号筛选过滤", "中", S_SAMPLE,
f"{LOGIN_NOTE}\n2. 从首行读取 `§sampleSpecModel§`\n3. 在 `#table_search_form_goodsSpec` 填入该关键字并点击「查 询」\n4. 断言结果行规格型号均包含该关键字", f"{LOGIN_NOTE}\n2. 从首行读取 `§sampleSpecModel§`\n3. 在 `#table_search_form_goodsSpec` 填入该关键字并点击「查 询」\n4. 断言结果行规格型号均包含该关键字",
"按规格型号过滤生效。", "按规格型号过滤生效。",
) )
sp( sp(
"SURG-STD-007", "标准套包维护 · 筛选与构成列表", "器械注册人筛选过滤", "中", S_SAMPLE, "SURG-STD-005", "标准套包维护 · 筛选与构成列表", "器械注册人筛选过滤", "中", S_SAMPLE,
f"{LOGIN_NOTE}\n2. 从首行读取 `§sampleMfrsName§`\n3. 在 `#table_search_form_goodsMfrsName` 填入该关键字并点击「查 询」\n4. 断言结果行器械注册人均包含该关键字", f"{LOGIN_NOTE}\n2. 从首行读取 `§sampleMfrsName§`\n3. 在 `#table_search_form_goodsMfrsName` 填入该关键字并点击「查 询」\n4. 断言结果行器械注册人均包含该关键字",
"按器械注册人过滤生效。", "按器械注册人过滤生效。",
) )
sp( sp(
"SURG-STD-011", "标准套包维护 · 筛选与构成列表", "构成列表核心列完整", "高", "SURG-STD-009", "标准套包维护 · 筛选与构成列表", "构成列表核心列完整", "高",
f"{S_SAMPLE};存在分组", f"{S_SAMPLE};存在分组",
f"{LOGIN_NOTE}\n2. 断言表头/首行包含包名称、包数量/每包含量、产品编号、产品名称、规格型号、器械注册人、单位、产地、耗材类型、操作", f"{LOGIN_NOTE}\n2. 断言表头/首行包含包名称、包数量/每包含量、产品编号、产品名称、规格型号、器械注册人、单位、产地、耗材类型、操作",
"核心列均可见,操作列含「编 辑」「移 除」。", "核心列均可见,操作列含「编 辑」「移 除」。",
) )
sp( sp(
"SURG-STD-008", "标准套包维护 · 筛选与构成列表", "包名称展开/收起子项", "高", "SURG-STD-006", "标准套包维护 · 筛选与构成列表", "包名称展开/收起子项", "高",
f"{S_SAMPLE};存在可展开的包分组", f"{S_SAMPLE};存在可展开的包分组",
f"{LOGIN_NOTE}\n2. 读取首个包分组名称 `§samplePkgGroupName§`\n3. 点击展开该分组,断言出现子项行\n4. 再次点击收起,断言子项行隐藏", f"{LOGIN_NOTE}\n2. 读取首个包分组名称 `§samplePkgGroupName§`\n3. 点击展开该分组,断言出现子项行\n4. 再次点击收起,断言子项行隐藏",
"展开显示子项、收起隐藏子项;分组计数不受影响。", "展开显示子项、收起隐藏子项;分组计数不受影响。",
...@@ -411,34 +411,34 @@ sp( ...@@ -411,34 +411,34 @@ sp(
"重置清空筛选并恢复默认列表。", "重置清空筛选并恢复默认列表。",
) )
sp( sp(
"SURG-STD-014", "标准套包维护 · 筛选与构成列表", "分页与页大小切换", "中", "SURG-STD-010", "标准套包维护 · 筛选与构成列表", "分页与页大小切换", "中",
f"{S_SAMPLE};列表总数大于单页行数上限", f"{S_SAMPLE};列表总数大于单页行数上限",
f"{LOGIN_NOTE}\n2. 切换每页条数(如 50 → 100)\n3. 断言单页行数不超过所选值,分页总数不变", f"{LOGIN_NOTE}\n2. 切换每页条数(如 50 → 100)\n3. 断言单页行数不超过所选值,分页总数不变",
"页大小切换生效;总数保持稳定。", "页大小切换生效;总数保持稳定。",
) )
sp( sp(
"SURG-STD-010", "标准套包维护 · 筛选与构成列表", "无匹配组合空态(mock反例)", "低", S_PAGE, "SURG-STD-007", "标准套包维护 · 筛选与构成列表", "无匹配组合空态(mock反例)", "低", S_PAGE,
f"{MOCK_NOTE}\n2. 在 `#table_search_form_keyword` 输入 mock 不存在关键字 `__NOT_EXIST_STD__99999`\n3. 点击「查 询」\n4. 断言「暂无数据」且分页总数为 0", f"{MOCK_NOTE}\n2. 在 `#table_search_form_keyword` 输入 mock 不存在关键字 `__NOT_EXIST_STD__99999`\n3. 点击「查 询」\n4. 断言「暂无数据」且分页总数为 0",
"无匹配时空态明确,不白屏;总数为 0。", "无匹配时空态明确,不白屏;总数为 0。",
) )
sp( sp(
"SURG-STD-011", "标准套包维护 · 添加基础包弹窗", "打开添加基础包弹窗", "高", S_PAGE, "SURG-STD-009", "标准套包维护 · 添加基础包弹窗", "打开添加基础包弹窗", "高", S_PAGE,
f"{LOGIN_NOTE}\n2. 点击「添加基础包」\n3. 断言弹窗可见,含左侧基础包分类树与右侧基础包明细\n4. 断言含「取 消」「确定(N)」", f"{LOGIN_NOTE}\n2. 点击「添加基础包」\n3. 断言弹窗可见,含左侧基础包分类树与右侧基础包明细\n4. 断言含「取 消」「确定(N)」",
"添加基础包弹窗字段完整;确定按钮带选择计数。", "添加基础包弹窗字段完整;确定按钮带选择计数。",
) )
sp( sp(
"SURG-STD-012", "标准套包维护 · 添加基础包弹窗", "弹窗内筛选与选择计数联动", "高", S_PAGE, "SURG-STD-008", "标准套包维护 · 添加基础包弹窗", "弹窗内筛选与选择计数联动", "高", S_PAGE,
f"{LOGIN_NOTE}\n2. 打开添加基础包弹窗,在搜索框输入基础包关键字\n3. 勾选 1 个基础包\n4. 断言「确定(N)」计数由 0 变为 1", f"{LOGIN_NOTE}\n2. 打开添加基础包弹窗,在搜索框输入基础包关键字\n3. 勾选 1 个基础包\n4. 断言「确定(N)」计数由 0 变为 1",
"筛选生效;选择数与确定按钮计数联动。", "筛选生效;选择数与确定按钮计数联动。",
) )
sp( sp(
"SURG-STD-013", "标准套包维护 · 添加基础包弹窗", "弹窗内构成数量可编辑", "中", "SURG-STD-009", "标准套包维护 · 添加基础包弹窗", "弹窗内构成数量可编辑", "中",
f"{S_PAGE};已勾选至少 1 个基础包", f"{S_PAGE};已勾选至少 1 个基础包",
f"{LOGIN_NOTE}\n2. 定位明细行「构成数量」步进器,点击「Increase Value」\n3. 断言数量 +1 且合计随之更新", f"{LOGIN_NOTE}\n2. 定位明细行「构成数量」步进器,点击「Increase Value」\n3. 断言数量 +1 且合计随之更新",
"构成数量可编辑;加减生效。", "构成数量可编辑;加减生效。",
) )
sp( sp(
"SURG-STD-014", "标准套包维护 · 添加基础包弹窗", "添加基础包取消不落库", "高", S_PAGE, "SURG-STD-010", "标准套包维护 · 添加基础包弹窗", "添加基础包取消不落库", "高", S_PAGE,
f"{LOGIN_NOTE}\n2. 记录当前分页总数 `§sampleTotal§`\n3. 打开添加基础包,勾选 1 个基础包后点「取 消」\n4. 断言构成列表总数不变", f"{LOGIN_NOTE}\n2. 记录当前分页总数 `§sampleTotal§`\n3. 打开添加基础包,勾选 1 个基础包后点「取 消」\n4. 断言构成列表总数不变",
"取消后不新增构成行。", "取消后不新增构成行。",
) )
...@@ -449,18 +449,18 @@ sp( ...@@ -449,18 +449,18 @@ sp(
"未选择时确定不可用。", "未选择时确定不可用。",
) )
sp( sp(
"SURG-STD-015", "标准套包维护 · 添加基础包弹窗", "重复添加同一基础包", "中", "SURG-STD-011", "标准套包维护 · 添加基础包弹窗", "重复添加同一基础包", "中",
f"{S_PAGE};标准套包已含某基础包(或先添加一个);允许写套包", f"{S_PAGE};标准套包已含某基础包(或先添加一个);允许写套包",
f"{LOGIN_NOTE}\n2. 再次打开添加基础包,选择同一基础包并确定\n3. 断言按产品规则拒绝/合并/累加,不产生重复脏行", f"{LOGIN_NOTE}\n2. 再次打开添加基础包,选择同一基础包并确定\n3. 断言按产品规则拒绝/合并/累加,不产生重复脏行",
"重复添加不产生重复行(策略以现网为准)。", "重复添加不产生重复行(策略以现网为准)。",
) )
sp( sp(
"SURG-STD-016", "标准套包维护 · 添加散件弹窗", "打开添加散件弹窗", "高", S_PAGE, "SURG-STD-012", "标准套包维护 · 添加散件弹窗", "打开添加散件弹窗", "高", S_PAGE,
f"{LOGIN_NOTE}\n2. 点击「添加散件」\n3. 断言弹窗可见并展示「产品列表」视图,含产品名称/规格型号/器械注册人/耗材类型筛选与分页", f"{LOGIN_NOTE}\n2. 点击「添加散件」\n3. 断言弹窗可见并展示「产品列表」视图,含产品名称/规格型号/器械注册人/耗材类型筛选与分页",
"添加散件弹窗产品列表视图完整。", "添加散件弹窗产品列表视图完整。",
) )
sp( sp(
"SURG-STD-017", "标准套包维护 · 添加散件弹窗", "产品列表筛选与选择散件", "高", "SURG-STD-013", "标准套包维护 · 添加散件弹窗", "产品列表筛选与选择散件", "高",
f"{S_PAGE};已打开添加散件弹窗", f"{S_PAGE};已打开添加散件弹窗",
f"{LOGIN_NOTE}\n2. 在产品名称筛选框输入 `§sampleProductName§`\n3. 勾选首个匹配散件\n4. 断言散件进入应拣明细,计数同步变化", f"{LOGIN_NOTE}\n2. 在产品名称筛选框输入 `§sampleProductName§`\n3. 勾选首个匹配散件\n4. 断言散件进入应拣明细,计数同步变化",
"筛选生效;选中散件进入应拣明细。", "筛选生效;选中散件进入应拣明细。",
...@@ -483,19 +483,19 @@ sp( ...@@ -483,19 +483,19 @@ sp(
"取消后不新增散件行。", "取消后不新增散件行。",
) )
sp( sp(
"SURG-STD-021", "标准套包维护 · 添加散件弹窗", "万级散件列表首屏可交互", "低", "SURG-STD-014", "标准套包维护 · 添加散件弹窗", "万级散件列表首屏可交互", "低",
f"{S_PAGE};添加散件产品列表总量达万级(示例「共 16299 条」)", f"{S_PAGE};添加散件产品列表总量达万级(示例「共 16299 条」)",
f"{LOGIN_NOTE}\n2. 打开添加散件弹窗\n3. 断言首屏可交互(筛选/分页可用),不卡死\n4. 避免全量断言,仅校验首行与总数格式", f"{LOGIN_NOTE}\n2. 打开添加散件弹窗\n3. 断言首屏可交互(筛选/分页可用),不卡死\n4. 避免全量断言,仅校验首行与总数格式",
"万级列表首屏可用;无超时白屏。", "万级列表首屏可用;无超时白屏。",
) )
sp( sp(
"SURG-STD-022", "标准套包维护 · 行内操作(编辑 / 移除)", "行内编辑构成项", "高", "SURG-STD-015", "标准套包维护 · 行内操作(编辑 / 移除)", "行内编辑构成项", "高",
f"{S_SAMPLE};先展开目标包", f"{S_SAMPLE};先展开目标包",
f"{LOGIN_NOTE}\n2. 点击子项行「编 辑」\n3. 断言编辑弹窗/表单可见并回填当前值\n4. 点击「取 消」关闭", f"{LOGIN_NOTE}\n2. 点击子项行「编 辑」\n3. 断言编辑弹窗/表单可见并回填当前值\n4. 点击「取 消」关闭",
"编辑回填正确;取消不影响数据。", "编辑回填正确;取消不影响数据。",
) )
sp( sp(
"SURG-STD-023", "标准套包维护 · 行内操作(编辑 / 移除)", "移除二次确认防误删整包", "高", "SURG-STD-018", "标准套包维护 · 行内操作(编辑 / 移除)", "移除二次确认防误删整包", "高",
f"{S_SAMPLE};先展开目标包", f"{S_SAMPLE};先展开目标包",
f"{LOGIN_NOTE}\n2. 记录当前行数\n3. 点击子项行「移 除」\n4. 若出现确认框则点「取 消」\n5. 断言行数未变化", f"{LOGIN_NOTE}\n2. 记录当前行数\n3. 点击子项行「移 除」\n4. 若出现确认框则点「取 消」\n5. 断言行数未变化",
"移除有二次确认;取消不删除。", "移除有二次确认;取消不删除。",
......
...@@ -187,7 +187,7 @@ PAGES = [ ...@@ -187,7 +187,7 @@ PAGES = [
"断言账号、姓名与该行 `${sampleLoginName}`、`${sampleUserName}` 一致(允许规范化空白)", "断言账号、姓名与该行 `${sampleLoginName}`、`${sampleUserName}` 一致(允许规范化空白)",
], ],
"编辑弹窗可见;回填与列表行一致。", "编辑弹窗可见;回填与列表行一致。",
pre="列表至少 1 条", auto="USER-USR-009", pre="列表至少 1 条", auto="USER-USR-007",
), ),
case( case(
12, "编辑用户", "功能验证点", "P1", "编辑取消不保存", 58, 12, "编辑用户", "功能验证点", "P1", "编辑取消不保存", 58,
...@@ -197,7 +197,7 @@ PAGES = [ ...@@ -197,7 +197,7 @@ PAGES = [
"再次打开编辑,断言姓名仍为原值", "再次打开编辑,断言姓名仍为原值",
], ],
"取消后列表与回看均为修改前姓名。", "取消后列表与回看均为修改前姓名。",
pre="列表至少 1 条", auto="USER-USR-010", pre="列表至少 1 条", auto="USER-USR-008",
), ),
case( case(
13, "编辑用户", "边界验证点", "P1", "编辑时账号只读", 61, 13, "编辑用户", "边界验证点", "P1", "编辑时账号只读", 61,
...@@ -206,7 +206,7 @@ PAGES = [ ...@@ -206,7 +206,7 @@ PAGES = [
"断言登录账号不可改(disabled/readonly 或无法输入)", "断言登录账号不可改(disabled/readonly 或无法输入)",
], ],
"编辑态登录账号只读。", "编辑态登录账号只读。",
pre="列表至少 1 条", auto="USER-USR-011", pre="列表至少 1 条", auto="USER-USR-007",
), ),
case( case(
14, "编辑用户", "功能验证点", "P1", "编辑弹窗可见签名图片", 56, 14, "编辑用户", "功能验证点", "P1", "编辑弹窗可见签名图片", 56,
...@@ -216,7 +216,7 @@ PAGES = [ ...@@ -216,7 +216,7 @@ PAGES = [
"点「取 消」,不保存", "点「取 消」,不保存",
], ],
"签名图片控件可见;本条不上传、不保存。", "签名图片控件可见;本条不上传、不保存。",
pre="列表至少 1 条", auto="USER-USR-012", pre="列表至少 1 条", auto="USER-USR-008",
), ),
case( case(
15, "授权", "功能验证点", "P0", "打开授权后取消不生效", 68, 15, "授权", "功能验证点", "P0", "打开授权后取消不生效", 68,
...@@ -227,7 +227,7 @@ PAGES = [ ...@@ -227,7 +227,7 @@ PAGES = [
"再次打开同一用户授权,断言勾选与操作前一致", "再次打开同一用户授权,断言勾选与操作前一致",
], ],
"授权层可打开;取消后勾选不落库。", "授权层可打开;取消后勾选不落库。",
pre="列表至少 1 条", auto="USER-USR-013", pre="列表至少 1 条", auto="USER-USR-009",
), ),
case( case(
16, "列表与分页", "功能验证点", "P1", "切换20条每页页面正常且列表同步", 32, 16, "列表与分页", "功能验证点", "P1", "切换20条每页页面正常且列表同步", 32,
...@@ -238,7 +238,7 @@ PAGES = [ ...@@ -238,7 +238,7 @@ PAGES = [
], ],
"切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。", "切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。",
pre="默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip", pre="默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip",
auto="USER-USR-014", auto="USER-USR-010",
), ),
case( case(
17, "列表与分页", "功能验证点", "P2", "打开列设置面板", 83, 17, "列表与分页", "功能验证点", "P2", "打开列设置面板", 83,
...@@ -269,7 +269,7 @@ PAGES = [ ...@@ -269,7 +269,7 @@ PAGES = [
"关闭弹框", "关闭弹框",
], ],
"用户角色授权弹框打开;弹框账号与所选列表行一致。", "用户角色授权弹框打开;弹框账号与所选列表行一致。",
pre="列表至少 1 条且登录账号非空", auto="USER-USR-015", pre="列表至少 1 条且登录账号非空", auto="USER-USR-011",
), ),
case( case(
20, "授权", "功能验证点", "P1", "授权弹框添加移除角色行", 82, 20, "授权", "功能验证点", "P1", "授权弹框添加移除角色行", 82,
......
...@@ -198,7 +198,7 @@ def user_body(seq: Seq) -> str: ...@@ -198,7 +198,7 @@ def user_body(seq: Seq) -> str:
seq.add( seq.add(
"中", "中",
"每页条数切换", "每页条数切换",
"切换「20 条/页」「50 条/页」等后当前页行数为 min(N,所选值),总数不变(用户管理自动化 USER-USR-014 覆盖 20 条/页)", "切换「20 条/页」「50 条/页」等后当前页行数为 min(N,所选值),总数不变(用户管理自动化 USER-USR-010 覆盖 20 条/页)",
), ),
seq.add("中", "跳至指定页", "分页「跳至」输入合法页码后跳到对应页"), seq.add("中", "跳至指定页", "分页「跳至」输入合法页码后跳到对应页"),
], ],
...@@ -703,7 +703,7 @@ def quality(p: dict, count: int, tp_count: int) -> str: ...@@ -703,7 +703,7 @@ def quality(p: dict, count: int, tp_count: int) -> str:
"② 跨院登录账号按拒绝且不覆盖写点,若产品允许共用需改判定 " "② 跨院登录账号按拒绝且不覆盖写点,若产品允许共用需改判定 "
"③ 所属医院默认值与顶栏约定以现网为准 " "③ 所属医院默认值与顶栏约定以现网为准 "
"④ 签名/启用禁用/任职明细/改密后登录同用户管理对应 TP,本页只保留医院维度 " "④ 签名/启用禁用/任职明细/改密后登录同用户管理对应 TP,本页只保留医院维度 "
"⑤ UI-SPEC 测试关注点写了「状态」筛选,现网扫描仅在编辑弹窗有启用/禁用、无高级筛选「状态」;按扫描写点,自动化 `USER-UNI-013` 改为按科室过滤(TP_USER_UNI_016)" "⑤ UI-SPEC 测试关注点写了「状态」筛选,现网扫描仅在编辑弹窗有启用/禁用、无高级筛选「状态」;按扫描写点,自动化 `USER-UNI-011` 改为按科室过滤(TP_USER_UNI_016)"
) )
downstream = ( downstream = (
"可转结构化 MD/CSV(`npm run generate:md-user`);扫描 `npm run discover:scan-user`;" "可转结构化 MD/CSV(`npm run generate:md-user`);扫描 `npm run discover:scan-user`;"
......
...@@ -45,6 +45,295 @@ def case( ...@@ -45,6 +45,295 @@ def case(
} }
def vendor_qual_sync_tab_cases() -> list[dict]:
"""VENDOR-QUL-010~057:与 vendor-qual-tab-cases.ts / vendor-qual-sync-tabs.spec.ts 一致。"""
# (主 Tab, 组合第二字段, 起始 auto 序号)
tabs: list[tuple[str, str, int]] = [
("授权链路效期预警", "(授权三字段)", 10),
("供应商三证", "(仅销售商)", 16),
("器械注册人", "(五项组合含展开区)", 20),
("器械注册证号", "(八项组合含展开区)", 24),
("授权书", "(六项组合含展开区)", 28),
("承诺书", "(承诺类型+状态)", 32),
("法人委托书", "(业务员+状态)", 36),
("二级供应商三证", "(三项组合,无证件类型)", 40),
]
# 各主 Tab 列表区子页签(与 data/vendor-qual-subtabs.data.ts 一致)
list_subtab_summary: dict[str, str] = {
"授权链路效期预警": "三个月内过期/一个月内过期/已过期",
"供应商三证": "全部/未过期/已过期/已缺失",
"器械注册人": "全部/已维护/未维护",
"器械注册证号": "全部/未过期/已过期",
"授权书": "全部/未过期/已过期",
"承诺书": "全部/未过期/已过期",
"法人委托书": "全部/未过期/已过期",
"二级供应商三证": "全部/未过期/已过期",
}
def list_subtab_switch_title(tab: str) -> str:
labels = list_subtab_summary.get(tab, "")
if tab in ("授权链路效期预警",):
return "效期子 Tab 三个月内/一个月内/已过期可切换"
return f"列表子 Tab {labels}可切换"
def list_subtab_count_title(tab: str) -> str:
if tab == "授权链路效期预警":
return "效期子 Tab 计数与列表总数一致"
return "列表子 Tab 计数与列表总数一致"
# 列表子 Tab 用例编号(授权链路与 010 块连续;其余主 Tab 为 044~057)
expiry_case_ids: dict[str, tuple[int, int]] = {
"授权链路效期预警": (14, 15),
"供应商三证": (44, 45),
"器械注册人": (46, 47),
"器械注册证号": (48, 49),
"授权书": (50, 51),
"承诺书": (52, 53),
"法人委托书": (54, 55),
"二级供应商三证": (56, 57),
}
out: list[dict] = []
for tab, second, start in tabs:
h3 = tab
pre_tab = f"{tab} Tab"
if tab == "授权链路效期预警":
combo_title = "首行选中授权书名称、授权单位、被授权单位组合查询"
combo_steps = [
"切换至「授权链路效期预警」Tab 并点击「查 询」",
"点选列表首行",
"取首行「授权书名称」「授权单位」「被授权单位」写入筛选并「查 询」",
"断言结果行与三项条件一致",
]
combo_pre = f"{pre_tab};首行三项非空"
elif tab == "供应商三证":
combo_title = "首行选中销售商(自定义)名称查询"
combo_steps = [
"切换 Tab 并查询",
"点选首行",
"取销售商(自定义)名称写入筛选并查询",
"断言结果与销售商条件一致",
]
combo_pre = f"{pre_tab};首行销售商(自定义)非空"
elif tab == "二级供应商三证":
combo_title = "首行选中销售商(自定义)名称、供应商名称、数据来源类型组合查询"
combo_steps = [
"切换 Tab 并查询",
"点选首行",
"取销售商(自定义)名称、供应商名称、数据来源类型组合查询",
"断言结果与三项条件一致",
]
combo_pre = f"{pre_tab};首行三项非空"
elif tab == "器械注册人":
combo_title = "首行五项组合查询(器械注册人、状态、全国总代理及展开区类别、销售商(自定义)名称)"
combo_steps = [
"切换 Tab 并查询",
"点选首行",
"填写器械注册人、状态、全国总代理",
"点击展开并填写类别、销售商(自定义)名称",
"断言结果与五项条件一致",
]
combo_pre = f"{pre_tab};首行五项样本非空"
elif tab == "器械注册证号":
combo_title = (
"首行八项组合查询(器械注册人、销售商(自定义)名称、状态及展开区注册证号等五项)"
)
combo_steps = [
"切换 Tab 并查询",
"点选首行",
"填写器械注册人、销售商(自定义)名称、状态",
"点击展开并填写注册证号、是否有附件、注册产品名称、是否新证、产品名称",
"断言结果与八项条件一致",
]
combo_pre = f"{pre_tab};首行八项样本非空"
elif tab == "授权书":
combo_title = (
"首行选中器械注册人、销售商(自定义)名称、状态及展开区授权书名称、注册证号、被授权单位组合查询"
)
combo_steps = [
"切换至「授权书」Tab 并点击「查 询」",
"点选列表首行",
"取首行「器械注册人」「销售商(自定义)名称」「状态」写入筛选",
"点击「展 开」,取首行「授权书名称」「注册证号」「被授权单位」写入筛选并「查 询」",
"断言结果行与六项条件一致",
]
combo_pre = f"{pre_tab};首行六项样本非空"
elif tab == "承诺书":
combo_title = "首行选中销售商(自定义)名称、承诺类型、状态组合查询"
combo_steps = [
"切换 Tab 并查询",
"点选首行",
"取销售商(自定义)名称、承诺类型、状态组合查询",
"断言结果与三项条件一致",
]
combo_pre = f"{pre_tab};首行承诺类型、状态非空"
elif tab == "法人委托书":
combo_title = "首行选中销售商(自定义)名称、业务员、状态组合查询"
combo_steps = [
"切换 Tab 并查询",
"点选首行",
"取销售商(自定义)名称、业务员、状态组合查询",
"断言结果与三项条件一致",
]
combo_pre = f"{pre_tab};首行业务员、状态非空"
else:
combo_title = f"首行选中销售商(自定义)名称与{second}组合查询"
combo_steps = ["切换 Tab 并查询", "点选首行", f"取销售商与「{second}」组合查询", "断言结果与条件一致"]
combo_pre = f"{pre_tab};列表至少 1 条"
out.append(
case(
start,
h3,
"功能验证点",
"P0",
combo_title,
11,
combo_steps,
"组合查询命中;分页与结果一致。",
pre=combo_pre,
auto=f"VENDOR-QUL-{start:03d}",
)
)
out.append(
case(
start + 1,
h3,
"功能验证点",
"P1",
"填写销售商(自定义)名称后重置清空条件",
11,
["点选首行并填写销售商(自定义)名称", "点击重置", "断言输入框为空"],
"重置后销售商(自定义)名称条件被清空。",
pre=f"{pre_tab};列表至少 1 条",
auto=f"VENDOR-QUL-{start + 1:03d}",
)
)
out.append(
case(
start + 2,
h3,
"功能验证点",
"P0",
"查询后导出无报错",
15,
["查询并点选首行", "点击导出", "断言无业务错误"],
"导出入口可用且无报错。",
pre=f"{pre_tab};列表至少 1 条",
auto=f"VENDOR-QUL-{start + 2:03d}",
)
)
view_title = f"{tab}·首行查看详情销售商(自定义)与列表一致" if tab != "授权链路效期预警" else f"{tab}·首行查看详情销售商(自定义)与列表一致"
out.append(
case(
start + 3,
"查看",
"功能验证点",
"P0",
view_title,
14,
["点选首行并点击查看", "断言详情销售商(自定义)与列表一致", "关闭详情"],
"详情可打开;销售商(自定义)与列表一致。",
pre=f"{pre_tab};列表至少 1 行含查看",
auto=f"VENDOR-QUL-{start + 3:03d}",
)
)
if tab in expiry_case_ids:
exp_sw, exp_cnt = expiry_case_ids[tab]
switch_steps = (
["依次切换效期子 Tab(三个月内/一个月内/已过期或等价文案)", "断言列表有数据或空态"]
if tab == "授权链路效期预警"
else [
f"依次切换列表子 Tab({list_subtab_summary[tab]})",
"断言列表有数据或空态",
]
)
out.append(
case(
exp_sw,
h3,
"功能验证点",
"P0",
list_subtab_switch_title(tab),
10,
switch_steps,
"列表子 Tab 均可切换且列表正常。",
pre=pre_tab,
auto=f"VENDOR-QUL-{exp_sw:03d}",
)
)
out.append(
case(
exp_cnt,
h3,
"功能验证点",
"P1",
list_subtab_count_title(tab),
10,
["切换列表子 Tab", "Tab 计数等于分页共 N 条(可读时)"],
"Tab 计数与分页总数一致(可读时)。",
pre=f"{pre_tab};子 Tab 标签可读计数",
auto=f"VENDOR-QUL-{exp_cnt:03d}",
)
)
return out
def vendor_qual_column_settings_other_tab_cases() -> list[dict]:
"""VENDOR-QUL-058~073:各主 Tab 列设置(证件效期提醒 005/006 见 vendor-qual.spec.ts)。"""
tabs: list[tuple[str, int]] = [
("授权链路效期预警", 58),
("供应商三证", 60),
("器械注册人", 62),
("器械注册证号", 64),
("授权书", 66),
("承诺书", 68),
("法人委托书", 70),
("二级供应商三证", 72),
]
out: list[dict] = []
for tab, start in tabs:
pre_tab = f"{tab} Tab"
out.append(
case(
start,
f"{tab} · 列设置",
"功能验证点",
"P2",
"打开列设置面板",
12,
[
f"切换至「{tab}」Tab 并点击「查 询」",
"点击列表工具栏列设置(setting 齿轮)",
"断言弹出列设置面板,含字段设置/列展示与「重置」",
"关闭面板",
],
"列设置面板可正常打开与关闭。",
pre=f"{pre_tab};列表表头已加载",
auto=f"VENDOR-QUL-{start:03d}",
)
)
out.append(
case(
start + 1,
f"{tab} · 列设置",
"功能验证点",
"P2",
"列设置取消勾选后再勾选列表列恢复",
12,
[
f"切换至「{tab}」Tab 并点击「查 询」",
"打开列设置,取消勾选一列非核心字段并关闭",
"断言表头该列隐藏",
"再次打开列设置,勾选恢复并关闭",
"断言表头该列恢复显示",
],
"列隐藏与恢复与勾选状态一致。",
pre=f"{pre_tab};存在可隐藏的非核心列",
auto=f"VENDOR-QUL-{start + 1:03d}",
)
)
return out
PAGES = [ PAGES = [
page( page(
code="VENDOR-QRY", code="VENDOR-QRY",
...@@ -59,49 +348,43 @@ PAGES = [ ...@@ -59,49 +348,43 @@ PAGES = [
write_note="本页只读查询,无新增/编辑提交。", write_note="本页只读查询,无新增/编辑提交。",
samples="从器械注册人 Tab 首行读取 `${sampleMfrsName}`;禁止写死厂商名称;mock:`__NOT_EXIST_MFRS__99999`。", samples="从器械注册人 Tab 首行读取 `${sampleMfrsName}`;禁止写死厂商名称;mock:`__NOT_EXIST_MFRS__99999`。",
cases=[ cases=[
case(1, "页面进入", "功能验证点", "P0", "菜单进入厂商查询页", 1, case(1, "Tab 与搜索", "功能验证点", "P0", "切换经销商 Tab", 10,
["按菜单「基础设置 → 厂商管理 → 厂商查询」进入",
"断言 URL 含 `/spd/basic/mfrs/paltform/mfrsName/factory`",
"断言可见「器械注册人」「经销商」Tab、厂商名称搜索框及列表表头"],
"进入厂商查询页;不误入供应商资质管理。",
pre="已登录且具备厂商管理权限", auto="VENDOR-QRY-001"),
case(2, "Tab 与搜索", "功能验证点", "P0", "默认器械注册人列表有数据或空态明确", 9,
["切换或停留「器械注册人」", "断言出现数据行,或「暂无数据」且分页总数为 0"],
"器械注册人列表稳定;有数据时分页「共 N 条」N≥1。", auto="VENDOR-QRY-002"),
case(3, "Tab 与搜索", "功能验证点", "P0", "切换经销商 Tab", 10,
["点击「经销商」", "断言 Tab 激活,列表有数据或空态明确"], ["点击「经销商」", "断言 Tab 激活,列表有数据或空态明确"],
"经销商列表加载成功;总数可与器械注册人不同。", auto="VENDOR-QRY-003"), "经销商列表加载成功;总数可与器械注册人不同。", auto="VENDOR-QRY-001"),
case(4, "Tab 与搜索", "功能验证点", "P0", "按厂商名称搜索", 11, case(2, "Tab 与搜索", "功能验证点", "P0", "按厂商名称搜索", 11,
["从首行读取 `${sampleMfrsName}` 取关键字", "在厂商名称搜索框填入关键字并回车", ["从首行读取 `${sampleMfrsName}` 取关键字", "在厂商名称搜索框填入关键字并回车",
"断言结果行「厂商名称」包含关键字"], "断言结果行「厂商名称」包含关键字"],
"厂商名称列与关键字匹配。", "厂商名称列与关键字匹配。",
pre="器械注册人至少 1 条且厂商名称非空", auto="VENDOR-QRY-004"), pre="器械注册人至少 1 条且厂商名称非空", auto="VENDOR-QRY-002"),
case(5, "Tab 与搜索", "功能验证点", "P1", "清空搜索恢复", 19, case(3, "Tab 与搜索", "功能验证点", "P1", "清空搜索恢复", 19,
["在已过滤状态下清空搜索框并回车", "断言分页总数不少于过滤后"], ["在已过滤状态下清空搜索框并回车", "断言分页总数不少于过滤后"],
"恢复更宽结果集。", pre="已按厂商名称过滤", auto="VENDOR-QRY-005"), "恢复更宽结果集。", pre="已按厂商名称过滤", auto="VENDOR-QRY-003"),
case(6, "Tab 与搜索", "异常验证点", "P1", "无匹配空态(mock反例)", 17, case(4, "Tab 与搜索", "异常验证点", "P1", "无匹配空态(mock反例)", 17,
["厂商名称填入 `__NOT_EXIST_MFRS__99999` 并回车", "断言空态且分页总数为 0"], ["厂商名称填入 `__NOT_EXIST_MFRS__99999` 并回车", "断言空态且分页总数为 0"],
"无匹配时不报错;总数为 0。", auto="VENDOR-QRY-006"), "无匹配时不报错;总数为 0。", auto="VENDOR-QRY-004"),
case(7, "Tab 与搜索", "功能验证点", "P0", "列表核心列完整", 12, case(5, "Tab 与搜索", "功能验证点", "P1", "分页翻至第 2 页", 13,
["断言表头含序号、厂商编号、厂商名称、产地、详细地址、简称、开户银行、银行账号"],
"核心列可见。", auto="VENDOR-QRY-007"),
case(8, "Tab 与搜索", "功能验证点", "P1", "分页翻至第 2 页", 13,
["总数足够时翻到第 2 页", "断言列表可加载;不足两页则 skip"], ["总数足够时翻到第 2 页", "断言列表可加载;不足两页则 skip"],
"第 2 页可加载。", pre="器械注册人总数足够翻页", auto="VENDOR-QRY-008"), "第 2 页可加载。", pre="器械注册人总数足够翻页", auto="VENDOR-QRY-005"),
case(9, "Tab 与搜索", "边界验证点", "P2", "仅空格关键字搜索", 15, case(6, "Tab 与搜索", "功能验证点", "P0", "经销商选首行验证厂商名称查询条件", 21,
["厂商名称仅填空格并回车", "断言按空查询处理或提示,不白屏"], ["切换至「经销商」Tab", "点选列表首行并断言行选中",
"不报错;行为明确。"), "取首行厂商名称关键字写入「厂商名称」搜索并回车",
case(10, "页面进入", "异常验证点", "P0", "未登录访问拦截", 5, "断言列表「厂商名称」列命中且搜索框已写入关键字"],
["新建独立 BrowserContext,不加载 auth/user.json(test.use 空 storageState)", "首行选中后按厂商名称条件查询结果正确。",
"直接打开厂商查询 URL(/spd/basic/mfrs/paltform/mfrsName/factory)", pre="经销商至少 1 条且厂商名称非空", auto="VENDOR-QRY-006"),
"断言跳转登录或不展示业务列表"], case(7, "Tab 与搜索", "功能验证点", "P1", "切换20条每页页面正常且列表同步", 20,
"未登录不可见厂商数据。", ["停留或切换「器械注册人」Tab", "分页区打开每页条数下拉,选择「20 条/页」",
pre="使用独立 context,storageState 为空(不加载 test_1 登录态)", "断言分页器显示 20 条/页、当前页行数为 min(总条数,20)、「共 N 条」总条数不变"],
auto="VENDOR-QRY-010"), "每页条数切换后列表与分页展示同步更新。",
case(11, "Tab 与搜索", "集成验证点", "P1", "厂商名称与资质管理进院供应商可对照", 18, pre="列表有数据且总条数大于 20(页面上可选 20/50/100/200 条/页)", auto="VENDOR-QRY-007"),
["记录厂商查询中某厂商名称(若有)", "打开供应商资质管理,用同源关键字检索或目视对照", case(8, "列设置", "功能验证点", "P2", "打开列设置面板", 22,
"断言不出现明显主数据串名(允许一方无记录)"], ["点击列表工具栏列设置(setting 齿轮,非 column-height 密度)",
"厂商主数据跨菜单可对照,不串名。"), "断言弹出列设置面板,含字段设置/列展示与「重置」", "关闭面板"],
"列设置面板可正常打开与关闭。", pre="列表表头已加载", auto="VENDOR-QRY-008"),
case(9, "列设置", "功能验证点", "P2", "列设置取消勾选后再勾选列表列恢复", 23,
["打开列设置,取消勾选一列非核心字段并关闭", "断言表头该列隐藏",
"再次打开列设置,勾选恢复并关闭", "断言表头该列恢复显示"],
"列隐藏与恢复与勾选状态一致。",
pre="存在可隐藏的非核心列(如简称)", auto="VENDOR-QRY-009"),
], ],
), ),
page( page(
...@@ -112,57 +395,47 @@ PAGES = [ ...@@ -112,57 +395,47 @@ PAGES = [
scan="vendor-qual", scan="vendor-qual",
count=99, count=99,
spec="UI-SPEC-SPD-VENDOR-002", spec="UI-SPEC-SPD-VENDOR-002",
spec_file="vendor-qual.spec.ts", spec_file="vendor-qual.spec.ts; vendor-qual-sync-tabs.spec.ts; vendor-qual-column-settings.spec.ts",
iframe=False, iframe=False,
write_note="本批只读:查看打开后关闭;不确认导出下载、不改证件。", write_note="本批只读:导出仅校验无报错;不改证件。",
samples="证件效期提醒 Tab 首行读取 `${sampleHospitalSupplier}`;mock:`__NOT_EXIST_SUPPLIER__99999`。", samples="各主 Tab 组合查询见 data/vendor-qual-tab-cases.ts;子 Tab 见 vendor-qual-subtabs.data.ts;列设置 9 Tab 全覆盖(005/006 + 058~073)。",
cases=[ cases=[
case(1, "页面进入", "功能验证点", "P0", "菜单进入供应商资质管理页", 1, case(1, "页面进入", "功能验证点", "P0", "进入页面列表 Tab 默认证件效期提醒", 1,
["按菜单「基础设置 → 厂商管理 → 供应商资质管理」进入", ["按菜单「基础设置 → 厂商管理 → 供应商资质管理」进入",
"断言 URL 含 `/spd/basic/mfrs/basic/qcManage`", "断言 URL 含 `/spd/basic/mfrs/basic/qcManage`",
"断言可见「查 询」「重 置」及证件效期提醒相关入口"], "断言列表 Tab 默认选中「证件效期提醒」"],
"进入供应商资质管理;不误入厂商查询。", "进入供应商资质管理;列表 Tab 默认证件效期提醒。",
pre="已登录且具备厂商管理权限", auto="VENDOR-QUL-001"), pre="已登录且具备厂商管理权限", auto="VENDOR-QUL-001"),
case(2, "筛选与 Tab", "功能验证点", "P0", "默认查询列表有数据或空态明确", 9, case(2, "证件效期提醒", "功能验证点", "P0", "首行选中销售商(自定义)名称与证件类型组合查询", 11,
["点击「查 询」并等待列表稳定", "断言出现数据行或空态明确"], ["查询后点选首行", "取销售商与证件类型组合查询", "断言结果列与条件一致"],
"查询无报错;有数据时分页 N≥1。", auto="VENDOR-QUL-002"), "组合查询命中。", auto="VENDOR-QUL-002"),
case(3, "筛选与 Tab", "功能验证点", "P0", "切换授权链路效期预警 Tab", 9, case(3, "证件效期提醒", "功能验证点", "P1", "填写销售商(自定义)名称后重置清空条件", 11,
["点击「授权链路效期预警」", "断言列表有数据或空态明确"], ["点选首行并填写销售商(自定义)名称", "点击重置", "断言输入框为空"],
"Tab 切换成功,列表字段可随 Tab 变化。", auto="VENDOR-QUL-003"), "重置后条件清空。", auto="VENDOR-QUL-003"),
case(4, "筛选与 Tab", "功能验证点", "P0", "近三个月到期计数一致", 10, case(4, "证件效期提醒", "功能验证点", "P0", "查询后导出无报错", 15,
["切换「近三个月到期」(或等价文案)", ["查询并点选首行", "点击导出", "断言无业务错误"],
"若可读到括号/冒号计数,断言等于分页「共 N 条」"], "导出无报错。", auto="VENDOR-QUL-004"),
"计数与分页一致(可读时)。", auto="VENDOR-QUL-004"), case(5, "证件效期提醒 · 列设置", "功能验证点", "P2", "打开列设置面板", 12,
case(5, "筛选与 Tab", "功能验证点", "P0", "按进院供应商筛选", 11, ["切换至「证件效期提醒」Tab 并点击「查 询」", "点击列表工具栏列设置(setting 齿轮)",
["从首行读取 `${sampleHospitalSupplier}` 取关键字", "断言弹出列设置面板,含字段设置/列展示与「重置」", "关闭面板"],
"在进院供应商名称填入关键字并「查 询」", "列设置面板可正常打开与关闭。",
"断言结果行「进院供应商」包含关键字"], pre="证件效期提醒 Tab;列表表头已加载", auto="VENDOR-QUL-005"),
"进院供应商列与条件一致。", case(6, "证件效期提醒 · 列设置", "功能验证点", "P2", "列设置取消勾选后再勾选列表列恢复", 12,
pre="证件效期提醒至少 1 条且进院供应商非空", auto="VENDOR-QUL-005"), ["切换至「证件效期提醒」Tab 并点击「查 询」", "打开列设置,取消勾选一列非核心字段并关闭",
case(6, "筛选与 Tab", "功能验证点", "P1", "重置恢复默认", 11, "断言表头该列隐藏", "再次打开列设置,勾选恢复并关闭", "断言表头该列恢复显示"],
["填写筛选条件后点击「重 置」", "断言列表按默认规则刷新"], "列隐藏与恢复与勾选状态一致。",
"筛选恢复默认。", auto="VENDOR-QUL-006"), pre="证件效期提醒 Tab;存在可隐藏的非核心列", auto="VENDOR-QUL-006"),
case(7, "筛选与 Tab", "异常验证点", "P1", "无匹配空态(mock反例)", 16, case(7, "证件效期提醒", "功能验证点", "P0", "效期子 Tab 三个月内/一个月内/已过期可切换", 10,
["进院供应商名称填入 `__NOT_EXIST_SUPPLIER__99999`", "点击「查 询」", ["依次切换三个效期子 Tab", "断言列表有数据或空态"],
"断言空态且分页总数为 0"], "子 Tab 可切换。", auto="VENDOR-QUL-007"),
"无匹配时不报错;总数为 0。", auto="VENDOR-QUL-007"), case(8, "证件效期提醒", "功能验证点", "P1", "效期子 Tab 计数与列表总数一致", 10,
case(8, "筛选与 Tab", "功能验证点", "P0", "证件效期提醒核心列完整", 12, ["切换子 Tab", "Tab 计数等于分页共 N 条"],
["处于证件效期提醒 Tab", "计数与列表一致。", auto="VENDOR-QUL-008"),
"断言表头含序号、数据来源、进院供应商、有效期至、证件名称、证件类型、效期提醒、操作"], case(9, "查看", "功能验证点", "P0", "首行查看详情销售商(自定义)与列表一致", 14,
"核心列可见。", auto="VENDOR-QUL-008"), ["点选首行", "点击查看", "断言详情销售商(自定义)与列表一致", "关闭详情"],
case(9, "查看与导出", "功能验证点", "P0", "打开查看后关闭", 14, "详情可打开;销售商一致。", auto="VENDOR-QUL-009"),
["点击首行「查 看」", "断言详情层可见", "关闭/返回,断言回到列表"], *vendor_qual_sync_tab_cases(),
"详情可打开;关闭后回到列表。", *vendor_qual_column_settings_other_tab_cases(),
pre="列表至少 1 行含「查看」", auto="VENDOR-QUL-009"),
case(10, "查看与导出", "功能验证点", "P1", "导出按钮可见", 15,
["断言「导出」或「导出明细」按钮可见(本批不确认下载)"],
"导出入口可见。", auto="VENDOR-QUL-008"),
case(11, "筛选与 Tab", "边界验证点", "P2", "制单/效期日期起止同一天", 15,
["若存在日期区间筛选,起止选同一天并查询", "断言仅返回该日数据或空态明确,不报错"],
"同日区间行为明确。"),
case(12, "筛选与 Tab", "集成验证点", "P1", "效期提醒与有效期至抽样一致", 18,
["在近三个月到期列表抽样一行", "核对「效期提醒」文案与「有效期至」日期逻辑一致"],
"提醒文案与有效期至不矛盾。"),
], ],
), ),
] ]
...@@ -233,7 +506,50 @@ def audit_page(code: str, prefix: str, name: str, path: str, scan: str, count: i ...@@ -233,7 +506,50 @@ def audit_page(code: str, prefix: str, name: str, path: str, scan: str, count: i
PAGES.extend( PAGES.extend(
[ [
audit_page("VENDOR-AF1", "AF1", "授权变更初审", "/spd/basic/mfrs/paltform/mfrsName/authAudit", "vendor-auth-first", 40, "UI-SPEC-SPD-VENDOR-003", "auth"), page(
code="VENDOR-AF1",
prefix="AF1",
name="授权变更初审",
path="/spd/basic/mfrs/paltform/mfrsName/authAudit",
scan="vendor-auth-first",
count=40,
spec="UI-SPEC-SPD-VENDOR-003",
spec_file="vendor-audit.spec.ts",
iframe=False,
write_note="AF1-001~006;状态默认通过。",
samples="mock:`__NOT_EXIST_ORDER__99999`。",
cases=[
case(1, "筛选与列表", "功能验证点", "P1", "重置恢复默认", 13,
["确认「状态」筛选默认值为「通过」", "填写其它筛选项后点击「重 置」",
"断言文本条件清空、「状态」恢复为「通过」、列表按默认规则刷新"],
"重置成功;状态仍为「通过」。",
pre="已进入授权变更初审页;状态筛选默认值为「通过」", auto="VENDOR-AF1-001"),
case(2, "筛选与列表", "异常验证点", "P1", "无匹配空态(mock反例)", 17,
["确认「状态」筛选默认值为「通过」", "单号填入 `__NOT_EXIST_ORDER__99999` 并查询",
"断言空态且分页总数为 0"],
"无匹配时不报错;总数为 0。",
pre="已进入授权变更初审页;状态筛选默认值为「通过」", auto="VENDOR-AF1-002"),
case(3, "列设置", "功能验证点", "P0", "列设置取消勾选后再勾选列表列恢复", 20,
["打开列设置,取消勾选一列非核心列后确认表头隐藏", "再次勾选该列后确认表头恢复"],
"列设置 toggle 后列表列展示与勾选状态一致。",
pre="已进入授权变更初审页;状态筛选默认值为「通过」;列表已查询出数据", auto="VENDOR-AF1-003"),
case(4, "筛选与列表", "功能验证点", "P1", "筛选区展开与收起正常使用", 19,
["点击展开/收起切换筛选区", "断言收起区字段隐藏、展开后可见,且收起后再展开保留已填单号"],
"筛选区展开收起正常且条件不丢失。",
pre="已进入授权变更初审页;状态筛选默认值为「通过」;页面具备展开/收起按钮",
auto="VENDOR-AF1-004"),
case(5, "详情", "功能验证点", "P0", "点击详情进入详情页后返回", 15,
["点击行内「详 情」", "断言详情层可见且含单号片段", "返回/关闭且不提交审批"],
"详情只读可返回;列表状态不变。",
pre="已进入授权变更初审页;状态默认「通过」;存在含「详情」的行", auto="VENDOR-AF1-005"),
case(6, "组合查询", "功能验证点", "P0", "首行九项组合查询", 21,
["点选首行",
"用首行制单日期、单号、筛选项「销售商(自定义)」(列「销售商(自定义)名称」)、预转供应商、状态、变更类型、原供应商、医院ID、医院名称填入筛选并查询",
"断言结果行单号与首行一致"],
"组合查询结果与首行关键字段匹配。",
pre="已进入授权变更初审页;状态默认「通过」;列表至少有 1 条数据", auto="VENDOR-AF1-006"),
],
),
audit_page("VENDOR-AF2", "AF2", "授权变更终审", "/spd/basic/mfrs/paltform/mfrsName/authAuditFinal", "vendor-auth-final", 40, "UI-SPEC-SPD-VENDOR-004", "auth"), audit_page("VENDOR-AF2", "AF2", "授权变更终审", "/spd/basic/mfrs/paltform/mfrsName/authAuditFinal", "vendor-auth-final", 40, "UI-SPEC-SPD-VENDOR-004", "auth"),
audit_page("VENDOR-CF1", "CF1", "注册证变更初审", "/spd/basic/mfrs/paltform/mfrsName/regCertAudit", "vendor-cert-first", 40, "UI-SPEC-SPD-VENDOR-005", "cert"), audit_page("VENDOR-CF1", "CF1", "注册证变更初审", "/spd/basic/mfrs/paltform/mfrsName/regCertAudit", "vendor-cert-first", 40, "UI-SPEC-SPD-VENDOR-005", "cert"),
audit_page("VENDOR-CF2", "CF2", "注册证变更终审", "/spd/basic/mfrs/paltform/mfrsName/regCertAuditFinal", "vendor-cert-final", 40, "UI-SPEC-SPD-VENDOR-006", "cert"), audit_page("VENDOR-CF2", "CF2", "注册证变更终审", "/spd/basic/mfrs/paltform/mfrsName/regCertAuditFinal", "vendor-cert-final", 40, "UI-SPEC-SPD-VENDOR-006", "cert"),
...@@ -307,41 +623,85 @@ PAGES.append( ...@@ -307,41 +623,85 @@ PAGES.append(
spec="UI-SPEC-SPD-VENDOR-008", spec="UI-SPEC-SPD-VENDOR-008",
spec_file="vendor-eval-scheme.spec.ts", spec_file="vendor-eval-scheme.spec.ts",
iframe=False, iframe=False,
write_note="新增只打开后取消,不提交保存。", write_note="EVS-001 组合;014 筛选展开/收起;002/003 列设置;004~013 排序;创建日期默认 2026-04-27~2026-10-27。",
samples="有数据时从首行读取 `${sampleSchemeCode}`;mock:`__NOT_EXIST_ORDER__99999`。", samples="排序列见 `data/vendor-eval-scheme.data.ts`;列设置 toggle 见 evalSchemeColumnSettingsToggleCandidates。",
cases=[ cases=[
case(1, "页面进入", "功能验证点", "P0", "菜单进入评价方案维护页", 1, case(1, "筛选与维护", "功能验证点", "P0", "默认创建日期区间内首行四字段组合查询", 10,
["按菜单进入评价方案维护", "断言 URL 含 `evaluationPlan`", "断言可见查询与新增"], ["按菜单进入评价方案维护并断言 URL 含 `evaluationPlan`",
"进入评价方案维护页。", "「创建日期」填入 2026-04-27 至 2026-10-27 并点击查询",
pre="已登录且具备厂商管理权限", auto="VENDOR-EVS-001"), "点选列表首行",
case(2, "筛选与维护", "功能验证点", "P0", "默认查询列表有数据或空态明确", 9, "取首行创建日期、启用日期、方案编码、方案名称写入对应筛选项并查询",
["点击查询", "断言列表有数据或「暂无数据」空态"], "断言结果行方案编码、方案名称与首行一致"],
"查询无报错。", auto="VENDOR-EVS-002"), "四字段组合查询生效且首行条件匹配。",
case(3, "筛选与维护", "功能验证点", "P0", "按方案编码筛选", 10, pre="创建日期默认为 2026-04-27 至 2026-10-27;该区间内列表至少 1 条且首行四字段非空",
["取样本方案编码筛选并查询", "断言方案编码列匹配"], auto="VENDOR-EVS-001"),
"方案编码匹配。", case(2, "列设置", "功能验证点", "P2", "打开列设置面板", 13,
pre="列表有数据", auto="VENDOR-EVS-003"), ["创建日期填入 2026-04-27 至 2026-10-27 并查询",
case(4, "筛选与维护", "功能验证点", "P1", "重置恢复默认", 11, "点击列表工具栏列设置(setting 齿轮,非 column-height 密度)",
["填写筛选后重置", "断言条件清空、列表刷新"], "断言弹出列设置面板,含字段设置/列展示与「重置」",
"重置成功。", auto="VENDOR-EVS-004"), "关闭面板"],
case(5, "筛选与维护", "异常验证点", "P1", "无匹配空态(mock反例)", 15, "列设置面板可正常打开与关闭。",
["方案编码填 mock 后查询", "断言空态总数 0"], pre="创建日期默认为 2026-04-27 至 2026-10-27;列表表头已加载",
"无匹配不报错。", auto="VENDOR-EVS-005"), auto="VENDOR-EVS-002"),
case(6, "筛选与维护", "功能验证点", "P0", "列表核心列完整", 12, case(3, "列设置", "功能验证点", "P2", "列设置取消勾选后再勾选列表列恢复", 13,
["断言表头含方案编码、方案名称、创建人、创建时间、状态、操作"], ["创建日期填入 2026-04-27 至 2026-10-27 并查询",
"核心列可见。", auto="VENDOR-EVS-006"), "打开列设置,取消勾选一列非核心字段并关闭",
case(7, "新增", "功能验证点", "P0", "打开新增后取消", 13, "断言表头该列隐藏",
["记录总数", "点击新增打开编辑层", "取消/返回不提交", "断言总数不变"], "再次打开列设置,勾选恢复并关闭",
"取消不落库。", auto="VENDOR-EVS-007"), "断言表头该列重新可见"],
case(8, "新增", "异常验证点", "P0", "必填为空提交拦截", 16, "列隐藏与恢复与勾选状态一致。",
["打开新增不填必填项尝试提交(若可点)", "断言拦截提示,页面不关闭或总数不变"], pre="创建日期默认为 2026-04-27 至 2026-10-27;存在可隐藏的非核心列(如备注)",
"空提交被拦截。"), auto="VENDOR-EVS-003"),
case(9, "筛选与维护", "边界验证点", "P2", "方案名称超长输入", 15, case(
["打开新增,方案名称输入超长字符串(如 200+ 字符)", "断言前端截断/校验提示或后端拒绝,不白屏"], 14,
"超长输入被约束且页面可用。"), "筛选与维护",
case(10, "筛选与维护", "集成验证点", "P1", "方案与评价指标可关联", 18, "功能验证点",
["在方案配置中查看指标选择(只读打开)", "断言可选指标来源于评价指标维护启用项或规则允许集合"], "P1",
"方案与指标主数据衔接正确。"), "筛选区展开与收起正常使用",
11,
[
"点击「展开」,断言启用日期/方案编码/方案名称等扩展条件可见",
"点击「收起」,断言扩展条件隐藏且「展开」按钮可见",
"再次点击「展开」,断言扩展条件恢复可见",
"填写方案编码后收起再展开,断言条件值保留",
],
"展开与收起切换正常;收起后再展开已填条件不丢失。",
pre="筛选区含展开/收起控件",
auto="VENDOR-EVS-014",
),
*[
case(
4 + idx,
"列表排序",
"功能验证点",
"P1",
f"{col}列升序降序切换",
13,
[
"创建日期填入 2026-04-27 至 2026-10-27 并查询",
f"横向滚动使「{col}」列表头可见",
"第一次点击该列排序控件并校验升序指示器",
"第二次点击并校验降序指示器",
],
"排序按钮可点击;升序与降序指示器分别激活;列表数据正常展示。",
pre="创建日期默认区间已查询;列表至少 2 条数据",
auto=f"VENDOR-EVS-{4 + idx:03d}",
)
for idx, col in enumerate(
[
"方案编码",
"方案名称",
"创建人",
"创建日期",
"启用人",
"启用时间",
"停用人",
"停用时间",
"状态",
"备注",
]
)
],
], ],
) )
) )
...@@ -357,39 +717,38 @@ PAGES.append( ...@@ -357,39 +717,38 @@ PAGES.append(
spec="UI-SPEC-SPD-VENDOR-009", spec="UI-SPEC-SPD-VENDOR-009",
spec_file="vendor-eval-indicator.spec.ts", spec_file="vendor-eval-indicator.spec.ts",
iframe=False, iframe=False,
write_note="详情只读返回;不落库新增/编辑提交。", write_note="001~003 分类树;004~007 指标一级分类右侧列表。",
samples="右表首行读取 `${sampleIndicatorCode}`;列名可能为指标编码或指标编号;mock:`__NOT_EXIST_ORDER__99999`。", samples="",
cases=[ cases=[
case(1, "页面进入", "功能验证点", "P0", "菜单进入评价指标维护页", 1, case(1, "树与列表", "功能验证点", "P0", "点击左侧分类可正常展开", 10,
["按菜单进入评价指标维护", "断言 URL 含 `indicators`", "断言可见「指标分类」与查询"], ["在左侧指标分类树中点击收起状态分类的展开图标", "断言该分类处于展开态且子级分类可见"],
"进入评价指标维护页。", "分类树节点可正常展开,子级分类展示正确。",
pre="已登录且具备厂商管理权限", auto="VENDOR-EVI-001"), pre="已进入评价指标维护页;左侧存在至少一个收起状态的分类节点", auto="VENDOR-EVI-001"),
case(2, "树与列表", "功能验证点", "P0", "默认查询列表有数据或空态明确", 9, case(2, "树与列表", "功能验证点", "P0", "切换左侧不同分类右侧内容同步变化", 11,
["点击查询", "断言右表有数据或空态"], ["依次点击左侧两个不同分类节点", "断言右侧指标列表或空态随分类切换而更新,且与上一分类展示不一致"],
"查询无报错。", auto="VENDOR-EVI-002"), "切换分类后右侧内容区与所选分类一致并同步刷新。",
case(3, "树与列表", "功能验证点", "P0", "点击左侧分类树", 10, pre="已进入评价指标维护页;左侧至少两个可选分类且存在切换后右侧展示不同的分类对",
["点击左侧首个分类节点", "断言右侧列表刷新"], auto="VENDOR-EVI-002"),
"分类切换成功。", auto="VENDOR-EVI-003"), case(3, "树与列表", "功能验证点", "P0", "选中左侧分类首行同步分类名称查询条件", 12,
case(4, "树与列表", "功能验证点", "P0", "按指标编码筛选", 11, ["点击左侧指标分类树首行分类节点", "读取并断言「分类名称」查询框取值与所选分类名称一致"],
["取样本指标编码/编号筛选并查询", "断言编码列匹配"], "分类名称查询条件与所选分类同步展示。",
"指标编码匹配。", pre="已进入评价指标维护页;左侧分类树存在首行可选节点", auto="VENDOR-EVI-003"),
pre="右表有数据", auto="VENDOR-EVI-004"), case(4, "指标一级分类右侧列表", "功能验证点", "P0", "指标编号指标名称状态列可排序", 13,
case(5, "树与列表", "异常验证点", "P1", "无匹配空态(mock反例)", 15, ["依次对指标编号、指标名称、状态列触发升序/降序", "断言各列排序指示器切换正常且列表仍有数据"],
["指标编码填 mock 后查询", "断言空态"], "三列均可正常排序且无报错。",
"无匹配不报错。", auto="VENDOR-EVI-005"), pre="已选中左侧「指标一级分类」且右侧列表至少 2 条数据", auto="VENDOR-EVI-004"),
case(6, "树与列表", "功能验证点", "P0", "列表核心列完整", 12, case(5, "指标一级分类右侧列表", "功能验证点", "P0", "首行指标编码与指标名称组合查询", 14,
["断言表头含评价类型、指标编码或指标编号、指标名称、状态、操作"], ["读取首行指标编号/编码与指标名称填入筛选并查询", "断言结果行与首行取值匹配"],
"核心列可见。", auto="VENDOR-EVI-006"), "组合查询结果与首行指标编码、指标名称一致。",
case(7, "详情", "功能验证点", "P0", "打开详情后返回", 14, pre="已选中「指标一级分类」且右侧列表有数据", auto="VENDOR-EVI-005"),
["点击行内详情", "断言详情层可见", "返回不保存"], case(6, "指标一级分类右侧列表", "功能验证点", "P0", "重置清空指标编码与指标名称筛选", 15,
"详情只读可返回。", ["填写指标编码、指标名称后点击重置", "断言两项筛选框已清空"],
pre="存在详情操作行", auto="VENDOR-EVI-007"), "重置后筛选条件恢复为空。",
case(8, "树与列表", "边界验证点", "P2", "分类下无指标空态", 15, pre="已选中「指标一级分类」且右侧列表有数据", auto="VENDOR-EVI-006"),
["选择无指标的分类(若有)", "断言右表空态明确"], case(7, "指标一级分类右侧列表", "功能验证点", "P0", "列设置取消勾选后再勾选列表列恢复", 16,
"空分类下列表空态正确。"), ["打开列设置,取消勾选一列非核心列后确认表头隐藏", "再次勾选该列后确认表头恢复"],
case(9, "树与列表", "集成验证点", "P1", "启用指标可供方案使用", 18, "列设置 toggle 后列表列展示与勾选状态一致。",
["确认状态为启用的指标", "在评价方案维护中核对其可选(只读核对或抽样)"], pre="已选中「指标一级分类」且右侧列表已查询出数据", auto="VENDOR-EVI-007"),
"启用指标与方案引用衔接正确。"),
], ],
) )
) )
...@@ -405,42 +764,79 @@ PAGES.append( ...@@ -405,42 +764,79 @@ PAGES.append(
spec="UI-SPEC-SPD-VENDOR-010", spec="UI-SPEC-SPD-VENDOR-010",
spec_file="vendor-holiday.spec.ts", spec_file="vendor-holiday.spec.ts",
iframe=False, iframe=False,
write_note="左侧新增只打开后取消;不批量删除真实数据。", write_note="左/右侧新增与左侧编辑仅打开弹框后关闭,不提交;不批量删除真实数据。",
samples="左侧首行读取 `${sampleHolidayName}`/年度;mock:`__NOT_EXIST_ORDER__99999`。", samples="左侧首行读取 `${sampleHolidayName}`/年度;mock:`__NOT_EXIST_ORDER__99999`。",
cases=[ cases=[
case(1, "页面进入", "功能验证点", "P0", "菜单进入供应商节假日管理页", 1, case(1, "左右分栏", "功能验证点", "P0", "选中左侧节假日展示详情", 10,
["按菜单进入供应商节假日管理", "断言 URL 含 `prov/holiday`",
"断言可见节假日主区与新增"],
"进入供应商节假日管理页。",
pre="已登录且具备厂商管理权限", auto="VENDOR-HLD-001"),
case(2, "左右分栏", "功能验证点", "P0", "选中左侧节假日展示详情", 9,
["点击左侧首行", "断言默认假日详情或自定义列表区可见"], ["点击左侧首行", "断言默认假日详情或自定义列表区可见"],
"详情联动成功。", "详情联动成功。",
pre="左侧有数据", auto="VENDOR-HLD-002"), pre="左侧有数据", auto="VENDOR-HLD-001"),
case(3, "左右分栏", "异常验证点", "P1", "左侧搜索无匹配空态(mock反例)", 15, case(2, "左右分栏", "异常验证点", "P1", "左侧搜索无匹配空态(mock反例)", 9,
["左侧搜索框填 mock 并回车", "断言左侧列表空态"], ["左侧搜索框填 mock 并回车", "断言左侧列表空态"],
"无匹配空态明确。", auto="VENDOR-HLD-003"), "无匹配空态明确。", auto="VENDOR-HLD-002"),
case(4, "维护", "功能验证点", "P0", "左侧新增取消", 11, case(3, "维护", "功能验证点", "P0", "点击左侧新增弹出新增弹框", 11,
["点击新增", "取消/关闭不保存", "断言仍停留本页且无异常"], ["点击左侧「新增」", "断言新增弹框或表单层可见", "取消或关闭不保存"],
"取消不落库。", auto="VENDOR-HLD-004"), "新增弹框正常弹出。", auto="VENDOR-HLD-003"),
case(5, "左右分栏", "功能验证点", "P1", "自定义节假日列表区可见", 12, case(4, "维护", "功能验证点", "P0", "选中首行点击编辑弹出编辑弹框", 11,
["选中左侧行", "断言「供应商自定义节假日列表」区域可见(可为空态)"], ["选中左侧节假日设置列表首行", "点击该行「编辑」", "断言编辑弹框或表单层可见", "取消或关闭不保存"],
"自定义区可见。", auto="VENDOR-HLD-005"), "编辑弹框正常弹出。", auto="VENDOR-HLD-004"),
case(6, "维护", "异常验证点", "P0", "结束早于开始日期拦截", 16, case(5, "左右分栏", "功能验证点", "P0", "首行名称或年度搜索验证", 9,
["打开新增/编辑,结束日期早于开始日期尝试保存", "断言拒绝保存并提示"], ["选中左侧节假日设置列表首行", "读取首行「名称」或「年度」作为关键字",
"非法日期被拦截。"), "在左侧搜索框输入关键字并查询", "断言左侧列表结果行匹配该关键字"],
case(7, "维护", "边界验证点", "P2", "开始结束为同一天", 15, "按首行名称或年度搜索成功。", auto="VENDOR-HLD-005"),
["打开新增/编辑,开始与结束日期选同一天尝试保存(或仅校验控件)", case(6, "左右分栏", "功能验证点", "P0", "切换列表行右侧详情名称年度一致", 10,
"断言允许保存或明确提示规则(按产品规则),不报错白屏"], ["点击左侧节假日设置列表首行,断言右侧默认假日详情可见",
"同日区间规则明确。"), "切换选中另一行(与首行名称或年度不同)",
case(8, "左右分栏", "集成验证点", "P1", "进院供应商主数据一致", 18, "分别断言右侧详情「名称」「年度」与当前选中行一致"],
["在自定义列表抽样进院供应商(若有数据)", "断言可在厂商/供应商主数据中找到对应记录"], "切换行后右侧详情与选中行名称、年度一致。", auto="VENDOR-HLD-006"),
"自定义节假日供应商与主数据一致。"), case(7, "维护", "功能验证点", "P0", "默认假日详情区点击右侧新增弹出弹框", 12,
["选中左侧首行,断言右侧默认假日详情可见",
"点击右侧「新增」(供应商自定义节假日列表工具栏)",
"断言新增弹框或表单层可见", "取消或关闭不保存"],
"右侧新增弹框正常弹出。", auto="VENDOR-HLD-007"),
case(8, "列设置", "功能验证点", "P2", "右侧自定义列表打开列设置面板", 14,
["选中左侧首行,断言右侧默认假日详情可见",
"点击右侧供应商自定义列表工具栏列设置(setting 齿轮,非 column-height 密度)",
"断言弹出列设置面板,含字段设置/列展示与「重置」", "关闭面板"],
"列设置面板可正常打开与关闭。", auto="VENDOR-HLD-008"),
], ],
) )
) )
VENDOR_QUL_MD_APPENDIX = """
---
### 用例索引(VENDOR-QUL-001~073)
| 范围 | 内容 | 自动化 |
|------|------|--------|
| 001 | 进入页默认证件效期提醒 | `vendor-qual.spec.ts` |
| 002–009 | 证件效期提醒:组合/重置/导出/查看/效期子 Tab | `vendor-qual.spec.ts` |
| 005–006 | 证件效期提醒 · 列设置 | `vendor-qual.spec.ts` |
| 010–057 | 其余 8 主 Tab:组合/重置/导出/查看/列表子 Tab | `vendor-qual-sync-tabs.spec.ts` + `data/vendor-qual-tab-cases.ts` |
| 058–073 | 8 主 Tab · 列设置(打开 + 列隐藏恢复) | `vendor-qual-column-settings.spec.ts` |
### 其它证件主 Tab(与证件效期提醒 002–009 同步)
以下主 Tab 在 CSV **VENDOR-QUL-010~057** 与 `vendor-qual-sync-tabs.spec.ts` 中覆盖:**组合查询**、**重置**、**导出**、**查看**、**列表子 Tab**(文案见 `data/vendor-qual-subtabs.data.ts` / `data/scan/vendor-qual-subtabs.json`)。
| 主 Tab | 组合/重置/导出/查看 | 列表子 Tab | 列设置 |
|--------|---------------------|------------|--------|
| 证件效期提醒 | 002–004、009 | **007–008** | **005–006** |
| 授权链路效期预警 | 010–013(授权书名称/授权单位/被授权单位) | **014–015**(三个月内/一个月内/已过期) | **058–059** |
| 供应商三证 | 016–019(销售商(自定义)名称) | **044–045**(全部/未过期/已过期/已缺失) | **060–061** |
| 器械注册人 | 020–023(收起+展开五项) | **046–047**(全部/已维护/未维护) | **062–063** |
| 器械注册证号 | 024–027(收起+展开八项) | **048–049**(全部/未过期/已过期) | **064–065** |
| 授权书 | 028–031(收起+展开六项) | **050–051**(全部/未过期/已过期) | **066–067** |
| 承诺书 | 032–035(销售商/承诺类型/状态) | **052–053** | **068–069** |
| 法人委托书 | 036–039(销售商/业务员/状态) | **054–055** | **070–071** |
| 二级供应商三证 | 040–043(三项组合) | **056–057** | **072–073** |
**运行**:`npm run test:vendor-qual:all`(含上述三个 spec 文件)。
"""
def tp_id(p: dict, c: dict) -> str: def tp_id(p: dict, c: dict) -> str:
tp = c["tp"] tp = c["tp"]
if isinstance(tp, int): if isinstance(tp, int):
...@@ -463,7 +859,7 @@ def render_md(p: dict) -> str: ...@@ -463,7 +859,7 @@ def render_md(p: dict) -> str:
f"**元素扫描**:`data/scan/{p['scan']}-elements.json`({p['count']} 项) ", f"**元素扫描**:`data/scan/{p['scan']}-elements.json`({p['count']} 项) ",
f"**关联规范**:`knowledge-base/08-UI专项规范库/{p['spec']}.md` ", f"**关联规范**:`knowledge-base/08-UI专项规范库/{p['spec']}.md` ",
f"**版本**:2026-09-30 ", f"**版本**:2026-09-30 ",
f"**自动化框架**:Playwright + TypeScript(`tests/spd/{p['spec_file']}`,依赖 `auth.setup` 登录态)", f"**自动化框架**:Playwright + TypeScript(`tests/spd/` 下 {p['spec_file']},依赖 `auth.setup` 登录态)",
"", "",
f"**通用前置**:已执行 `auth.setup`,`storageState` 有效;测试账号具备「基础设置 → 厂商管理 → {p['name']}」权限。 ", f"**通用前置**:已执行 `auth.setup`,`storageState` 有效;测试账号具备「基础设置 → 厂商管理 → {p['name']}」权限。 ",
f"**页面约定**:{'主体在 scjs **iframe**。' if p['iframe'] else '整页渲染,**无报表 iframe**。'} ", f"**页面约定**:{'主体在 scjs **iframe**。' if p['iframe'] else '整页渲染,**无报表 iframe**。'} ",
...@@ -505,7 +901,10 @@ def render_md(p: dict) -> str: ...@@ -505,7 +901,10 @@ def render_md(p: dict) -> str:
while lines and lines[-1] in ("", "---"): while lines and lines[-1] in ("", "---"):
lines.pop() lines.pop()
lines.append("") lines.append("")
return "\n".join(lines) body = "\n".join(lines)
if p["code"] == "VENDOR-QUL":
body = body.rstrip() + VENDOR_QUL_MD_APPENDIX
return body
def is_unlogin(c: dict) -> bool: def is_unlogin(c: dict) -> bool:
......
...@@ -180,7 +180,7 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -180,7 +180,7 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
nxt("高", "经销商 Tab", "切换「经销商」后列表为经销商数据,总数与器械注册人可不同"), nxt("高", "经销商 Tab", "切换「经销商」后列表为经销商数据,总数与器械注册人可不同"),
nxt("高", "按厂商名称搜索", "输入厂商名称关键字后结果名称匹配,无匹配时空态明确"), nxt("高", "按厂商名称搜索", "输入厂商名称关键字后结果名称匹配,无匹配时空态明确"),
nxt("高", "列表核心列", "展示序号、厂商编号、厂商名称、产地、详细地址、简称、开户银行、银行账号"), nxt("高", "列表核心列", "展示序号、厂商编号、厂商名称、产地、详细地址、简称、开户银行、银行账号"),
nxt("中", "分页翻页", "总条数大于一页时可翻至第 2 页,页码高亮且首行更换;不足 2 页则该点不适用(自动化 `VENDOR-QRY-008`)"), nxt("中", "分页翻页", "总条数大于一页时可翻至第 2 页,页码高亮且首行更换;不足 2 页则该点不适用(自动化 `VENDOR-QRY-005`)"),
] ]
bound_points = [ bound_points = [
nxt("中", "超长厂商名称关键字", "超长输入可查询或被截断并提示,不白屏"), nxt("中", "超长厂商名称关键字", "超长输入可查询或被截断并提示,不白屏"),
...@@ -194,8 +194,17 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -194,8 +194,17 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
nxt("中", "Tab 与分页总数一致", "当前 Tab 下可见行逻辑与「共 N 条」一致"), nxt("中", "Tab 与分页总数一致", "当前 Tab 下可见行逻辑与「共 N 条」一致"),
] ]
func_points += [ func_points += [
nxt("中", "清空搜索恢复更宽结果", "按厂商名称过滤后清空搜索并回车,分页总数不少于过滤后(自动化 `VENDOR-QRY-005`)"), nxt("中", "清空搜索恢复更宽结果", "按厂商名称过滤后清空搜索并回车,分页总数不少于过滤后(自动化 `VENDOR-QRY-003`)"),
nxt("中", "切换每页条数", "当前页行数 = min(总条数, 所选档位);总条数不变;切换 Tab 后分页重置合理;无数据则不适用"), nxt(
"中",
"切换每页条数",
"当前页行数 = min(总条数, 所选档位);总条数不变;切换 Tab 后分页重置合理;无数据则不适用(自动化 `VENDOR-QRY-007`)",
),
nxt(
"高",
"经销商选首行按厂商名称查询",
"经销商 Tab 点选首行后,将首行厂商名称关键字写入搜索并回车,结果名称匹配且搜索框已写入关键字(自动化 `VENDOR-QRY-006`)",
),
] ]
chunks.append( chunks.append(
section( section(
...@@ -208,6 +217,31 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -208,6 +217,31 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
], ],
) )
) )
chunks.append(
section(
"列设置",
[
(
"功能验证点",
[
nxt(
"低",
"打开列设置面板",
"点击列表工具栏 setting 齿轮(非 column-height 表格密度),弹出含字段设置/列展示与「重置」的面板,可正常关闭(自动化 `VENDOR-QRY-008`)",
),
nxt(
"低",
"列设置隐藏后再恢复",
"取消勾选非核心列后表头隐藏,再次勾选后表头恢复,勾选状态与表头一致(自动化 `VENDOR-QRY-009`)",
),
],
),
("边界验证点", ["- 无"]),
("异常验证点", ["- 无"]),
("集成验证点", ["- 无"]),
],
)
)
elif kind == "qual": elif kind == "qual":
chunks.append( chunks.append(
section( section(
...@@ -249,6 +283,35 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -249,6 +283,35 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
) )
) )
elif kind == "audit": elif kind == "audit":
if p["pageKey"] == "vendor-auth-first":
chunks.append(
section(
"授权变更列表与详情",
[
(
"功能验证点",
[
nxt("中", "重置筛选", "填写筛选项后重置,文本条件清空且状态恢复默认「通过」"),
nxt("中", "筛选区展开收起", "展开/收起切换正常,收起后再展开保留已填条件"),
nxt("高", "列设置 toggle 恢复", "取消勾选非核心列后表头隐藏,再勾选恢复"),
nxt("高", "打开详情后返回", "行内详情可见且含单号,返回不提交审批"),
nxt(
"高",
"首行九项组合查询",
"首行制单日期、单号、销售商(自定义)等九项组合筛选后结果匹配",
),
],
),
("边界验证点", []),
(
"异常验证点",
[nxt("低", "无匹配单据 mock", "不存在单号查询后空态且总数为 0")],
),
("集成验证点", []),
],
)
)
return "\n".join(chunks), n
doc = p.get("doc_type", "变更") doc = p.get("doc_type", "变更")
peer = p.get("peer", "") peer = p.get("peer", "")
is_auth = "授权" in doc is_auth = "授权" in doc
...@@ -409,35 +472,18 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -409,35 +472,18 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
( (
"功能验证点", "功能验证点",
[ [
nxt("高", "左树加载", "指标分类树可见,可搜索分类名称"), nxt("高", "点击左侧分类可正常展开", "点击收起状态的分类展开图标后,节点为展开态且子级分类可见"),
nxt("高", "选中分类刷新右表", "点击分类后右侧列表为该分类指标"), nxt("高", "切换左侧不同分类右侧内容同步变化", "依次选中不同分类后,右侧指标列表或空态随所选分类刷新且与上一分类展示不一致"),
nxt("高", "指标编码/名称筛选", "右侧按编码、名称查询结果匹配"), nxt("高", "选中左侧分类首行同步分类名称查询条件", "点击分类树首行节点后,左侧「分类名称」查询框取值与所选分类名称一致"),
nxt("高", "列表核心列", "展示评价类型、指标编码、指标名称、状态、操作(编辑/详情/日志)"), nxt("高", "指标一级分类下可排序列", "指标编号、指标名称、状态列均可升序/降序切换"),
nxt("高", "新增分类", "左侧「新增」可创建分类,树中出现新节点"), nxt("高", "首行指标编码与指标名称组合查询", "组合筛选后结果与首行一致"),
nxt("高", "新增指标", "右侧「新增」可创建指标并回填到列表"), nxt("高", "重置清空指标编码与指标名称", "重置后筛选框为空"),
nxt("中", "编辑/详情/日志", "行内编辑打开回填;详情只读;日志可打开"), nxt("高", "列设置 toggle 恢复", "取消勾选非核心列后表头隐藏,再勾选恢复"),
],
),
(
"边界验证点",
[
nxt("中", "分类下无指标", "右表空态明确,总数为 0"),
nxt("低", "分类名超长", "超长被拦截或截断并提示"),
],
),
(
"异常验证点",
[
nxt("高", "必填为空拦截", "指标编码/名称为空提交失败并提示"),
nxt("中", "删除或停用受引用指标", "若被方案引用则拒绝或提示(以产品规则为准)"),
],
),
(
"集成验证点",
[
nxt("中", "状态启用后可供方案使用", "状态为启用的指标可在评价方案中被选择"),
], ],
), ),
("边界验证点", []),
("异常验证点", []),
("集成验证点", []),
], ],
) )
) )
...@@ -498,21 +544,21 @@ VENDOR_REVIEW = { ...@@ -498,21 +544,21 @@ VENDOR_REVIEW = {
VENDOR_DOWNSTREAM = { VENDOR_DOWNSTREAM = {
"vendor-query": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-query.spec.ts` + `pages/spd/vendor-query.page.ts`", "vendor-query": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-query.spec.ts` + `pages/spd/vendor-query.page.ts`",
"vendor-qual": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-qual.spec.ts` + `pages/spd/vendor-qual.page.ts`", "vendor-qual": "已落地 CSV/MD VENDOR-QUL-001~073;Playwright:`vendor-qual.spec.ts`、`vendor-qual-sync-tabs.spec.ts`、`vendor-qual-column-settings.spec.ts` + `pages/spd/vendor-qual.page.ts` + `data/vendor-qual-*.ts`",
"vendor-auth-first": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-auth-first.page.ts`", "vendor-auth-first": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-auth-first.page.ts`",
"vendor-auth-final": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-auth-final.page.ts`", "vendor-auth-final": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-auth-final.page.ts`",
"vendor-cert-first": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-cert-first.page.ts`", "vendor-cert-first": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-cert-first.page.ts`",
"vendor-cert-final": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-cert-final.page.ts`", "vendor-cert-final": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-cert-final.page.ts`",
"vendor-cert-diff": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-cert-diff.spec.ts` + `pages/spd/vendor-cert-diff.page.ts`", "vendor-cert-diff": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-cert-diff.spec.ts` + `pages/spd/vendor-cert-diff.page.ts`",
"vendor-eval-scheme": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-eval-scheme.spec.ts` + `pages/spd/vendor-eval-scheme.page.ts`", "vendor-eval-scheme": "已落地 CSV/MD/Playwright 14 条(EVS-001 组合、014 筛选展开/收起、002/003 列设置、004~013 列排序):`data/vendor-eval-scheme.data.ts` + `vendor-eval-scheme.spec.ts` + `vendor-eval-scheme.page.ts`",
"vendor-eval-indicator": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-eval-indicator.spec.ts` + `pages/spd/vendor-eval-indicator.page.ts`", "vendor-eval-indicator": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-eval-indicator.spec.ts` + `pages/spd/vendor-eval-indicator.page.ts`",
"vendor-holiday": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-holiday.spec.ts` + `pages/spd/vendor-holiday.page.ts`", "vendor-holiday": "已落地 CSV/MD 与 Playwright(8 条 VENDOR-HLD-001~008):`testcase/基础管理-厂商管理-供应商节假日管理-UI自动化测试用例-01.csv` + `tests/spd/vendor-holiday.spec.ts` + `pages/spd/vendor-holiday.page.ts`",
} }
def h3_for(kind: str, p: dict) -> str: def h3_for(kind: str, p: dict) -> str:
if kind == "query": if kind == "query":
return "页面访问与导航、Tab、搜索与列表" return "页面访问与导航、Tab、搜索与列表、列设置"
if kind == "qual": if kind == "qual":
return "页面访问与导航、证件 Tab、效期与列表" return "页面访问与导航、证件 Tab、效期与列表"
if kind == "audit": if kind == "audit":
...@@ -522,7 +568,7 @@ def h3_for(kind: str, p: dict) -> str: ...@@ -522,7 +568,7 @@ def h3_for(kind: str, p: dict) -> str:
if kind == "scheme": if kind == "scheme":
return "页面访问与导航、方案筛选与维护" return "页面访问与导航、方案筛选与维护"
if kind == "indicator": if kind == "indicator":
return "页面访问与导航、指标分类树与指标列表" return "指标分类树与指标列表(EVI-001~007)"
return "页面访问与导航、默认假日与供应商自定义" return "页面访问与导航、默认假日与供应商自定义"
......
#!/usr/bin/env node
/**
* 统计报表字段名统一:按约定顺序替换,避免「默认二级/默认供应商」互相覆盖。
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
/** @type {[string, string][]} */
const REPLACEMENTS = [
['默认进院供应商', '默认销售商'],
['默认供应商', '默认销售商'],
['默认二级供应商', '默认供应商'],
['进院供应商名称', '销售商(自定义)名称'],
['二级供应商名称', '供应商名称'],
['区分进院供应商', '区分销售商(自定义)'],
['进院供应商', '销售商(自定义)'],
['二级供应商', '供应商'],
['产品编号', '物料编号'],
['产品编码', '物料编码'],
['产品名称', '物料名称'],
];
function migrateText(text) {
let out = text;
for (const [from, to] of REPLACEMENTS) {
out = out.split(from).join(to);
}
return out;
}
function walkFiles(dir, acc = []) {
for (const name of readdirSync(dir)) {
const full = path.join(dir, name);
const st = statSync(full);
if (st.isDirectory()) {
if (name === 'node_modules' || name === '.git') continue;
walkFiles(full, acc);
} else acc.push(full);
}
return acc;
}
const dataFiles = walkFiles(path.join(ROOT, 'data')).filter(
(f) => f.endsWith('.ts') && !f.endsWith('admit.data.ts'),
);
const testcaseCsv = walkFiles(path.join(ROOT, 'testcase')).filter(
(f) => f.includes('统计报表') && f.endsWith('.csv'),
);
const testcaseMd = walkFiles(path.join(ROOT, 'testcases')).filter(
(f) => f.includes('统计报表') && f.endsWith('.md'),
);
const statSpecs = walkFiles(path.join(ROOT, 'tests/spd')).filter((f) => {
if (!f.endsWith('.spec.ts')) return false;
const body = readFileSync(f, 'utf8');
return body.includes('统计报表') || body.includes('STAT-');
});
const pageFiles = [
'pages/spd/center-report-out.page.ts',
'pages/spd/center-report-shared.page.ts',
'pages/spd/center-report-sheet.page.ts',
'pages/spd/recon-cost-report-shared.page.ts',
'pages/spd/helpers/mock-empty-route.helper.ts',
'pages/spd/helpers/mock-search-from-config.helper.ts',
'pages/spd/pol-consumer.page.ts',
'core/column-header-match.util.ts',
].map((p) => path.join(ROOT, p));
const targets = [...dataFiles, ...testcaseCsv, ...testcaseMd, ...statSpecs, ...pageFiles];
let changed = 0;
for (const file of targets) {
const before = readFileSync(file, 'utf8');
const after = migrateText(before);
if (after !== before) {
writeFileSync(file, after, 'utf8');
changed += 1;
console.log('updated:', path.relative(ROOT, file));
}
}
console.log(`Done. ${changed} files updated.`);
#!/usr/bin/env node
/**
* 探查供应商资质管理各主 Tab 下「列表子页签」文案(非顶部证件分类 Tab)。
* 输出:data/scan/vendor-qual-subtabs.probe.json
*
* node scripts/probe-vendor-qual-subtabs.mjs
* 需 .env 中 TEST_USER/TEST_PASS 或已有 auth/user.json
*/
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const AUTH_STATE = path.join(ROOT, 'auth/user.json');
const BASE = process.env.BASE_URL || 'http://spdtest.cmic.com.cn:8080';
const OUT = path.join(ROOT, 'data/scan/vendor-qual-subtabs.probe.json');
if (existsSync(path.join(ROOT, '.env'))) {
process.loadEnvFile(path.join(ROOT, '.env'));
}
const MAIN_TABS = [
'证件效期提醒',
'授权链路效期预警',
'供应商三证',
'器械注册人',
'器械注册证号',
'授权书',
'承诺书',
'法人委托书',
'二级供应商三证',
];
async function ensureAuth(page, context) {
await page.goto(`${BASE}/spd/home/index`, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.waitForTimeout(1500);
if (!page.url().includes('/login')) return;
const user = process.env.TEST_USER;
const pass = process.env.TEST_PASS;
if (!user || !pass) throw new Error('需要 TEST_USER/TEST_PASS 或有效 auth/user.json');
await page.goto(`${BASE}/spd/login`, { waitUntil: 'domcontentloaded' });
await page.locator('#username, input[name="username"]').first().fill(user);
await page.locator('input[type="password"]').first().fill(pass);
await page.getByRole('button', { name: /登\s*录/ }).click();
await page.waitForURL(/\/spd\/home/, { timeout: 120_000 });
await context.storageState({ path: AUTH_STATE });
}
async function openQualPage(page) {
await page.goto(`${BASE}/spd/home/index`, { waitUntil: 'domcontentloaded' });
await page.getByRole('menuitem', { name: '基础设置' }).click();
await page.waitForTimeout(400);
const title = page.locator('.ant-menu-submenu-title').filter({ hasText: /^厂商管理$/ }).first();
if (await title.isVisible().catch(() => false)) {
const opened = await title
.locator('xpath=..')
.evaluate((el) => el.classList.contains('ant-menu-submenu-open'))
.catch(() => false);
if (!opened) await title.click();
}
await page.locator('.ant-menu-item').filter({ hasText: /^供应商资质管理$/ }).first().click();
await page.waitForURL(/basic\/qcManage/, { timeout: 30_000 });
await page.waitForTimeout(1200);
}
async function clickMainTab(page, tab) {
const pattern = new RegExp(tab.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const loc = page
.locator('.ant-tabs-tab, [role="tab"], .ant-radio-button-wrapper, .ant-segmented-item')
.filter({ hasText: pattern })
.first()
.or(page.getByRole('tab', { name: pattern }).first());
await loc.click({ timeout: 20_000 });
const active = page
.locator(
'.ant-tabs-tab-active, .ant-radio-button-wrapper-checked, .ant-segmented-item-selected, [role="tab"][aria-selected="true"]',
)
.filter({ hasText: pattern })
.first();
await active.waitFor({ state: 'visible', timeout: 20_000 });
await page.waitForTimeout(1000);
}
/** 列表工具栏上方、表格上方的快捷子页签(含计数按钮 / radio),限定在当前主 Tab pane。 */
async function collectListSubTabs(page) {
return page.evaluate(() => {
const panes = [...document.querySelectorAll('.ant-tabs-tabpane-active')].filter((p) =>
p.querySelector('.ant-table-wrapper'),
);
const root = panes[0] ?? document;
const table = root.querySelector?.('.ant-table-wrapper') ?? document.querySelector('.ant-table-wrapper');
if (!table) return [];
const tableTop = table.getBoundingClientRect().top;
const candidates = [
...root.querySelectorAll(
'.ant-radio-button-wrapper, .ant-segmented-item, .ant-tabs-tab, [role="tab"], button, a',
),
];
const seen = new Set();
const out = [];
for (const el of candidates) {
const rect = el.getBoundingClientRect();
if (rect.height < 20 || rect.width < 40) continue;
if (rect.top >= tableTop - 8) continue;
if (rect.top < tableTop - 220) continue;
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (!text || text.length > 40) continue;
if (/^(查\s*询|重\s*置|展\s*开|收\s*起|导\s*出)/.test(text)) continue;
if (/^全部$|^全部\s/.test(text) || /过期|缺失|维护|三个月|一个月/.test(text)) {
const key = text.replace(/\d+/g, 'N');
if (seen.has(key)) continue;
seen.add(key);
out.push(text);
}
}
return out;
});
}
async function main() {
mkdirSync(path.dirname(OUT), { recursive: true });
const browser = await chromium.launch({ headless: true, channel: 'chrome' });
const context = await browser.newContext({
storageState: existsSync(AUTH_STATE) ? AUTH_STATE : undefined,
viewport: { width: 1920, height: 1080 },
locale: 'zh-CN',
});
const page = await context.newPage();
await ensureAuth(page, context);
await openQualPage(page);
const result = { url: page.url(), scannedAt: new Date().toISOString(), mainTabs: {} };
for (const tab of MAIN_TABS) {
await clickMainTab(page, tab).catch((e) => {
console.warn('click failed', tab, e.message);
});
await page.locator('.ant-table-wrapper').first().waitFor({ state: 'visible', timeout: 20_000 }).catch(() => undefined);
const subTabs = await collectListSubTabs(page);
result.mainTabs[tab] = subTabs;
console.log(tab, '->', subTabs.join(' | '));
}
writeFileSync(OUT, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
console.log('Wrote', OUT);
await browser.close();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
#!/usr/bin/env node
/**
* 复跑 2026-10-27 basic-module-run.log 中曾失败的 spec(非全量 450 条)。
* 用法: node scripts/run-basic-module-fix-verify.mjs
*/
import { spawnSync } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const STAMP = process.env.FIX_VERIFY_STAMP || new Date().toISOString().slice(0, 16).replace(/[-:T]/g, '');
const OUT = path.join(ROOT, 'reports', `basic-fix-verify-${STAMP}`);
/** 与 reports/basic-module-run.log 中 ✘ 条目对应的 spec 文件(去重) */
const SPECS = [
'tests/spd/admit-material-list.spec.ts',
'tests/spd/admit-material.spec.ts',
'tests/spd/admit-product-apply.spec.ts',
'tests/spd/admit-product-list.spec.ts',
'tests/spd/admit-supply.spec.ts',
'tests/spd/admit-task.spec.ts',
'tests/spd/basic-branch.spec.ts',
'tests/spd/basic-finance-month.spec.ts',
'tests/spd/basic-hospital-info.spec.ts',
'tests/spd/basic-notice.spec.ts',
'tests/spd/basic-role.spec.ts',
'tests/spd/basic-user-union.spec.ts',
'tests/spd/basic-user.spec.ts',
'tests/spd/flow-def.spec.ts',
'tests/spd/flow-inst.spec.ts',
'tests/spd/surgery-base-pkg.spec.ts',
'tests/spd/surgery-oper-type.spec.ts',
'tests/spd/surgery-std-pkg.spec.ts',
'tests/spd/vendor-audit.spec.ts',
'tests/spd/vendor-cert-diff.spec.ts',
'tests/spd/vendor-eval-indicator.spec.ts',
'tests/spd/vendor-eval-scheme.spec.ts',
'tests/spd/vendor-holiday.spec.ts',
'tests/spd/vendor-qual-column-settings.spec.ts',
'tests/spd/vendor-qual-sync-tabs.spec.ts',
'tests/spd/vendor-qual.spec.ts',
'tests/spd/vendor-query.spec.ts',
'tests/spd/warehouse-probe.spec.ts',
'tests/spd/warehouse-zone.spec.ts',
];
mkdirSync(OUT, { recursive: true });
const logFile = path.join(OUT, 'run.log');
function runStep(label, args) {
writeFileSync(logFile, `\n=== ${label} ===\n`, { flag: 'a' });
const r = spawnSync('npx', args, {
cwd: ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
env: { ...process.env, FORCE_COLOR: '0' },
});
writeFileSync(logFile, (r.stdout ?? '') + (r.stderr ?? ''), { flag: 'a' });
return r.status ?? 1;
}
const setupCode = runStep('setup', ['playwright', 'test', '--project=setup']);
if (setupCode !== 0) {
console.error(`setup 失败,见 ${logFile}`);
process.exit(setupCode);
}
const testArgs = [
'playwright',
'test',
...SPECS,
'--project=chromium',
'--workers=1',
`--output=${path.join(OUT, 'test-results')}`,
];
console.log(`开始复跑 ${SPECS.length} 个曾失败 spec,日志: ${logFile}`);
const r = spawnSync('npx', testArgs, {
cwd: ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
env: { ...process.env, FORCE_COLOR: '0' },
});
const out = (r.stdout ?? '') + (r.stderr ?? '');
writeFileSync(logFile, out, { flag: 'a' });
process.stdout.write(out);
process.exit(r.status ?? 1);
#!/usr/bin/env node
/**
* 复跑 2026-09-13 脚本修复涉及用例(非全量)。
* 用法: node scripts/run-fix-verify-20260913.mjs
*/
import { spawnSync } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const STAMP = process.env.FIX_VERIFY_STAMP || '20260913-2130';
const OUT = path.join(ROOT, 'reports', `fix-verify-${STAMP}`);
/** 今日脚本修复直接关联的用例编号(去重) */
const CASE_IDS = [
// 底部汇总 / 勾稽 / 抽屉同步 / 组合 / 筛选 / mock / 页签 / 重置
'STAT-CED-002',
'STAT-CED-003',
'STAT-CED-004',
'STAT-CED-009',
'STAT-CEE-002',
'STAT-CEE-003',
'STAT-CEE-004',
'STAT-CFO-003',
'STAT-CFO-004',
'STAT-CFO-005',
'STAT-CFP-004',
'STAT-CFP-005',
'STAT-CFP-007',
'STAT-CFP-008',
'STAT-CFP-009',
'STAT-RDA-007',
'STAT-RDA-008',
'STAT-RDA-009',
'STAT-RPE-007',
'STAT-PSI-013',
'STAT-TBY-005',
'STAT-CTA-007',
'STAT-DPS-001',
'STAT-DPS-014',
'STAT-CDN-006',
'STAT-NES-007',
'STAT-NOS-007',
'STAT-IES-004',
// 打印按钮
'STAT-IPV-013',
'STAT-IBL-027',
// 列对齐 / 核心列(visibleThead 表头定位)
'STAT-LMP-006',
'STAT-LMS-007',
'STAT-PSI-006',
'STAT-PTR-005',
'STAT-CMC-004',
'STAT-CPT-004',
'STAT-CRK-007',
'STAT-RNO-009',
'STAT-ROC-008',
'STAT-RPC-008',
'STAT-RSU-008',
'STAT-EXC-008',
'STAT-EXL-007',
'STAT-EXN-007',
'STAT-PDV-004',
'STAT-PKP-007',
'STAT-PRT-007',
'STAT-PSV-004',
'STAT-CDR-006',
'STAT-CFC-006',
'STAT-CSE-008',
'STAT-RBE-004',
'STAT-TBC-006',
'STAT-TBC-007',
'STAT-TBY-006',
'STAT-TBY-008',
'STAT-TDD-007',
'STAT-TPR-007',
'STAT-WEX-009',
'STAT-WST-009',
];
mkdirSync(OUT, { recursive: true });
const grep = CASE_IDS.join('|');
writeFileSync(path.join(OUT, 'cases.txt'), `${CASE_IDS.join('\n')}\n`, 'utf8');
writeFileSync(
path.join(OUT, 'README.md'),
`# 脚本修复复跑 ${STAMP}
- 用例数:${CASE_IDS.length}
- 命令:\`EXCLUDE_DEBUG_SPECS=1 PW_WORKERS=2 npx playwright test tests/spd --project=chromium --grep "<cases>"\`
- 说明:不含「无列设置按钮」45 条(产品问题);含 describeWithReuseOpen 隔离后曾连带 skip 的兄弟用例。
## 结果文件
| 文件 | 说明 |
|------|------|
| \`run.log\` | 完整控制台输出 |
| \`summary.json\` | 通过/失败/跳过计数 |
| \`playwright-report/\` | HTML 报告 |
| \`junit.xml\` | JUnit |
| \`cases.txt\` | 本批用例编号清单 |
`,
'utf8',
);
const env = {
...process.env,
EXCLUDE_DEBUG_SPECS: '1',
PW_WORKERS: process.env.PW_WORKERS || '2',
JUNIT: '1',
PW_HTML_DIR: path.join(OUT, 'playwright-report'),
PW_JUNIT_FILE: path.join(OUT, 'junit.xml'),
PW_OUTPUT_DIR: path.join(OUT, 'test-results'),
};
console.log(`[fix-verify] 输出目录: ${OUT}`);
console.log(`[fix-verify] 用例数: ${CASE_IDS.length}`);
const started = Date.now();
const result = spawnSync(
'npx',
[
'playwright',
'test',
'tests/spd',
'--project=chromium',
`--grep=${grep}`,
],
{ cwd: ROOT, env, stdio: 'inherit', shell: false },
);
const elapsedMs = Date.now() - started;
const summary = {
stamp: STAMP,
caseCount: CASE_IDS.length,
exitCode: result.status ?? 1,
elapsedMs,
elapsedHuman: `${Math.floor(elapsedMs / 60000)}m ${Math.floor((elapsedMs % 60000) / 1000)}s`,
finishedAt: new Date().toISOString(),
};
writeFileSync(path.join(OUT, 'summary.json'), JSON.stringify(summary, null, 2), 'utf8');
console.log(`[fix-verify] 完成 exit=${summary.exitCode} 耗时 ${summary.elapsedHuman}`);
process.exit(summary.exitCode);
...@@ -13,35 +13,32 @@ ADMIT-PAP-004,基础设置 · 准入管理,产品准入申请 · 筛选查询, ...@@ -13,35 +13,32 @@ ADMIT-PAP-004,基础设置 · 准入管理,产品准入申请 · 筛选查询,
ADMIT-PAP-005,基础设置 · 准入管理,产品准入申请 · 筛选查询,重置恢复默认,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 填入 `${sampleProductName}` ADMIT-PAP-005,基础设置 · 准入管理,产品准入申请 · 筛选查询,重置恢复默认,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 填入 `${sampleProductName}`
2. 点击「重 置」 2. 点击「重 置」
3. 断言筛选恢复默认",筛选字段恢复默认;列表按默认规则刷新。, 3. 断言筛选恢复默认",筛选字段恢复默认;列表按默认规则刷新。,
ADMIT-PAP-006,基础设置 · 准入管理,产品准入申请 · 筛选查询,无匹配结果空态(mock反例),中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 物料名称填入 `__NOT_EXIST_GOODS__99999` ADMIT-PAP-006,基础设置 · 准入管理,产品准入申请 · 状态与列表,列表核心列完整,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);默认查询至少 1 条数据,1. 断言表头含准入单号、产品编号、物料名称、规格型号、注册规格、注册型号、器械注册人、单位、操作,核心列可见;操作含「详情」「日志」。,
2. 点击「查 询」 ADMIT-PAP-007,基础设置 · 准入管理,产品准入申请 · 状态与列表,切换待申请 Tab,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 点击「待申请」
3. 断言空态且分页总数为 0",无匹配时不报错;总数为 0。,
ADMIT-PAP-007,基础设置 · 准入管理,产品准入申请 · 状态与列表,列表核心列完整,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);默认查询至少 1 条数据,1. 断言表头含准入单号、产品编号、物料名称、规格型号、注册规格、注册型号、器械注册人、单位、操作,核心列可见;操作含「详情」「日志」。,
ADMIT-PAP-008,基础设置 · 准入管理,产品准入申请 · 状态与列表,切换待申请 Tab,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 点击「待申请」
2. 断言有数据时行不属于已生效/已作废;计数为 0 时为空态",待申请 Tab 与列表状态一致。, 2. 断言有数据时行不属于已生效/已作废;计数为 0 时为空态",待申请 Tab 与列表状态一致。,
ADMIT-PAP-009,基础设置 · 准入管理,产品准入申请 · 状态与列表,翻至第 2 页,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);分页总数大于每页条数,"1. 记录第 1 页首行准入单号 ADMIT-PAP-008,基础设置 · 准入管理,产品准入申请 · 状态与列表,翻至第 2 页,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);分页总数大于每页条数,"1. 记录第 1 页首行准入单号
2. 点击页码「2」或「下一页」 2. 点击页码「2」或「下一页」
3. 断言页码高亮为 2,首行单号与第 1 页不同",翻页成功且数据更换。不足两页则 skip。, 3. 断言页码高亮为 2,首行单号与第 1 页不同",翻页成功且数据更换。不足两页则 skip。,
ADMIT-PAP-010,基础设置 · 准入管理,产品准入申请 · 状态与列表,跳至指定页,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);总页数不少于 2,分页存在「跳至」,"1. 在跳至输入框填入 `2` 并回车 ADMIT-PAP-009,基础设置 · 准入管理,产品准入申请 · 状态与列表,跳至指定页,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);总页数不少于 2,分页存在「跳至」,"1. 在跳至输入框填入 `2` 并回车
2. 断言当前页为 2",跳转到第 2 页。无跳至或仅 1 页则 skip。, 2. 断言当前页为 2",跳转到第 2 页。无跳至或仅 1 页则 skip。,
ADMIT-PAP-011,基础设置 · 准入管理,产品准入申请 · 新增与导入,打开新增产品后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 点击「新增产品」 ADMIT-PAP-010,基础设置 · 准入管理,产品准入申请 · 新增与导入,打开新增产品后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 点击「新增产品」
2. 断言可见物料名称、器械注册人、规格型号、注册证号及返回 2. 断言可见物料名称、器械注册人、规格型号、注册证号及返回
3. 点击返回,不保存",新增页字段可见;返回后列表不新增记录。, 3. 点击返回,不保存",新增页字段可见;返回后列表不新增记录。,
ADMIT-PAP-012,基础设置 · 准入管理,产品准入申请 · 新增与导入,新增取消不落库,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 记录分页总数 `${sampleTotal}` ADMIT-PAP-011,基础设置 · 准入管理,产品准入申请 · 新增与导入,新增取消不落库,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe),"1. 记录分页总数 `${sampleTotal}`
2. 打开新增,物料名称填 `${uniqProductName}`(建议 `AUTO-PAP-${timestamp}`)后返回不保存 2. 打开新增,物料名称填 `${uniqProductName}`(建议 `AUTO-PAP-${timestamp}`)后返回不保存
3. 按该名称查询,断言无此行,总数不变",取消后不产生新产品。, 3. 按该名称查询,断言无此行,总数不变",取消后不产生新产品。,
ADMIT-PAP-013,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,打开详情后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);默认查询至少 1 条数据,"1. 记录首行 `${sampleProductName}` ADMIT-PAP-012,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,打开详情后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);默认查询至少 1 条数据,"1. 记录首行 `${sampleProductName}`
2. 点击「详情」,断言名称一致 2. 点击「详情」,断言名称一致
3. 返回列表",详情与该行一致;返回后仍在产品准入申请列表。, 3. 返回列表",详情与该行一致;返回后仍在产品准入申请列表。,
ADMIT-PAP-014,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,待申请编辑回填,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);待申请至少 1 行含「编辑」,"1. 读取该行 `${sampleProductName}`、`${sampleSpec}` ADMIT-PAP-013,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,待申请编辑回填,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);待申请至少 1 行含「编辑」,"1. 读取该行 `${sampleProductName}`、`${sampleSpec}`
2. 点击「编辑」 2. 点击「编辑」
3. 断言名称与规格型号回填一致 3. 断言名称与规格型号回填一致
4. 返回不保存",编辑回填与列表行一致。无编辑按钮则 skip。, 4. 返回不保存",编辑回填与列表行一致。无编辑按钮则 skip。,
ADMIT-PAP-015,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,编辑取消不保存,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);存在可编辑行,"1. 打开编辑,把物料名称改为 `${uniqProductName}` 后返回不保存 ADMIT-PAP-014,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,编辑取消不保存,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);存在可编辑行,"1. 打开编辑,把物料名称改为 `${uniqProductName}` 后返回不保存
2. 再次打开,断言名称仍为原值",取消后数据不变。, 2. 再次打开,断言名称仍为原值",取消后数据不变。,
ADMIT-PAP-016,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,作废确认后取消,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);待申请至少 1 条可勾选,"1. 勾选 1 行并点击「作 废」 ADMIT-PAP-015,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,作废确认后取消,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);待申请至少 1 条可勾选,"1. 勾选 1 行并点击「作 废」
2. 断言确认框出现 2. 断言确认框出现
3. 点击「取 消」 3. 点击「取 消」
4. 断言该行仍在,状态未变为已作废",取消作废不改变状态。本条不点击确定。, 4. 断言该行仍在,状态未变为已作废",取消作废不改变状态。本条不点击确定。,
ADMIT-PAP-017,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,已通过行不提供编辑,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);已通过 Tab 计数大于 0,"1. 切换「已通过」 ADMIT-PAP-016,基础设置 · 准入管理,产品准入申请 · 详情、编辑与作废,已通过行不提供编辑,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入产品准入申请页(/spd/basic/dataSync/dataSync/productApply/list,无 iframe);已通过 Tab 计数大于 0,"1. 切换「已通过」
2. 断言首行无「编辑」,或打开后保存被拒绝",已通过行不能改写物料名称。计数为 0 则 skip。, 2. 断言首行无「编辑」,或打开后保存被拒绝",已通过行不能改写物料名称。计数为 0 则 skip。,
...@@ -4,37 +4,32 @@ ADMIT-SUP-001,基础设置 · 准入管理,供货目录申请 · 页面进入, ...@@ -4,37 +4,32 @@ ADMIT-SUP-001,基础设置 · 准入管理,供货目录申请 · 页面进入,
3. 断言可见「查 询」、「批量提交」、「新 增」及列表表头",进入供货目录申请页;不误入准入任务分配。, 3. 断言可见「查 询」、「批量提交」、「新 增」及列表表头",进入供货目录申请页;不误入准入任务分配。,
ADMIT-SUP-002,基础设置 · 准入管理,供货目录申请 · 筛选与状态,默认查询列表有数据或空态明确,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 点击「查 询」并等待列表稳定 ADMIT-SUP-002,基础设置 · 准入管理,供货目录申请 · 筛选与状态,默认查询列表有数据或空态明确,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 点击「查 询」并等待列表稳定
2. 断言出现数据行,或「暂无数据」且分页总数为 0",查询无报错;有数据时分页「共 N 条」N≥1;无数据时空态明确。, 2. 断言出现数据行,或「暂无数据」且分页总数为 0",查询无报错;有数据时分页「共 N 条」N≥1;无数据时空态明确。,
ADMIT-SUP-003,基础设置 · 准入管理,供货目录申请 · 筛选与状态,按进院供应商筛选,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);默认查询至少 1 条且进院供应商非空,"1. 从首行读取 `${sampleHospitalSupplier}` ADMIT-SUP-003,基础设置 · 准入管理,供货目录申请 · 筛选与状态,变更单号无匹配空态(mock反例),中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 变更单号填入 `__NOT_EXIST_ORDER__99999`;若筛选区无此框,则在物料名称填同一 mock
2. 在进院供应商选择该值,制单日期保持默认,点击「查 询」
3. 断言结果行「进院供应商」与所选值一致",进院供应商列与条件一致。,
ADMIT-SUP-004,基础设置 · 准入管理,供货目录申请 · 筛选与状态,重置恢复默认,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);已选择进院供应商,"1. 点击「重 置」
2. 断言进院供应商等筛选恢复默认",筛选恢复默认;列表按默认规则刷新。,
ADMIT-SUP-005,基础设置 · 准入管理,供货目录申请 · 筛选与状态,变更单号无匹配空态(mock反例),中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 变更单号填入 `__NOT_EXIST_ORDER__99999`;若筛选区无此框,则在物料名称填同一 mock
2. 点击「查 询」 2. 点击「查 询」
3. 断言空态且分页总数为 0",无匹配时不报错;总数为 0。, 3. 断言空态且分页总数为 0",无匹配时不报错;总数为 0。,
ADMIT-SUP-006,基础设置 · 准入管理,供货目录申请 · 筛选与状态,列表核心列完整,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);默认查询至少 1 条数据,1. 断言表头含申请单号、申请人、申请时间、进院供应商、二级供应商、业务模式、是否默认、备注、操作,核心列可见;操作含「编辑」或「详情」(随状态变化)。, ADMIT-SUP-004,基础设置 · 准入管理,供货目录申请 · 筛选与状态,列表核心列完整,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);默认查询至少 1 条数据,1. 断言表头含申请单号、申请人、申请时间、进院供应商、二级供应商、业务模式、是否默认、备注、操作,核心列可见;操作含「编辑」或「详情」(随状态变化)。,
ADMIT-SUP-007,基础设置 · 准入管理,供货目录申请 · 筛选与状态,切换待提交 Tab 且计数一致,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 点击「待提交」 ADMIT-SUP-005,基础设置 · 准入管理,供货目录申请 · 筛选与状态,切换待提交 Tab 且计数一致,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 点击「待提交」
2. 读取括号数量 2. 读取括号数量
3. 断言分页「共 N 条」等于该数量;有数据时状态为待提交",待提交 Tab 与分页总数一致。计数为 0 时为空态。, 3. 断言分页「共 N 条」等于该数量;有数据时状态为待提交",待提交 Tab 与分页总数一致。计数为 0 时为空态。,
ADMIT-SUP-008,基础设置 · 准入管理,供货目录申请 · 新增与编辑,新增放弃不落库,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 记录总数 `${sampleTotal}` ADMIT-SUP-006,基础设置 · 准入管理,供货目录申请 · 新增与编辑,新增放弃不落库,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),"1. 记录总数 `${sampleTotal}`
2. 打开新增,备注填 `${uniqRemark}`(建议 `AUTO-SUP-${timestamp}`)后返回不提交 2. 打开新增,备注填 `${uniqRemark}`(建议 `AUTO-SUP-${timestamp}`)后返回不提交
3. 断言总数不变,列表无该备注",未提交不产生申请单。, 3. 断言总数不变,列表无该备注",未提交不产生申请单。,
ADMIT-SUP-009,基础设置 · 准入管理,供货目录申请 · 新增与编辑,业务模式与是否默认只切换不提交,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);已打开新增页,"1. 展开业务模式、是否默认,断言存在可选项 ADMIT-SUP-007,基础设置 · 准入管理,供货目录申请 · 新增与编辑,业务模式与是否默认只切换不提交,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);已打开新增页,"1. 展开业务模式、是否默认,断言存在可选项
2. 各选一项后返回,不提交 2. 各选一项后返回,不提交
3. 断言列表总数不变",选项可见;不提交则无新单。, 3. 断言列表总数不变",选项可见;不提交则无新单。,
ADMIT-SUP-010,基础设置 · 准入管理,供货目录申请 · 新增与编辑,待提交编辑回填,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);待提交至少 1 行含「编辑」,"1. 读取该行 `${sampleHospitalSupplier}`、`${sampleBizMode}`、`${sampleDefaultFlag}` ADMIT-SUP-008,基础设置 · 准入管理,供货目录申请 · 新增与编辑,待提交编辑回填,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);待提交至少 1 行含「编辑」,"1. 读取该行 `${sampleHospitalSupplier}`、`${sampleBizMode}`、`${sampleDefaultFlag}`
2. 点击「编辑」 2. 点击「编辑」
3. 断言进院供应商、业务模式、是否默认与该行一致 3. 断言进院供应商、业务模式、是否默认与该行一致
4. 返回不提交",编辑回填与列表行一致。无待提交或无编辑则 skip。, 4. 返回不提交",编辑回填与列表行一致。无待提交或无编辑则 skip。,
ADMIT-SUP-011,基础设置 · 准入管理,供货目录申请 · 新增与编辑,编辑取消不保存,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);存在可编辑的待提交行,"1. 打开编辑,把备注改为 `${uniqRemark}` 后返回不提交 ADMIT-SUP-009,基础设置 · 准入管理,供货目录申请 · 新增与编辑,编辑取消不保存,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);存在可编辑的待提交行,"1. 打开编辑,把备注改为 `${uniqRemark}` 后返回不提交
2. 再次打开,断言备注仍为原值",取消后备注不变。, 2. 再次打开,断言备注仍为原值",取消后备注不变。,
ADMIT-SUP-012,基础设置 · 准入管理,供货目录申请 · 批量提交与详情,未勾选时批量提交不可用,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),无勾选行,"1. 断言「批量提交」计数为 0,或按钮不可用 ADMIT-SUP-010,基础设置 · 准入管理,供货目录申请 · 批量提交与详情,未勾选时批量提交不可用,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe),无勾选行,"1. 断言「批量提交」计数为 0,或按钮不可用
2. 若仍可点击,断言出现请选择提示,且无单据离开待提交",未勾选不提交任何申请单。, 2. 若仍可点击,断言出现请选择提示,且无单据离开待提交",未勾选不提交任何申请单。,
ADMIT-SUP-013,基础设置 · 准入管理,供货目录申请 · 批量提交与详情,勾选后批量提交取消,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);待提交至少 1 条,"1. 勾选 1 行,记录申请单号与状态 ADMIT-SUP-011,基础设置 · 准入管理,供货目录申请 · 批量提交与详情,勾选后批量提交取消,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;已进入供货目录申请页(/spd/basic/dataSync/dataSync/supplyApply/list,无 iframe);待提交至少 1 条,"1. 勾选 1 行,记录申请单号与状态
2. 点击「批量提交」 2. 点击「批量提交」
3. 若出现确认框,点「取 消」 3. 若出现确认框,点「取 消」
4. 断言该单仍为待提交",取消批量提交不改变状态。本条不点击确定。无待提交则 skip。, 4. 断言该单仍为待提交",取消批量提交不改变状态。本条不点击确定。无待提交则 skip。,
ADMIT-SUP-014,基础设置 · 准入管理,供货目录申请 · 筛选与状态,多条件组合查询(制单日期等6项),高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 · 准入管理」权限;已进入准入管理对应功能页;筛选区已展开,"1. 进入页面并等待列表加载 ADMIT-SUP-012,基础设置 · 准入管理,供货目录申请 · 筛选与状态,多条件组合查询(制单日期等6项),高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 · 准入管理」权限;已进入准入管理对应功能页;筛选区已展开,"1. 进入页面并等待列表加载
2. 「制单日期」日期区间选择当天 2. 「制单日期」日期区间选择当天
3. 「销售商」下拉选择任一可选项 3. 「销售商」下拉选择任一可选项
4. 「供应商」下拉选择任一可选项 4. 「供应商」下拉选择任一可选项
......
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