Commit b8b4c69c authored by liguangyu06's avatar liguangyu06
Browse files

新增产品管理全链路自动化资产,并修复详情层/树页/路径串页等稳定问题。

parent 297e887d
#!/usr/bin/env node
/**
* 真环境补充产品管理「按钮 → 弹窗/抽屉/详情页」截图。
*
* 输出:
* docs/基础管理-产品管理-{页面}/补充-*.png
* data/scan/prodm-overlay-capture.json
* 然后可执行:
* python3 scripts/sync-docs-ui-specs.py --only PRODM-001,...
*
* Usage:
* node scripts/capture-prodm-overlays.mjs
* node scripts/capture-prodm-overlays.mjs prodm-maintain prodm-price-adj
*/
import { existsSync, mkdirSync, writeFileSync, readdirSync } 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_SUMMARY = path.join(ROOT, 'data/scan/prodm-overlay-capture.json');
if (existsSync(path.join(ROOT, '.env'))) {
process.loadEnvFile(path.join(ROOT, '.env'));
}
/** @type {{pageKey:string, menu:string, folder:string, path:string, leftTree?:boolean, toolbar:RegExp[], row:RegExp[]}[]} */
export const PAGES = [
{
pageKey: 'prodm-check-reinit',
menu: '产品核对重新发起',
folder: '基础管理-产品管理-产品核对重新发起',
path: '/spd/basic/goods/paltform/product/check',
toolbar: [/批量重新审核/, /导\s*出/],
row: [/编\s*辑/],
},
{
pageKey: 'prodm-maintain',
menu: '产品维护',
folder: '基础管理-产品管理-产品维护',
path: '/spd/basic/goods/paltform/product/calatog',
leftTree: true,
toolbar: [/添\s*加/, /导\s*出/, /导入扩展属性/, /新\s*增分类/],
row: [/详\s*情/, /编\s*辑/],
},
{
pageKey: 'prodm-material',
menu: '物料信息管理',
folder: '基础管理-产品管理-物料信息管理',
path: '/spd/basic/goods/paltform/product/zrcalatog',
leftTree: true,
toolbar: [/添\s*加/, /导\s*出/, /导入扩展属性/, /导入Excel/],
row: [/详\s*情/, /编\s*辑/],
},
{
pageKey: 'prodm-first-audit',
menu: '产品初审核对',
folder: '基础管理-产品管理-产品初审核对',
path: '/spd/basic/goods/paltform/product/first',
toolbar: [/审\s*核/, /导\s*出/, /批\s*量/],
row: [/详\s*情/, /审\s*核/, /编\s*辑/],
},
{
pageKey: 'prodm-supply',
menu: '产品供应关系维护',
folder: '基础管理-产品管理-产品供应关系维护',
path: '/spd/basic/goods/paltform/purchases/calatog',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/, /导\s*入/],
row: [/详\s*情/, /编\s*辑/],
},
{
pageKey: 'prodm-price-check',
menu: '产品物价核对',
folder: '基础管理-产品管理-产品物价核对',
path: '/spd/basic/goods/paltform/product/priceCheck',
toolbar: [/批量是否收费/, /批量审核/, /导\s*出/],
row: [/详\s*情/, /审\s*核/, /编\s*辑/],
},
{
pageKey: 'prodm-campus',
menu: '院区产品设置',
folder: '基础管理-产品管理-院区产品设置',
path: '/spd/basic/goods/paltform/area/calatog',
leftTree: true,
toolbar: [/导入医院产品/, /一键导入/, /导出Excel/, /打印产品码/, /下发科室/, /批量调整/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-unit-pkg',
menu: '院区单元包设置',
folder: '基础管理-产品管理-院区单元包设置',
path: '/spd/basic/goods/paltform/area/unit',
leftTree: true,
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/, /导\s*入/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-center-limit-ai',
menu: '中心库上下限建议(AI)',
folder: '基础管理-产品管理-中心库上下限建议(AI)',
path: '/spd/basic/goods/paltform/product/centerLimitAi',
toolbar: [/全部调整/, /批量调整/, /导\s*出/, /采\s*纳/],
row: [/详\s*情/, /编\s*辑/, /调\s*整/],
},
{
pageKey: 'prodm-req-channel',
menu: '请领出库渠道设置',
folder: '基础管理-产品管理-请领出库渠道设置',
path: '/spd/basic/goods/platform/apply/rules',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-center-limit',
menu: '中心库上下限建议',
folder: '基础管理-产品管理-中心库上下限建议',
path: '/spd/basic/goods/paltform/product/centerLimit',
toolbar: [/全部调整/, /批量调整/, /导\s*出/, /采\s*纳/],
row: [/详\s*情/, /编\s*辑/, /调\s*整/],
},
{
pageKey: 'prodm-settle',
menu: '结算点设置',
folder: '基础管理-产品管理-结算点设置',
path: '/spd/basic/goods/platform/settle/rules',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-price-adj',
menu: '调价管理',
folder: '基础管理-产品管理-调价管理',
path: '/spd/basic/goods/platform/price',
toolbar: [/新\s*增/, /导入excel/, /导\s*入/],
row: [/详\s*情/, /编\s*辑/, /再次调价/, /打\s*印/],
},
{
pageKey: 'prodm-info-change',
menu: '产品信息变更',
folder: '基础管理-产品管理-产品信息变更',
path: '/spd/basic/goods/platform/product-info',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-change-detail',
menu: '变更单明细查询',
folder: '基础管理-产品管理-变更单明细查询',
path: '/spd/basic/goods/platform/change',
toolbar: [/导\s*出/],
row: [/详\s*情/, /查\s*看/],
},
{
pageKey: 'prodm-peripheral-add',
menu: '外围系统产品新增',
folder: '基础管理-产品管理-外围系统产品新增',
path: '/spd/basic/goods/interface/peripheralSystem',
toolbar: [/新\s*增/, /生\s*成/, /导\s*出/, /批\s*量/],
row: [/详\s*情/, /编\s*辑/, /处\s*理/],
},
{
pageKey: 'prodm-peripheral-chg',
menu: '外围系统产品变更管理',
folder: '基础管理-产品管理-外围系统产品变更管理',
path: '/spd/basic/goods/interface/peripheralChange',
toolbar: [/生\s*成/, /导\s*出/, /批\s*量/],
row: [/详\s*情/, /处\s*理/, /编\s*辑/],
},
{
pageKey: 'prodm-di-query',
menu: '产品DI查询',
folder: '基础管理-产品管理-产品DI查询',
path: '/spd/basic/goods/platform/diSearch',
toolbar: [/导\s*出/],
row: [/详\s*情/, /查\s*看/],
},
{
pageKey: 'prodm-stockout',
menu: '缺货目录维护',
folder: '基础管理-产品管理-缺货目录维护',
path: '/spd/basic/goods/paltform/stockoutManagement',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /删\s*除/],
},
{
pageKey: 'prodm-center-zone',
menu: '中心库库区产品设置',
folder: '基础管理-产品管理-中心库库区产品设置',
path: '/spd/basic/goods/platform/area/product',
leftTree: true,
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/, /导\s*入/, /批\s*量/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-stockout-detail',
menu: '缺货目录明细',
folder: '基础管理-产品管理-缺货目录明细',
path: '/spd/basic/goods/paltform/stockoutList',
toolbar: [/导\s*出/],
row: [/详\s*情/, /查\s*看/],
},
{
pageKey: 'prodm-material-chg',
menu: '物料信息变更',
folder: '基础管理-产品管理-物料信息变更',
path: '/spd/basic/goods/platform/zr-product-info',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-di-change',
menu: '产品DI变更',
folder: '基础管理-产品管理-产品DI变更',
path: '/spd/basic/goods/platform/dichange',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-qual-rule',
menu: '产品资质齐全规则',
folder: '基础管理-产品管理-产品资质齐全规则',
path: '/spd/basic/goods/product/rules',
toolbar: [/新\s*增/, /添\s*加/, /导\s*出/],
row: [/详\s*情/, /编\s*辑/, /日\s*志/],
},
{
pageKey: 'prodm-qual-overview',
menu: '产品资质一览',
folder: '基础管理-产品管理-产品资质一览',
path: '/spd/basic/goods/product/overview',
toolbar: [/导\s*出/],
row: [/详\s*情/, /查\s*看/],
},
{
pageKey: 'prodm-material-chg-detail',
menu: '物料变更单明细查询',
folder: '基础管理-产品管理-物料变更单明细查询',
path: '/spd/basic/goods/platform/zrchange',
toolbar: [/导\s*出/],
row: [/详\s*情/, /查\s*看/],
},
];
function slugLabel(text) {
return String(text || 'overlay')
.replace(/\s+/g, '')
.replace(/[\\/:*?"<>|]/g, '_')
.slice(0, 40);
}
export async function closeOverlays(page) {
await page.keyboard.press('Escape').catch(() => undefined);
await page.waitForTimeout(200);
for (let i = 0; i < 4; i += 1) {
const cancel = page
.locator('.ant-modal .ant-modal-footer button, .ant-drawer .ant-drawer-footer button, .ant-popconfirm button')
.filter({ hasText: /取\s*消|关\s*闭/ })
.last();
if (await cancel.isVisible().catch(() => false)) {
await cancel.click({ timeout: 5_000 }).catch(() => undefined);
await page.waitForTimeout(300);
continue;
}
const x = page.locator('.ant-modal-wrap .ant-modal-close, .ant-drawer-close').last();
if (await x.isVisible().catch(() => false)) {
await x.click({ timeout: 5_000 }).catch(() => undefined);
await page.waitForTimeout(300);
continue;
}
break;
}
// custom detail pages / put-page
const back = page
.locator('.put-page button, .detail-spin-page button, .approve-page button, .xc-layout-tb.xc-page button, .ant-page-header-back')
.filter({ hasText: /返\s*回/ })
.first();
if (await back.isVisible().catch(() => false)) {
await back.click({ timeout: 8_000 }).catch(() => undefined);
await page.waitForTimeout(1000);
}
}
export async function layerInfo(page) {
return page.evaluate(() => {
const visible = (el) => {
if (!(el instanceof HTMLElement)) return false;
const s = getComputedStyle(el);
if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
const r = el.getBoundingClientRect();
if (r.width <= 0 || r.height <= 0) return false;
let p = el.parentElement;
while (p) {
if (getComputedStyle(p).display === 'none') return false;
p = p.parentElement;
}
return true;
};
const modal = [...document.querySelectorAll('.ant-modal')].find((m) => {
const wrap = m.closest('.ant-modal-wrap');
return wrap && visible(wrap) && visible(m);
});
const drawer = [...document.querySelectorAll('.ant-drawer-open .ant-drawer-content-wrapper, .ant-drawer-open')].find(visible);
const put = [...document.querySelectorAll('.put-page')].find(visible);
// SPD custom detail pages (调价详情 / 产品详情等)
const detailPage = [...document.querySelectorAll('.detail-spin-page, .approve-page, .xc-page.xc-layout-tb')].find((el) => {
if (!visible(el)) return false;
const text = el.innerText || '';
return /返\s*回/.test(text) && (/单号|物料编号|基本信息|审批进程|销售商/.test(text) || el.querySelector('.modal-detail-body, .ant-descriptions'));
});
const title =
(modal && modal.querySelector('.ant-modal-title')?.textContent?.trim()) ||
(drawer && document.querySelector('.ant-drawer-open .ant-drawer-title')?.textContent?.trim()) ||
(put && put.querySelector('.ant-page-header-heading-title, .ant-descriptions-title, h1,h2,h3')?.textContent?.trim()) ||
(detailPage &&
(
detailPage.querySelector('.ant-page-header-heading-title')?.textContent?.trim() ||
detailPage.innerText?.match(/单号[::]\s*\S+/)?.[0] ||
[...document.querySelectorAll('.ant-tabs-tab')].map((t) => t.textContent?.trim()).find((t) => /基本信息|供应信息|详情/.test(t || '')) ||
'详情页'
)) ||
'';
let kind = null;
if (modal) kind = 'modal';
else if (drawer) kind = 'drawer';
else if (put || detailPage) kind = 'detail';
return { kind, title: String(title || '').replace(/\s+/g, ' ').slice(0, 60) };
});
}
export async function waitLayer(page, timeoutMs = 8_000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const info = await layerInfo(page);
if (info.kind) return info;
await page.waitForTimeout(250);
}
return { kind: null, title: '' };
}
export async function preparePage(page, item) {
await page.goto(`${BASE}${item.path}`, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.waitForTimeout(2500);
if (page.url().includes('/login')) throw new Error('login redirect');
if (item.leftTree) {
const switcher = page.locator('.ant-tree-switcher:not(.ant-tree-switcher-noop)').first();
if (await switcher.isVisible().catch(() => false)) {
await switcher.click({ timeout: 5_000 }).catch(() => undefined);
await page.waitForTimeout(400);
}
const node = page.locator('.ant-tree .ant-tree-title').first();
if (await node.isVisible().catch(() => false)) {
await node.click({ timeout: 5_000 }).catch(() => undefined);
await page.waitForTimeout(800);
}
}
const query = page.getByRole('button', { name: /查\s*询/ }).first();
if (await query.isVisible().catch(() => false)) {
await query.click({ timeout: 8_000 }).catch(() => undefined);
await page.waitForTimeout(2000);
}
// expand filters if needed to reveal more toolbar
const expand = page.getByRole('button', { name: /展\s*开/ }).first();
if (await expand.isVisible().catch(() => false)) {
// keep collapsed for toolbar shots; no-op
}
}
function nextIndex(docsDir) {
const existing = existsSync(docsDir)
? readdirSync(docsDir).filter((n) => n.startsWith('补充-') && n.endsWith('.png'))
: [];
const nums = existing
.map((n) => {
const m = n.match(/^补充-(\d+)-/);
return m ? Number(m[1]) : 0;
})
.filter(Boolean);
return (nums.length ? Math.max(...nums) : 0) + 1;
}
async function shot(page, docsDir, label) {
mkdirSync(docsDir, { recursive: true });
const idx = nextIndex(docsDir);
const file = path.join(docsDir, `补充-${String(idx).padStart(2, '0')}-${slugLabel(label)}.png`);
await page.screenshot({ path: file, fullPage: false });
return file;
}
export async function findAction(page, pattern, scope = 'page') {
const roots =
scope === 'row'
? [
page.locator('.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)').first(),
]
: [page];
for (const root of roots) {
const candidates = [
root.getByRole('button', { name: pattern }).first(),
root.locator('button.ant-btn, .ant-btn, a').filter({ hasText: pattern }).first(),
root.locator('span,a,button').filter({ hasText: pattern }).first(),
];
for (const loc of candidates) {
if (!(await loc.count().catch(() => 0))) continue;
const box = await loc.boundingBox().catch(() => null);
if (box && box.width > 0 && box.height > 0) return loc;
}
}
// page-level fallback for toolbar (Ant letter-spacing names)
const loose = page.locator('button, .ant-btn, a').filter({ hasText: pattern });
const n = await loose.count();
for (let i = 0; i < n; i += 1) {
const loc = loose.nth(i);
const box = await loc.boundingBox().catch(() => null);
if (box && box.width > 0 && box.height > 0) return loc;
}
return null;
}
async function tryClickAndCapture(page, docsDir, locator, labelHint, kindHint) {
if (!locator || !(await locator.count().catch(() => 0))) {
return { ok: false, reason: 'not-visible', label: labelHint };
}
const box = await locator.boundingBox().catch(() => null);
if (!box || box.width <= 0 || box.height <= 0) {
return { ok: false, reason: 'not-visible', label: labelHint };
}
const disabled = await locator.isDisabled().catch(() => false);
if (disabled) return { ok: false, reason: 'disabled', label: labelHint };
const text = ((await locator.innerText().catch(() => '')) || labelHint).replace(/\s+/g, '');
if (/^导出|^导出Excel|^打印|^打印产品码$|^导入excel$/i.test(text) && kindHint !== 'force') {
return { ok: false, reason: 'skip-export-print', label: text };
}
const beforeUrl = page.url();
const beforeSig = await page.evaluate(() => {
const tabs = [...document.querySelectorAll('.ant-tabs-tab')].map((t) => t.textContent?.trim()).join('|');
const hasDetail = !!document.querySelector('.detail-spin-page, .approve-page, .modal-detail-body');
return `${tabs}::${hasDetail}`;
});
await locator.click({ timeout: 10_000, force: true }).catch(async () => {
await locator.click({ timeout: 10_000 });
});
await page.waitForTimeout(1500);
const layer = await waitLayer(page, 8_000);
const navigated = page.url() !== beforeUrl && !page.url().includes('/login');
const afterSig = await page.evaluate(() => {
const tabs = [...document.querySelectorAll('.ant-tabs-tab')].map((t) => t.textContent?.trim()).join('|');
const hasDetail = !!document.querySelector('.detail-spin-page, .approve-page, .modal-detail-body');
return `${tabs}::${hasDetail}`;
});
const uiChanged = afterSig !== beforeSig;
if (!layer.kind && !navigated && !uiChanged) {
const pop = page.locator('.ant-popconfirm, .ant-popover-open, .ant-modal-confirm').first();
if (await pop.isVisible().catch(() => false)) {
const file = await shot(page, docsDir, `确认-${text}`);
await closeOverlays(page);
return { ok: true, kind: 'confirm', title: text, file, label: text };
}
// message/toast only
const msg = page.locator('.ant-message-notice, .ant-notification').first();
if (await msg.isVisible().catch(() => false)) {
const file = await shot(page, docsDir, `提示-${text}`);
await closeOverlays(page);
return { ok: true, kind: 'toast', title: text, file, label: text };
}
await closeOverlays(page);
return { ok: false, reason: 'no-overlay', label: text };
}
const kind = layer.kind || (navigated || uiChanged ? 'detail' : 'overlay');
const title = layer.title || text;
const file = await shot(page, docsDir, `${kind}-${title || text}`);
if (navigated) {
await page.goto(beforeUrl, { waitUntil: 'domcontentloaded', timeout: 120_000 }).catch(() => undefined);
await page.waitForTimeout(1500);
} else {
await closeOverlays(page);
// if still on detail, hard reload list
const stillDetail = await page.evaluate(
() => !!document.querySelector('.detail-spin-page, .approve-page') && /返\s*回/.test(document.body.innerText || ''),
);
if (stillDetail) {
await page.goto(beforeUrl, { waitUntil: 'domcontentloaded', timeout: 120_000 }).catch(() => undefined);
await page.waitForTimeout(1500);
}
}
return { ok: true, kind, title, file, label: text };
}
async function capturePage(browser, item) {
const docsDir = path.join(ROOT, 'docs', item.folder);
const context = await browser.newContext({
storageState: AUTH_STATE,
viewport: { width: 1920, height: 1080 },
locale: 'zh-CN',
});
const page = await context.newPage();
const result = { pageKey: item.pageKey, menu: item.menu, path: item.path, shots: [], skipped: [], errors: [] };
try {
await preparePage(page, item);
// toolbar buttons
for (const pattern of item.toolbar) {
await closeOverlays(page);
const btn = await findAction(page, pattern, 'page');
const r = await tryClickAndCapture(page, docsDir, btn, String(pattern), 'toolbar');
if (r.ok) result.shots.push(r);
else result.skipped.push(r);
if (!(await page.locator('.ant-table, .ant-form').first().isVisible().catch(() => false))) {
await preparePage(page, item);
}
}
// advanced filter drawer (common)
await closeOverlays(page);
const adv = await findAction(page, /高级筛选/, 'page');
if (adv) {
const r = await tryClickAndCapture(page, docsDir, adv, '高级筛选', 'force');
if (r.ok) result.shots.push(r);
else result.skipped.push(r);
}
// row actions on first visible data row
await closeOverlays(page);
let row = page.locator('.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)').first();
if (!(await row.isVisible().catch(() => false))) {
await preparePage(page, item);
row = page.locator('.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)').first();
}
if (await row.isVisible().catch(() => false)) {
for (const pattern of item.row) {
await closeOverlays(page);
if (!(await row.isVisible().catch(() => false))) await preparePage(page, item);
const action = await findAction(page, pattern, 'row');
const r = await tryClickAndCapture(page, docsDir, action, String(pattern), 'row');
if (r.ok) result.shots.push(r);
else result.skipped.push(r);
if (!(await page.locator('.ant-table').first().isVisible().catch(() => false))) {
await preparePage(page, item);
}
}
} else {
result.skipped.push({ ok: false, reason: 'no-data-row', label: 'row-actions' });
}
} catch (err) {
result.errors.push(String(err));
}
await context.close();
return result;
}
async function main() {
if (!existsSync(AUTH_STATE)) throw new Error(`missing ${AUTH_STATE}`);
const filter = process.argv.slice(2);
const pages = filter.length ? PAGES.filter((p) => filter.includes(p.pageKey)) : PAGES;
const browser = await chromium.launch({ headless: true, channel: 'chrome' });
const summary = [];
for (const item of pages) {
process.stdout.write(`Capture ${item.pageKey} ... `);
const row = await capturePage(browser, item);
summary.push(row);
console.log(`shots=${row.shots.length} skipped=${row.skipped.length} errors=${row.errors.length}`);
for (const s of row.shots) {
console.log(` + ${s.kind}: ${s.title || s.label} -> ${path.basename(s.file)}`);
}
}
await browser.close();
mkdirSync(path.dirname(OUT_SUMMARY), { recursive: true });
writeFileSync(OUT_SUMMARY, JSON.stringify({ scannedAt: new Date().toISOString(), base: BASE, summary }, null, 2), 'utf8');
const totalShots = summary.reduce((n, s) => n + s.shots.length, 0);
console.log('\n========== CAPTURE SUMMARY ==========');
console.log(`pages=${summary.length} shots=${totalShots}`);
console.log('Wrote', OUT_SUMMARY);
console.log('Next: python3 scripts/sync-docs-ui-specs.py --only PRODM-001,...,PRODM-026');
}
const isDirectRun =
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isDirectRun) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
#!/usr/bin/env python3
"""生成基础管理-产品管理 26 页结构化 MD(testcases/)与同源 CSV(testcase/)。
约束:H3/用例标题/绑定的 TP 名须符合 `.cursor/rules/ui-test-point-rules.mdc` 第三节
(含 3.2 原文、3.2.1 反别名、3.6 列排序每列一条、3.7 默认→20 条/页、3.5 不写菜单进入/未登录)。
"""
from __future__ import annotations
import csv
import importlib.util
import json
import re
import sys
from collections import Counter, defaultdict
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
LOCATORS_DIR = ROOT / "locators" / "spd"
SCAN_DIR = ROOT / "data" / "scan"
TP_DIR = ROOT / "testpoint"
MD_DIR = ROOT / "testcases"
CSV_DIR = ROOT / "testcase"
TODAY = date.today().isoformat()
POSITIVE_USER = "test_1"
MOCK = "__NOT_EXIST_PRODM__99999"
GROUP = "产品管理"
CSV_FIELDS = [
"用例编号",
"功能模块",
"功能测试点",
"用例标题",
"优先级",
"前置条件",
"测试步骤",
"预期结果",
"实际结果",
]
TP_LINE_RE = re.compile(
r"\*\*(TP_PRODM_[A-Z]+_\d{3})\*\*((高|中|低))|([^|]+)|验证要点:"
)
H3_RE = re.compile(r"^### (.+)$")
H4_RE = re.compile(r"^#### (.+)$")
PRIO_MD = {"高": "P0", "中": "P1", "低": "P2"}
sys.path.insert(0, str(ROOT / "scripts"))
from spd_list_table_caps import list_table_caps, sortable_column_labels # noqa: E402
_spec = importlib.util.spec_from_file_location(
"prodm_tp", ROOT / "scripts" / "generate-prodm-testpoints.py"
)
_prodm = importlib.util.module_from_spec(_spec)
assert _spec.loader is not None
_spec.loader.exec_module(_prodm)
PRODM_PAGES: list[dict] = _prodm.PAGES
page_has_advanced_drawer = _prodm.page_has_advanced_drawer
toolbar_caps = _prodm.toolbar_caps
detect_page_size_changer = _prodm.detect_page_size_changer
needs_horizontal_scroll = _prodm.needs_horizontal_scroll
detect_expand = _prodm.detect_expand
def case(
seq: int,
h3: str,
h4: str,
prio: str,
title: str,
tp_title: str,
steps: list[str],
expect: str,
pre: str = "",
tp_hint: str = "",
) -> dict:
return {
"seq": seq,
"h3": h3,
"h4": h4,
"prio": prio,
"title": title,
"tp_title": tp_title,
"tp_hint": tp_hint,
"steps": steps,
"expect": expect,
"pre": pre,
"tp_id": "",
}
def load_tp_index(name: str) -> dict[str, list[dict]]:
path = TP_DIR / f"基础管理-{GROUP}-{name}-测试点分析-01.md"
text = path.read_text(encoding="utf-8")
index: dict[str, list[dict]] = defaultdict(list)
h3 = ""
h4 = ""
for line in text.splitlines():
m3 = H3_RE.match(line)
if m3:
h3 = m3.group(1).strip()
continue
m4 = H4_RE.match(line)
if m4:
h4 = m4.group(1).strip()
continue
mt = TP_LINE_RE.search(line)
if mt:
index[mt.group(3).strip()].append(
{
"id": mt.group(1),
"prio": mt.group(2),
"title": mt.group(3).strip(),
"h3": h3,
"h4": h4,
}
)
return index
def resolve_tp(index: dict[str, list[dict]], c: dict, label: str) -> dict:
title = c["tp_title"]
hits = index.get(title) or []
hint = c.get("tp_hint") or ""
if hint:
filtered = [h for h in hits if hint in h["h3"] or hint in h["title"]]
if filtered:
hits = filtered
if len(hits) == 1:
return hits[0]
raise SystemExit(
f"{label}: 无法唯一匹配测试点「{title}」hint={hint!r} hits={[h['id'] for h in hits]}\n"
f"可用: {', '.join(sorted(index)[:40])}…"
)
def combo_query(*, advanced: bool) -> dict:
if advanced:
return case(
0,
"高级筛选抽屉",
"功能验证点",
"P0",
"全量查询条件组合查询",
"全量查询条件组合查询",
[
"默认查询 → 从首行提取全量样本(不写死业务数据,见 rules 3.4)",
"快捷区填入样本中有值的可见字段",
"高级筛选抽屉填入样本中所有非空字段(文本直接填、下拉选同值;禁用字段记录默认)",
"「查询/筛选」后抽样首行;首行同时满足样本中全部已填字段",
],
"查询正常无报错;查询条件写值正常;查询结果与查询条件一致。",
pre="默认查询至少 1 条,否则 skip",
)
return case(
0,
"快捷筛选查询",
"功能验证点",
"P0",
"全量可见条件组合查询",
"全量可见条件组合查询",
[
"默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)",
"快捷区可见非空条件全部填入后查询",
"抽样首行同时满足全部已填条件;无未捕获异常",
],
"查询正常无报错;查询结果与查询条件一致。",
pre="默认查询至少 1 条,否则 skip",
)
def reset_case() -> dict:
return case(
0,
"快捷筛选查询",
"功能验证点",
"P1",
"快捷区重置恢复默认",
"快捷区重置恢复默认",
[
"任意填写快捷区可见字段后点击「重 置」",
"断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",
],
"重置后条件恢复默认;页面无报错。",
)
def empty_list_case() -> dict:
return case(
0,
"列表",
"边界验证点",
"P1",
"无匹配空态(mock反例)",
"无匹配空列表",
[
f"名称/编号类关键字填入 `{MOCK}` 后查询",
"断言表头仍在,0 行或空态,分页总数为 0",
],
"无匹配不报错;不展示过期成功态数据。",
)
def expand_case() -> dict:
return case(
0,
"查询条件展开收起",
"功能验证点",
"P1",
"展开后更多条件可见",
"展开后更多条件可见",
[
"点击「展开」",
"断言额外筛选字段进入可视区且可填写",
"按钮文案变为「收起」",
],
"展开后更多条件可见并可交互。",
)
def advanced_open_case() -> dict:
return case(
0,
"高级筛选抽屉",
"功能验证点",
"P0",
"点击高级筛选打开抽屉",
"点击高级筛选打开抽屉",
[
"点击「高级筛选」",
"断言抽屉标题为「高级筛选」,关键字段可交互",
"关闭抽屉,快捷区条件不被误清空(以现网为准)",
],
"高级筛选抽屉可打开并关闭。",
)
def tree_cases(tree_label: str) -> list[dict]:
return [
case(
0,
f"左侧{tree_label}树",
"功能验证点",
"P0",
"树默认有选中或可展开节点",
"树默认有选中或可展开节点",
[
f"断言左侧{tree_label}树可见且有选中或可展开节点",
"读取选中节点名称 `${sampleTreeNode}`",
"断言右侧列表/表单与当前节点范围一致或为空态",
],
"树有选中;右侧不展示其它节点数据为成功态。",
),
case(
0,
f"左侧{tree_label}树",
"功能验证点",
"P0",
"切换树节点刷新右侧",
"切换树节点刷新右侧",
[
"记录当前节点与右侧首行(若有)",
"点选另一树节点(仅 1 个则 skip)",
"断言右侧数据范围切换,不残留上一节点首行",
],
"切换节点后右侧刷新。",
),
]
def list_core(cols: str, row_ops: str, *, include_row: bool) -> list[dict]:
items = [
case(
0,
"列表",
"功能验证点",
"P0",
"列表核心列完整",
"列表核心列完整",
[
f"断言表头含核心列(抽样):{cols}",
"若有数据,抽样首行关键字段非空",
],
"核心列完整;列表结构正确。",
),
]
if include_row and row_ops and row_ops.strip() not in ("—", "-"):
ops = [o.strip() for o in row_ops.replace("、", ",").split(",") if o.strip()]
first = ops[0]
# 多操作时测试点可能在「行内操作」H3 下拆成 行内「x」可达
if len(ops) > 1:
for op in ops[:3]:
items.append(
case(
0,
"行内操作",
"功能验证点",
"P0",
f"行内「{op}」可达(只读冒烟)",
f"行内「{op}」可达",
[
f"首行有数据时点击「{op}」",
"断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回",
"关闭弹层或点返回,列表仍可见",
],
f"行内「{op}」可达;关闭后回到列表。",
pre="列表至少 1 条",
tp_hint="行内操作",
)
)
else:
items.append(
case(
0,
"列表",
"集成验证点",
"P0",
f"行内「{first}」可达(只读冒烟)",
"行内操作可达",
[
f"首行有数据时点击「{first}」",
"断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回",
"关闭弹层或点返回,列表仍可见",
],
"行内操作可达;关闭后回到列表。",
pre="列表至少 1 条",
)
)
return items
def list_table_feature_cases(page_key: str) -> list[dict]:
caps = list_table_caps(page_key)
out: list[dict] = []
if caps["column_settings"]:
out.append(
case(
0,
"列表",
"功能验证点",
"P2",
"列设置隐藏后再恢复",
"列设置隐藏后再恢复",
[
"默认查询后点击表格工具栏列设置(齿轮)",
"取消勾选一列非核心字段并关闭面板,断言表头隐藏",
"再次打开列设置勾选恢复并关闭,断言表头恢复",
],
"勾选状态与表头一致。",
pre="列表表头已加载",
)
)
for col in sortable_column_labels(page_key):
out.append(
case(
0,
"列表",
"功能验证点",
"P1",
f"{col}列升序降序切换",
"各可排序列升序降序",
[
"默认查询后列表有数据",
f"横向滚动使「{col}」列表头可见(宽表时)",
"第一次点击该列排序控件并校验升序指示器",
"第二次点击并校验降序指示器",
],
"排序指示器与方向一致;列表仍有数据。",
pre="至少 2 条数据,否则 skip",
)
)
if needs_horizontal_scroll(page_key):
out.append(
case(
0,
"列表",
"边界验证点",
"P1",
"横向滚动与列对齐",
"横向滚动与列对齐",
[
"宽表场景下横向滚动使远侧列进入视口",
"断言表头列数与数据列数一致、对齐无重叠",
],
"远侧列可进入视口;表头与数据列对齐。",
pre="列表有数据且列超出视口,否则 skip",
)
)
return out
def pagination_cases(page_key: str) -> list[dict]:
items = [
case(
0,
"分页",
"功能验证点",
"P0",
"有数据时总条数",
"有数据时总条数",
[
"默认查询后读取「共 N 条」",
"断言 N 与本次查询匹配条数规则一致(有数据或 0)",
],
"「共 N 条」与查询结果一致。",
),
case(
0,
"分页",
"功能验证点",
"P0",
"切换至第 2 页",
"切换至第 2 页",
[
"记录第 1 页首行关键字段",
"点击页码「2」或「下一页」",
"断言页码高亮正确,首行数据与第 1 页不同",
],
"翻页成功;数据更换。",
pre="总条数大于一页,否则 skip",
),
]
if detect_page_size_changer(page_key):
items.append(
case(
0,
"分页",
"功能验证点",
"P1",
"切换20条每页页面正常且列表同步",
"切换每页条数",
[
"记录「共 N 条」与当前页可见行数",
"打开每页条数下拉,选择「20 条/页」",
"断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",
],
"每页条数切换生效;总数不变;列表同步。",
pre="N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)",
)
)
return items
def export_case() -> dict:
return case(
0,
"导出",
"功能验证点",
"P0",
"当前查询条件下导出",
"当前查询条件下导出",
[
"先执行一次查询",
"点击「导 出」或「导出」或「导出Excel」",
"断言无未捕获异常(不校验文件内容)",
],
"导出流程可启动。",
)
def print_case() -> dict:
return case(
0,
"打印",
"功能验证点",
"P0",
"按当前条件打印",
"按当前条件打印",
[
"点击「打 印」或「打印产品码」等页面级打印入口",
"断言可拉起对话框/预览/toast,无 error",
],
"打印流程可启动。",
)
def write_op_cases(action: str) -> list[dict]:
return [
case(
0,
action,
"功能验证点",
"P0",
f"打开「{action}」对应弹窗/抽屉",
"打开对应弹窗/抽屉",
[
f"点击「{action}」或等价入口",
"断言弹窗/抽屉/详情可见,关键字段可交互",
"可见取消/关闭/返回",
],
f"「{action}」层打开;关键控件可见。",
tp_hint=action,
),
case(
0,
action,
"边界验证点",
"P1",
f"「{action}」必填为空拦截",
"必填为空拦截",
[
f"打开「{action}」,必填项留空后点击「确 定」",
"断言出现必填提示,弹层仍打开,列表不刷新为成功态",
],
"确定被拦截;不落库。",
tp_hint=action,
pre="该层存在必填提交,否则 skip",
),
case(
0,
action,
"功能验证点",
"P1",
f"「{action}」取消不落库",
"取消不落库",
[
f"打开「{action}」,任意填写非空后点击「取 消」或关闭",
"断言弹层关闭,列表行数与打开前一致(抽样)",
],
"取消后列表无新增/无修改。",
tp_hint=action,
),
]
def status_tab_case(labels: str) -> dict:
return case(
0,
"状态 Tab",
"功能验证点",
"P0",
"状态 Tab 与列表一致",
"状态 Tab 与列表一致",
[
f"依次切换 Tab:{labels}(以现网为准)",
"每次切换后断言列表状态列与 Tab 语义一致或为空态",
],
"Tab 与列表一致;无报错。",
)
def build_cases(meta: dict) -> list[dict]:
pk = meta["pageKey"]
advanced = page_has_advanced_drawer(pk)
cases: list[dict] = []
if meta.get("layout") == "left_tree":
cases += tree_cases(meta.get("tree_label", "分类"))
cases.append(reset_case())
cases.append(combo_query(advanced=advanced))
if detect_expand(pk) or meta.get("filter_collapsed"):
cases.append(expand_case())
if advanced:
cases.append(advanced_open_case())
if meta.get("has_tabs"):
cases.append(status_tab_case(meta.get("tab_labels") or "以现网为准"))
row_ops = meta.get("row_ops", "详情")
include_row = meta.get("row_action", True)
cases += list_core(meta.get("columns", "主表核心列"), row_ops, include_row=include_row)
cases += list_table_feature_cases(pk)
cases.append(empty_list_case())
cases += pagination_cases(pk)
caps = toolbar_caps(pk)
if caps["export"]:
cases.append(export_case())
if caps["print"]:
cases.append(print_case())
for op in meta.get("write_ops") or []:
# 打开冒烟 + 取消;必填拦截保留一条
wo = write_op_cases(op)
cases.append(wo[0])
cases.append(wo[2]) # 取消不落库
cases.append(wo[1]) # 必填
return cases
def short_code(prefix: str) -> str:
# TP_PRODM_CAM -> CAM
return prefix.replace("TP_PRODM_", "")
def page_doc(meta: dict) -> dict:
pk = meta["pageKey"]
code = f"PRODM-{short_code(meta['prefix'])}"
scan_path = SCAN_DIR / f"{pk}-elements.json"
el_count = 0
if scan_path.exists():
el_count = json.loads(scan_path.read_text(encoding="utf-8")).get("elementCount", 0)
return {
"code": code,
"group": GROUP,
"name": meta["name"],
"path": meta["path"],
"scan": pk,
"count": el_count,
"spec": f"UI-SPEC-SPD-{meta['specId']}",
"spec_file": f"{pk}.spec.ts",
"tp_total": 0,
"nav_note": (
f"菜单「基础设置 → {GROUP} → {meta['name']}」;"
f"`openPath('{meta['path']}')` 并断言 URL,勿误入兄弟页。"
),
"iframe_note": "整页渲染;定位在 `page` 上。",
"write_note": "写操作仅打开弹层后取消或做必填拦截,不提交成功落库;导出/打印不校验文件内容。",
"samples": f"查询后从首行读取 `${{sampleCode}}`/`${{sampleName}}`;mock:`{MOCK}`。",
"skip": "会话过期、无菜单权限、菜单进入单条、未登录探针、接口失败注入、导出文件内容、写操作成功落库、弹窗全字段",
"cases": build_cases(meta),
}
def bind_tps(p: dict) -> None:
index = load_tp_index(p["name"])
p["tp_total"] = sum(len(v) for v in index.values())
for c in p["cases"]:
rec = resolve_tp(index, c, p["code"])
c["tp_id"] = rec["id"]
c["prio"] = PRIO_MD[rec["prio"]]
c["h4"] = rec["h4"]
# H3 以测试点文档为准(写操作业务名等)
if rec["h3"]:
c["h3"] = rec["h3"]
def renumber_cases(cases: list[dict]) -> None:
for i, c in enumerate(cases, start=1):
c["seq"] = i
def render_csv_rows(p: dict) -> list[dict]:
logged = (
f"测试环境可访问;正例账号 {POSITIVE_USER} 已通过 auth.setup 写入 storageState;"
f"具备「基础设置 → {GROUP} → {p['name']}」权限"
)
rows: list[dict] = []
for c in p["cases"]:
case_id = f"{p['code']}-{c['seq']:03d}"
pre_parts = [logged, f"已进入{p['name']}页({p['path']})"]
if c["pre"]:
pre_parts.append(c["pre"])
steps = "\n".join(
[f"1. 确认已加载 storageState({POSITIVE_USER})"]
+ [f"{i + 1}. {s}" for i, s in enumerate(c["steps"], start=1)]
)
rows.append(
{
"用例编号": case_id,
"功能模块": f"基础设置 · {p['group']}",
"功能测试点": f"{p['name']} · {c['h3']}",
"用例标题": c["title"],
"优先级": {"P0": "高", "P1": "中", "P2": "低"}[c["prio"]],
"前置条件": ";".join(pre_parts),
"测试步骤": steps,
"预期结果": c["expect"],
"实际结果": "",
}
)
return rows
def render_md(p: dict) -> str:
n = len(p["cases"])
cnt = Counter(c["prio"] for c in p["cases"])
prio_txt = " / ".join(f"{k}={cnt[k]}" for k in ("P0", "P1", "P2") if cnt[k])
lines = [
f"# 基础管理-{p['group']}-{p['name']} UI 自动化测试用例",
"",
f"**来源测试点**:`testpoint/基础管理-{GROUP}-{p['name']}-测试点分析-01.md` ",
f"**执行 CSV**:`testcase/基础管理-{GROUP}-{p['name']}-UI自动化测试用例-01.csv` ",
f"**页面 URL**:`{p['path']}` ",
f"**定位器 YAML**:`locators/spd/{p['scan']}.locator.yaml` ",
f"**元素扫描**:`data/scan/{p['scan']}-elements.json`({p['count']} 项) ",
f"**关联规范**:`knowledge-base/08-UI专项规范库/{p['spec']}.md` ",
f"**版本**:{TODAY} ",
f"**自动化框架**:Playwright + TypeScript(规划 `tests/spd/{p['spec_file']}`,依赖 `auth.setup` 登录态)",
"",
f"**通用前置**:正例账号 `{POSITIVE_USER}` + `storageState`;具备「基础设置 → {GROUP} → {p['name']}」权限。 ",
f"**页面约定**:{p['iframe_note']} ",
"**隔离**:每用例独立 context(playwright-storage-state-isolation)。 ",
f"**菜单导航**:{p['nav_note']} ",
f"**写操作**:{p['write_note']} ",
f"**动态取数**:{p['samples']}",
"",
"---",
"",
f"## 基础设置 · {p['group']}",
"",
]
current_h3 = ""
current_h4 = ""
for c in p["cases"]:
h3 = f"{p['name']} · {c['h3']}"
if h3 != current_h3:
current_h3 = h3
current_h4 = ""
lines += [f"### {h3}", ""]
if c["h4"] != current_h4:
current_h4 = c["h4"]
lines += [f"#### {current_h4}", ""]
case_id = f"{p['code']}-{c['seq']:03d}"
tc = f"TC-UI-{case_id}"
pre_txt = c["pre"] or f"已进入{p['name']}页"
lines += [
f"##### 【{c['prio']}】【{tc}】【{c['tp_id']}】{c['title']}",
"",
f"- 前置条件:{pre_txt}",
"- 操作步骤:",
]
for i, step in enumerate(c["steps"], 1):
lines.append(f" {i}. {step}")
lines.append(
f"- 自动化断言:规划与 CSV `{case_id}` 及 `tests/spd/{p['spec_file']}` 一致"
)
lines += ["", "###### 预期结果", "", c["expect"], "", "---", ""]
while lines and lines[-1] in ("", "---"):
lines.pop()
lines += [
"",
"## 质量与覆盖说明",
"",
"| 项 | 说明 |",
"|----|------|",
f"| 用例数 | **{n}** 条({prio_txt}) |",
f"| 来源 | 从 {p['tp_total']} 条测试点选取可稳定自动化场景;H3 与测试点同名 |",
f"| 未纳入自动化 | {p['skip']} |",
"| 列排序 | 测试点 1 条「各可排序列升序降序」;本 MD **每 sorter 列一条** `{列名}列升序降序切换`(3.6) |",
"| 每页条数 | 有 size-changer 时标题「切换20条每页…」,绑定 TP「切换每页条数」(3.7) |",
"| 单一职责 | 每条只断言一个核心业务结果 |",
"",
]
return "\n".join(lines)
def main() -> None:
filter_keys = set(sys.argv[1:])
pages = [p for p in PRODM_PAGES if not filter_keys or p["pageKey"] in filter_keys]
if not pages:
raise SystemExit("No pages matched")
MD_DIR.mkdir(parents=True, exist_ok=True)
CSV_DIR.mkdir(parents=True, exist_ok=True)
total = 0
for meta in pages:
p = page_doc(meta)
bind_tps(p)
renumber_cases(p["cases"])
# 列排序允许多条用例共用同一 TP
sort_tp = "各可排序列升序降序"
ids = [c["tp_id"] for c in p["cases"] if c.get("tp_title") != sort_tp]
dup = [i for i, n in Counter(ids).items() if n > 1]
if dup:
raise SystemExit(f"{p['code']} 多条用例共用 TP: {sorted(dup)}")
md_path = MD_DIR / f"基础管理-{GROUP}-{p['name']}-UI自动化测试用例-01.md"
csv_path = CSV_DIR / f"基础管理-{GROUP}-{p['name']}-UI自动化测试用例-01.csv"
md_path.write_text(render_md(p), encoding="utf-8")
rows = render_csv_rows(p)
with csv_path.open("w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=CSV_FIELDS)
w.writeheader()
w.writerows(rows)
total += len(rows)
print(f"OK {p['code']}\t{len(rows):2d} cases\t{md_path.name}")
print(f"done pages={len(pages)} cases={total}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从 testcase CSV 生成 tests/spd/prodm-*.spec.ts,与 CSV 用例编号+标题+功能测试点严格一致。"""
from __future__ import annotations
import csv
import re
from collections import OrderedDict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CSV_DIR = ROOT / "testcase"
SPEC_DIR = ROOT / "tests" / "spd"
PAGES = [
("prodmCheckReinitPage", "prodm-check-reinit.spec.ts", "基础管理-产品管理-产品核对重新发起-UI自动化测试用例-01.csv"),
("prodmMaintainPage", "prodm-maintain.spec.ts", "基础管理-产品管理-产品维护-UI自动化测试用例-01.csv"),
("prodmMaterialPage", "prodm-material.spec.ts", "基础管理-产品管理-物料信息管理-UI自动化测试用例-01.csv"),
("prodmFirstAuditPage", "prodm-first-audit.spec.ts", "基础管理-产品管理-产品初审核对-UI自动化测试用例-01.csv"),
("prodmSupplyPage", "prodm-supply.spec.ts", "基础管理-产品管理-产品供应关系维护-UI自动化测试用例-01.csv"),
("prodmPriceCheckPage", "prodm-price-check.spec.ts", "基础管理-产品管理-产品物价核对-UI自动化测试用例-01.csv"),
("prodmCampusPage", "prodm-campus.spec.ts", "基础管理-产品管理-院区产品设置-UI自动化测试用例-01.csv"),
("prodmUnitPkgPage", "prodm-unit-pkg.spec.ts", "基础管理-产品管理-院区单元包设置-UI自动化测试用例-01.csv"),
("prodmCenterLimitAiPage", "prodm-center-limit-ai.spec.ts", "基础管理-产品管理-中心库上下限建议(AI)-UI自动化测试用例-01.csv"),
("prodmReqChannelPage", "prodm-req-channel.spec.ts", "基础管理-产品管理-请领出库渠道设置-UI自动化测试用例-01.csv"),
("prodmCenterLimitPage", "prodm-center-limit.spec.ts", "基础管理-产品管理-中心库上下限建议-UI自动化测试用例-01.csv"),
("prodmSettlePage", "prodm-settle.spec.ts", "基础管理-产品管理-结算点设置-UI自动化测试用例-01.csv"),
("prodmPriceAdjPage", "prodm-price-adj.spec.ts", "基础管理-产品管理-调价管理-UI自动化测试用例-01.csv"),
("prodmInfoChangePage", "prodm-info-change.spec.ts", "基础管理-产品管理-产品信息变更-UI自动化测试用例-01.csv"),
("prodmChangeDetailPage", "prodm-change-detail.spec.ts", "基础管理-产品管理-变更单明细查询-UI自动化测试用例-01.csv"),
("prodmPeripheralAddPage", "prodm-peripheral-add.spec.ts", "基础管理-产品管理-外围系统产品新增-UI自动化测试用例-01.csv"),
("prodmPeripheralChgPage", "prodm-peripheral-chg.spec.ts", "基础管理-产品管理-外围系统产品变更管理-UI自动化测试用例-01.csv"),
("prodmDiQueryPage", "prodm-di-query.spec.ts", "基础管理-产品管理-产品DI查询-UI自动化测试用例-01.csv"),
("prodmStockoutPage", "prodm-stockout.spec.ts", "基础管理-产品管理-缺货目录维护-UI自动化测试用例-01.csv"),
("prodmCenterZonePage", "prodm-center-zone.spec.ts", "基础管理-产品管理-中心库库区产品设置-UI自动化测试用例-01.csv"),
("prodmStockoutDetailPage", "prodm-stockout-detail.spec.ts", "基础管理-产品管理-缺货目录明细-UI自动化测试用例-01.csv"),
("prodmMaterialChgPage", "prodm-material-chg.spec.ts", "基础管理-产品管理-物料信息变更-UI自动化测试用例-01.csv"),
("prodmDiChangePage", "prodm-di-change.spec.ts", "基础管理-产品管理-产品DI变更-UI自动化测试用例-01.csv"),
("prodmQualRulePage", "prodm-qual-rule.spec.ts", "基础管理-产品管理-产品资质齐全规则-UI自动化测试用例-01.csv"),
("prodmQualOverviewPage", "prodm-qual-overview.spec.ts", "基础管理-产品管理-产品资质一览-UI自动化测试用例-01.csv"),
("prodmMaterialChgDetailPage", "prodm-material-chg-detail.spec.ts", "基础管理-产品管理-物料变更单明细查询-UI自动化测试用例-01.csv"),
]
ROW_OP_RE = re.compile(r"^行内「(.+?)」可达")
OPEN_RE = re.compile(r"^打开「(.+?)」对应弹窗")
CANCEL_RE = re.compile(r"^「(.+?)」取消不落库$")
REQUIRED_RE = re.compile(r"^「(.+?)」必填为空拦截$")
SORT_RE = re.compile(r"^(.+)列升序降序切换$")
def parse_csv(path: Path):
text = path.read_text(encoding="utf-8-sig")
return [r for r in csv.DictReader(text.splitlines()) if r.get("用例编号")]
def body_for(title, fixture):
"""Return (typescript body, is_unauth)."""
f = fixture
if title.startswith("菜单进入"):
return (
""" await {f}.openFromMenu();
await {f}.expectOnPage();
await {f}.expectEnterMarkers();""".format(f=f),
False,
)
if title == "未登录访问拦截":
return (
""" const probe = await {f}.probeUnauthAccess();
expect(probe.onLogin || !probe.hasBusinessRow, '未登录不可见业务数据').toBeTruthy();""".format(
f=f
),
True,
)
if title == "树默认有选中或可展开节点":
return (
""" await {f}.open();
await {f}.expectTreeDefaultScope();""".format(f=f),
False,
)
if title == "切换树节点刷新右侧":
return (
""" await {f}.open();
const switched = await {f}.switchOtherTreeNode();
expect(switched, '应能切换树节点').toBeTruthy();""".format(f=f),
False,
)
if title == "快捷区重置恢复默认":
return (
""" await {f}.open();
await {f}.clickResetQuick();
await {f}.expectOnPage();""".format(f=f),
False,
)
if title in ("全量可见条件组合查询", "全量查询条件组合查询"):
return (
""" test.skip(
true,
'组合查询须按页面配置 quickFields/drawerFields 实现 searchWithQuickAndDrawerCombo(对齐 center-report-sheet STAT-IOD-004),禁止临时启发式填表',
);""",
False,
)
if title == "点击高级筛选打开抽屉":
return (
""" await {f}.open();
await {f}.openAdvancedFilterDrawer();""".format(f=f),
False,
)
if title in ("展开后更多条件可见", "展开第二行筛选", "展开多行筛选", "筛选区收起展开"):
return (
""" await {f}.open();
await {f}.toggleFilterExpand();""".format(f=f),
False,
)
if title == "列表核心列完整":
return (
""" await {f}.open();
await {f}.assertConfiguredHeaders();""".format(f=f),
False,
)
if title == "列设置隐藏后再恢复":
return (
""" await {f}.open();
await {f}.clickQuery();
await {f}.executeColumnSettingsToggleRestoreTest();""".format(f=f),
False,
)
sort_m = SORT_RE.match(title)
if sort_m:
col = sort_m.group(1)
return (
""" await {f}.open();
await {f}.expectColumnSortAscDesc({col!r});""".format(f=f, col=col),
False,
)
if title == "切换至第 2 页" or title == "分页翻至第 2 页":
return (
""" await {f}.open();
await {f}.goToPage2ForProdm();""".format(f=f),
False,
)
if title == "切换20条每页页面正常且列表同步":
return (
""" await {f}.open();
const {{ total, rowsBefore, rowsAfter }} = await {f}.switchToPageSizeTwenty();
const expectedRows = Math.min(total, 20);
expect(rowsAfter, '当前页行数应与 20 条/页及总条数一致').toBe(expectedRows);
if (total > 20) {{
expect(rowsAfter, '切换后可见行应随每页条数变化').not.toBe(rowsBefore);
}}
expect(await {f}.getTotalCount(), '总条数应不变').toBe(total);""".format(f=f),
False,
)
if title == "有数据时总条数":
return (
""" await {f}.open();
await {f}.assertTotalWhenHasData();""".format(f=f),
False,
)
if "无匹配空态" in title:
return (
""" await {f}.open();
await {f}.searchByMockKeyword();
const snap = await {f}.snapshotList();
expect(snap.rows === 0 || snap.emptyVisible, '无匹配应空态').toBeTruthy();""".format(
f=f
),
False,
)
if title == "横向滚动与列对齐":
return (
""" await {f}.open();
await {f}.expectHorizontalScrollAlignment();""".format(f=f),
False,
)
if title in ("当前查询条件下导出", "导出入口可触发", "导出明细可触发"):
return (
""" await {f}.open();
await {f}.clickExportIfVisible();""".format(f=f),
False,
)
if title in ("按当前条件打印", "打印入口可触发"):
return (
""" await {f}.open();
await {f}.clickPrintIfVisible();""".format(f=f),
False,
)
if title in ("状态 Tab 与列表一致", "状态 Tab 切换"):
return (
""" await {f}.open();
await {f}.clickStatusTabs();""".format(f=f),
False,
)
m = ROW_OP_RE.search(title)
if m:
op = m.group(1)
return (
""" await {f}.open();
await {f}.rowActionSmoke({op!r});""".format(f=f, op=op),
False,
)
m = OPEN_RE.search(title)
if m:
label = m.group(1)
return (
""" await {f}.open();
await {f}.openToolbarOverlay({label!r});
await {f}.closeOverlay();""".format(f=f, label=label),
False,
)
m = REQUIRED_RE.match(title)
if m:
label = m.group(1)
return (
""" await {f}.open();
await {f}.assertRequiredInterceptFor({label!r});""".format(f=f, label=label),
False,
)
m = CANCEL_RE.match(title)
if m:
label = m.group(1)
return (
""" await {f}.open();
await {f}.cancelToolbarWithoutCommit({label!r});""".format(f=f, label=label),
False,
)
raise ValueError("未映射用例标题: {0}".format(title))
def render_spec(fixture, csv_name, spec_file, cases):
grouped = OrderedDict()
for c in cases:
grouped.setdefault(c["功能测试点"].strip(), []).append(c)
npm_script = spec_file.replace(".spec.ts", "")
lines = [
"import { expect, test } from '../../fixtures';",
"",
"/**",
" * 全量用例:与 testcase/{0} 严格一致".format(csv_name),
" * 运行:npm run test:{0}:all".format(npm_script),
" */",
"test.describe('基础设置 · 产品管理', () => {",
]
for point, items in grouped.items():
lines.append(" test.describe({0}, () => {{".format(repr(point)))
unauth_pending = []
for c in items:
cid = c["用例编号"].strip()
title = c["用例标题"].strip()
test_title = "{0} {1}".format(cid, title)
body, is_unauth = body_for(title, fixture)
smoke = " { tag: '@smoke' }," if cid.endswith("-001") else ""
if is_unauth:
unauth_pending.append((test_title, smoke, body))
continue
lines.append(" test({0},{1} async ({{ {2} }}) => {{".format(repr(test_title), smoke, fixture))
lines.append(body)
lines.append(" });")
lines.append("")
if unauth_pending:
lines.append(" test.describe('未登录', () => {")
lines.append(" test.use({ storageState: { cookies: [], origins: [] } });")
for test_title, smoke, body in unauth_pending:
lines.append(
" test({0},{1} async ({{ {2} }}) => {{".format(repr(test_title), smoke, fixture)
)
for line in body.splitlines():
lines.append(" {0}".format(line.strip()))
lines.append(" });")
lines.append(" });")
while lines and lines[-1] == "":
lines.pop()
lines.append(" });")
lines.append("")
lines.append("});")
lines.append("")
return "\n".join(lines)
def main():
SPEC_DIR.mkdir(parents=True, exist_ok=True)
for fixture, spec_file, csv_name in PAGES:
cases = parse_csv(CSV_DIR / csv_name)
for c in cases:
body_for(c["用例标题"].strip(), fixture)
spec = render_spec(fixture, csv_name, spec_file, cases)
out = SPEC_DIR / spec_file
out.write_text(spec, encoding="utf-8")
print("OK {0}\t{1} tests".format(spec_file, len(cases)))
print("done")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate 产品管理 (PRODM-001~026) testpoint MD and patch UI-SPEC / INDEX.
约束:测试点须严格遵循 `.cursor/rules/ui-test-point-rules.mdc` 第三节;禁止自造 TP 名或默认拆单字段筛选。
范围:`testpoint/scope/基础管理-产品管理-UI测试点范围.md`
"""
from __future__ import annotations
import importlib.util
import json
import re
import sys
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCAN_DIR = ROOT / "data" / "scan"
LOCATORS_DIR = ROOT / "locators" / "spd"
OUT_DIR = ROOT / "testpoint"
KB = ROOT / "knowledge-base" / "08-UI专项规范库"
INDEX = KB / "INDEX.md"
SCOPE = "testpoint/scope/基础管理-产品管理-UI测试点范围.md"
TODAY = date.today().isoformat()
sys.path.insert(0, str(ROOT / "scripts"))
from spd_list_table_caps import list_table_caps, sortable_column_labels # noqa: E402
_spec = importlib.util.spec_from_file_location(
"iface_tp", ROOT / "scripts" / "generate-iface-testpoints.py"
)
assert _spec and _spec.loader
iface = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(iface)
Seq = iface.Seq
section = iface.section
none_merged = iface.none_merged
filter_collapse_section = iface.filter_collapse_section
tree_section = iface.tree_section
row_ops_section = iface.row_ops_section
page_has_advanced_drawer = iface.page_has_advanced_drawer
toolbar_caps = iface.toolbar_caps
status_tab_section = iface.status_tab_section
PEERS = (
"产品核对重新发起、产品维护、物料信息管理、产品初审核对、产品供应关系维护、产品物价核对、"
"院区产品设置、院区单元包设置、中心库上下限建议(AI)、请领出库渠道设置、中心库上下限建议、"
"结算点设置、调价管理、产品信息变更、变更单明细查询、外围系统产品新增、外围系统产品变更管理、"
"产品DI查询、缺货目录维护、中心库库区产品设置、缺货目录明细、物料信息变更、产品DI变更、"
"产品资质齐全规则、产品资质一览、物料变更单明细查询"
)
PAGES = [
{
"pageKey": "prodm-check-reinit",
"specId": "PRODM-001",
"name": "产品核对重新发起",
"prefix": "TP_PRODM_CHK",
"path": "/spd/basic/goods/paltform/product/check",
"ready": "状态 Tab(全部/初审驳回/物价驳回等)、筛选(可展开)、批量重新审核/导出、列表与行内编辑可见",
"cover": "导航、状态 Tab、筛选展开、列表分页、导出、行内编辑弹窗",
"columns": "物料编号、是否植入、物料名称、器械注册人、规格型号、单位、耗材类型、销售商等(以现网为准)",
"row_ops": "编辑",
"has_tabs": True,
"tab_labels": "全部、初审驳回、物价驳回",
"filter_collapsed": True,
"write_ops": ["重新发起审核"],
"sibling_diff": "核对驳回后重新发起审核入口,非产品初审/物价核对主流程页",
},
{
"pageKey": "prodm-maintain",
"specId": "PRODM-002",
"name": "产品维护",
"prefix": "TP_PRODM_MNT",
"path": "/spd/basic/goods/paltform/product/calatog",
"ready": "左右分栏:左侧耗材分类树(可搜/新增分类)、右侧筛选+高级筛选、添加/导出/导入扩展属性、列表与详情",
"cover": "导航、分类树、快捷/高级筛选、列表分页、导出导入、分类维护弹窗、详情页",
"columns": "HIS编码、阳采ID、规格型号、器械注册人、单位、耗材类型、品牌、器械注册证号等",
"row_ops": "详情",
"layout": "left_tree",
"tree_label": "耗材分类",
"write_ops": ["分类维护", "添加"],
"sibling_diff": "SPD 产品目录维护 + 分类树;勿与「物料信息管理」望海物料页混淆",
},
{
"pageKey": "prodm-material",
"specId": "PRODM-003",
"name": "物料信息管理",
"prefix": "TP_PRODM_MAT",
"path": "/spd/basic/goods/paltform/product/zrcalatog",
"ready": "左右分栏:左侧分类树、右侧筛选+高级筛选、添加/导出/导入扩展属性/导入Excel、列表与详情",
"cover": "导航、分类树、筛选、列表分页、导入导出、详情",
"columns": "HIS编码、阳采ID、规格型号、单位、耗材类型、品牌、医保相关编码等",
"row_ops": "详情",
"layout": "left_tree",
"tree_label": "耗材分类",
"write_ops": ["添加"],
"sibling_diff": "望海/物料信息目录;勿与「产品维护」SPD 产品目录混淆",
},
{
"pageKey": "prodm-first-audit",
"specId": "PRODM-004",
"name": "产品初审核对",
"prefix": "TP_PRODM_FAU",
"path": "/spd/basic/goods/paltform/product/first",
"ready": "筛选(可展开)、审核/导出/批量、列表与行内审核/详情",
"cover": "导航、筛选展开、列表分页、导出、审核弹窗",
"columns": "物料编号、名称、规格、注册人、审核状态等(以现网为准)",
"row_ops": "详情、审核、编辑",
"filter_collapsed": True,
"write_ops": ["产品初审核对"],
"sibling_diff": "初审核对入口;勿与「产品核对重新发起」「产品物价核对」混淆",
},
{
"pageKey": "prodm-supply",
"specId": "PRODM-005",
"name": "产品供应关系维护",
"prefix": "TP_PRODM_SUP",
"path": "/spd/basic/goods/paltform/purchases/calatog",
"ready": "供应关系筛选、新增/导出/导入、列表与详情/编辑",
"cover": "导航、筛选、列表分页、导出导入、行内详情",
"columns": "产品、供应商/销售商、供应关系字段(以现网为准)",
"row_ops": "详情、编辑",
"layout": "left_tree",
"tree_label": "分类",
"sibling_diff": "产品与供应商供应关系维护,非院区产品库区设置",
},
{
"pageKey": "prodm-price-check",
"specId": "PRODM-006",
"name": "产品物价核对",
"prefix": "TP_PRODM_PRC",
"path": "/spd/basic/goods/paltform/product/priceCheck",
"ready": "筛选(可展开)、批量是否收费/批量审核/导出、列表与行内详情/审核",
"cover": "导航、筛选展开、列表分页、导出、批量审核工具栏",
"columns": "物料、物价/收费相关列、审核状态等(以现网为准)",
"row_ops": "详情、审核、编辑",
"filter_collapsed": True,
"write_ops": ["批量审核", "批量是否收费"],
"sibling_diff": "物价核对批量收费/审核;勿与初审核对或重新发起页混淆",
},
{
"pageKey": "prodm-campus",
"specId": "PRODM-007",
"name": "院区产品设置",
"prefix": "TP_PRODM_CAM",
"path": "/spd/basic/goods/paltform/area/calatog",
"ready": "左右分栏:左侧库区树、右侧筛选+高级筛选、导入/导出Excel/打印产品码/下发科室/批量调整、列表与详情/编辑/日志",
"cover": "导航、库区树、筛选、列表分页、导入导出打印、详情/日志弹窗",
"columns": "规格型号、耗材类型、默认上架货位、单位、库存上下限、单元含量、状态等",
"row_ops": "详情、编辑、日志",
"layout": "left_tree",
"tree_label": "请领下发库区",
"write_ops": ["导入医院产品", "一键导入", "批量调整"],
"sibling_diff": "院区→库区产品设置;勿与「中心库库区产品设置」「科室库区产品设置」混淆",
},
{
"pageKey": "prodm-unit-pkg",
"specId": "PRODM-008",
"name": "院区单元包设置",
"prefix": "TP_PRODM_UPK",
"path": "/spd/basic/goods/paltform/area/unit",
"ready": "筛选+高级筛选、新增/导出/导入、列表与单元包详情",
"cover": "导航、筛选、列表分页、导出、单元包详情",
"columns": "单元包名称/编码、院区、产品含量相关列(以现网为准)",
"row_ops": "详情、编辑、日志",
"write_ops": ["新增"],
"sibling_diff": "院区单元包配置;勿与「院区产品设置」库区产品列表混淆",
},
{
"pageKey": "prodm-center-limit-ai",
"specId": "PRODM-009",
"name": "中心库上下限建议(AI)",
"prefix": "TP_PRODM_CLA",
"path": "/spd/basic/goods/paltform/product/centerLimitAi",
"ready": "筛选(可展开)、全部调整/批量调整/导出/采纳、列表与调整入口",
"cover": "导航、筛选展开、列表分页、导出、调整工具栏",
"columns": "建议上下限、现上下限、采纳状态等(以现网为准)",
"row_ops": "详情、编辑、调整",
"filter_collapsed": True,
"write_ops": ["全部调整", "批量调整", "采纳"],
"sibling_diff": "AI 建议页;勿与「中心库上下限建议」非 AI 页混淆",
},
{
"pageKey": "prodm-req-channel",
"specId": "PRODM-010",
"name": "请领出库渠道设置",
"prefix": "TP_PRODM_RCH",
"path": "/spd/basic/goods/platform/apply/rules",
"ready": "左侧树(若有)+ 筛选+高级筛选、新增/导出、列表与详情/编辑/日志",
"cover": "导航、筛选、列表分页、导出、行内操作",
"columns": "渠道规则、院区/库区、产品范围等(以现网为准)",
"row_ops": "详情、编辑、日志",
"layout": "left_tree",
"tree_label": "规则目录",
"sibling_diff": "请领出库渠道规则;勿与结算点设置混淆",
},
{
"pageKey": "prodm-center-limit",
"specId": "PRODM-011",
"name": "中心库上下限建议",
"prefix": "TP_PRODM_CLM",
"path": "/spd/basic/goods/paltform/product/centerLimit",
"ready": "筛选(可展开)、全部调整/批量调整/导出、列表与调整入口",
"cover": "导航、筛选展开、列表分页、导出、调整工具栏",
"columns": "建议上下限、现上下限、状态等(以现网为准)",
"row_ops": "详情、编辑、调整",
"filter_collapsed": True,
"write_ops": ["全部调整", "批量调整"],
"sibling_diff": "非 AI 上下限建议;勿与「中心库上下限建议(AI)」混淆",
},
{
"pageKey": "prodm-settle",
"specId": "PRODM-012",
"name": "结算点设置",
"prefix": "TP_PRODM_STL",
"path": "/spd/basic/goods/platform/settle/rules",
"ready": "左侧树(若有)+ 筛选+高级筛选、新增、列表与详情/日志",
"cover": "导航、筛选、列表分页、高级筛选、日志弹窗",
"columns": "结算点规则、院区/产品范围等(以现网为准)",
"row_ops": "详情、编辑、日志",
"layout": "left_tree",
"tree_label": "规则目录",
"sibling_diff": "结算点规则;勿与请领出库渠道设置混淆",
},
{
"pageKey": "prodm-price-adj",
"specId": "PRODM-013",
"name": "调价管理",
"prefix": "TP_PRODM_PAD",
"path": "/spd/basic/goods/platform/price",
"ready": "筛选+高级筛选、新增/导入excel、调价单列表与再次调价/详情/打印/编辑",
"cover": "导航、筛选、列表分页、新增/导入、详情与打印",
"columns": "单号、制单/计划/实际生效时间、备注、状态、销售商、供应商等",
"row_ops": "再次调价、详情、打印、编辑",
"write_ops": ["新增"],
"sibling_diff": "调价单管理;状态在列表列,非独立状态 Tab",
},
{
"pageKey": "prodm-info-change",
"specId": "PRODM-014",
"name": "产品信息变更",
"prefix": "TP_PRODM_ICH",
"path": "/spd/basic/goods/platform/product-info",
"ready": "筛选+高级筛选、新增、变更单列表与详情/打印/编辑",
"cover": "导航、筛选、列表分页、详情",
"columns": "单号、制单时间、状态、变更摘要等(以现网为准)",
"row_ops": "详情、编辑、日志",
"write_ops": ["新增"],
"sibling_diff": "产品信息变更单;勿与「物料信息变更」「变更单明细查询」混淆",
},
{
"pageKey": "prodm-change-detail",
"specId": "PRODM-015",
"name": "变更单明细查询",
"prefix": "TP_PRODM_CHD",
"path": "/spd/basic/goods/platform/change",
"ready": "筛选(可展开)、导出、变更明细列表与分页",
"cover": "导航、筛选展开、列表分页、导出",
"columns": "变更单号、产品、变更字段、时间等(以现网为准)",
"row_ops": "详情、查看",
"filter_collapsed": True,
"row_action": False,
"sibling_diff": "产品变更明细只读查询;勿与「物料变更单明细查询」混淆",
},
{
"pageKey": "prodm-peripheral-add",
"specId": "PRODM-016",
"name": "外围系统产品新增",
"prefix": "TP_PRODM_PPA",
"path": "/spd/basic/goods/interface/peripheralSystem",
"ready": "筛选(可展开)、导出、批量推送、列表与详情",
"cover": "导航、筛选展开、列表分页、导出、批量推送/详情弹窗",
"columns": "外围系统产品、推送状态等(以现网为准)",
"row_ops": "详情",
"filter_collapsed": True,
"write_ops": ["批量推送"],
"sibling_diff": "产品管理侧外围新增;勿与接口管理「外围产品变更」混淆",
},
{
"pageKey": "prodm-peripheral-chg",
"specId": "PRODM-017",
"name": "外围系统产品变更管理",
"prefix": "TP_PRODM_PPC",
"path": "/spd/basic/goods/interface/peripheralChange",
"ready": "筛选(可展开)、列表与处理入口",
"cover": "导航、筛选展开、列表分页、行内处理",
"columns": "变更类型、产品、外围字段等(以现网为准)",
"row_ops": "详情、编辑",
"filter_collapsed": True,
"sibling_diff": "产品管理侧外围变更管理;勿与接口管理「外围产品变更」混淆",
},
{
"pageKey": "prodm-di-query",
"specId": "PRODM-018",
"name": "产品DI查询",
"prefix": "TP_PRODM_DIQ",
"path": "/spd/basic/goods/platform/diSearch",
"ready": "筛选(可展开)、导出、DI 列表与详情",
"cover": "导航、筛选展开、列表分页、导出、DI 详情",
"columns": "DI、产品、注册相关列(以现网为准)",
"row_ops": "详情",
"filter_collapsed": True,
"sibling_diff": "DI 只读查询;勿与「产品DI变更」混淆",
},
{
"pageKey": "prodm-stockout",
"specId": "PRODM-019",
"name": "缺货目录维护",
"prefix": "TP_PRODM_STO",
"path": "/spd/basic/goods/paltform/stockoutManagement",
"ready": "筛选、导出、缺货维护弹窗、列表与详情",
"cover": "导航、筛选、列表分页、导出、缺货信息维护弹窗/详情",
"columns": "缺货产品、维护状态、替代品等(以现网为准)",
"row_ops": "详情、编辑",
"write_ops": ["缺货信息维护"],
"sibling_diff": "缺货目录维护写操作;勿与「缺货目录明细」只读明细混淆",
},
{
"pageKey": "prodm-center-zone",
"specId": "PRODM-020",
"name": "中心库库区产品设置",
"prefix": "TP_PRODM_CZO",
"path": "/spd/basic/goods/platform/area/product",
"ready": "筛选+高级筛选、添加中心库库区产品/导出、列表与分页",
"cover": "导航、筛选、列表分页、导出、添加弹窗",
"columns": "库区、产品、上下限/货位等(以现网为准)",
"row_ops": "详情、编辑",
"write_ops": ["添加中心库库区产品"],
"sibling_diff": "中心库库区绑定;勿与「院区产品设置」「科室库区产品设置」混淆",
},
{
"pageKey": "prodm-stockout-detail",
"specId": "PRODM-021",
"name": "缺货目录明细",
"prefix": "TP_PRODM_STD",
"path": "/spd/basic/goods/paltform/stockoutList",
"ready": "筛选(可展开)、导出、明细列表与替代产品弹窗",
"cover": "导航、筛选展开、列表分页、导出、替代产品弹窗",
"columns": "缺货明细、替代产品相关列(以现网为准)",
"row_ops": "详情、查看",
"filter_collapsed": True,
"write_ops": ["替代产品"],
"sibling_diff": "缺货明细查询;勿与「缺货目录维护」混淆",
},
{
"pageKey": "prodm-material-chg",
"specId": "PRODM-022",
"name": "物料信息变更",
"prefix": "TP_PRODM_MCH",
"path": "/spd/basic/goods/platform/zr-product-info",
"ready": "筛选+高级筛选、新增、变更单列表与详情/打印",
"cover": "导航、筛选、列表分页、详情",
"columns": "单号、制单时间、状态、物料变更摘要等(以现网为准)",
"row_ops": "详情、编辑、日志",
"write_ops": ["新增"],
"sibling_diff": "物料信息变更单;勿与「产品信息变更」混淆",
},
{
"pageKey": "prodm-di-change",
"specId": "PRODM-023",
"name": "产品DI变更",
"prefix": "TP_PRODM_DIC",
"path": "/spd/basic/goods/platform/dichange",
"ready": "筛选(可展开)、DI 变更列表与详情",
"cover": "导航、筛选展开、列表分页、详情页",
"columns": "DI 变更单号、状态、产品等(以现网为准)",
"row_ops": "详情、编辑、日志",
"filter_collapsed": True,
"sibling_diff": "DI 变更维护;勿与「产品DI查询」混淆",
},
{
"pageKey": "prodm-qual-rule",
"specId": "PRODM-024",
"name": "产品资质齐全规则",
"prefix": "TP_PRODM_QRL",
"path": "/spd/basic/goods/product/rules",
"ready": "规则筛选、新增、规则列表与编辑/详情",
"cover": "导航、筛选、列表分页、新增/编辑",
"columns": "规则名称、适用范围、齐全判定条件等(以现网为准)",
"row_ops": "详情、编辑、日志",
"write_ops": ["新增"],
"sibling_diff": "资质齐全规则配置;勿与「产品资质一览」查询页混淆",
},
{
"pageKey": "prodm-qual-overview",
"specId": "PRODM-025",
"name": "产品资质一览",
"prefix": "TP_PRODM_QOV",
"path": "/spd/basic/goods/product/overview",
"ready": "筛选、导出、资质一览列表与资质判断结果",
"cover": "导航、筛选、列表分页、导出、资质判断结果弹窗",
"columns": "产品、资质齐全状态、证照相关列(以现网为准)",
"row_ops": "详情、查看",
"write_ops": ["资质判断结果"],
"sibling_diff": "资质一览查询;勿与「产品资质齐全规则」配置页混淆",
},
{
"pageKey": "prodm-material-chg-detail",
"specId": "PRODM-026",
"name": "物料变更单明细查询",
"prefix": "TP_PRODM_MCD",
"path": "/spd/basic/goods/platform/zrchange",
"ready": "筛选(可展开)、导出、物料变更明细列表与分页",
"cover": "导航、筛选展开、列表分页、导出",
"columns": "变更单号、物料、变更字段、时间等(以现网为准)",
"row_ops": "详情、查看",
"filter_collapsed": True,
"row_action": False,
"sibling_diff": "物料变更明细只读;勿与「变更单明细查询」产品侧混淆",
},
]
for p in PAGES:
p["file"] = f"基础管理-产品管理-{p['name']}-测试点分析-01.md"
SCOPE_OUTSIDE_COMMON = (
"全局左侧菜单与顶栏导航噪声、导出/打印文件字节内容、弹窗/抽屉全字段组合与真实落库、断网/弱网"
)
def load_scan(page_key: str) -> dict:
data = json.loads((SCAN_DIR / f"{page_key}-elements.json").read_text(encoding="utf-8"))
return {"url": data["url"].split("?")[0], "elementCount": data["elementCount"]}
def detect_expand(page_key: str) -> bool:
path = SCAN_DIR / f"{page_key}-elements.json"
if not path.exists():
return False
text = path.read_text(encoding="utf-8")
return bool(re.search(r"展\s*开", text))
def detect_page_size_changer(page_key: str) -> bool:
path = SCAN_DIR / f"{page_key}-elements.json"
if not path.exists():
return False
text = path.read_text(encoding="utf-8")
# 仅认 Ant size-changer,避免「条/页」文案误命中自检/其它区域
return "ant-pagination-options-size-changer" in text
def needs_horizontal_scroll(page_key: str) -> bool:
"""3.2:列超出视口才写。启发式:可排序列≥8 或有列设置(宽表常见)。"""
caps = list_table_caps(page_key)
if caps.get("column_settings"):
return True
return len(sortable_column_labels(page_key)) >= 8
def write_op_section(seq: Seq, action: str) -> str:
"""独立写操作 H3:名称用业务文案;3.2 打开层 + 必填拦截(边界)。"""
return section(
action,
[
(
"功能验证点",
[
seq.add(
"高",
"打开对应弹窗/抽屉",
f"触发「{action}」后弹窗/抽屉可见,标题与关键字段可交互;取消不落库(范围内才写保存)",
),
seq.add("中", "取消不落库", "填写后取消或关闭,列表不新增/不修改对应记录"),
],
),
(
"边界验证点",
[
seq.add("中", "必填为空拦截", "必填项为空时确定被拦截并提示,不落库"),
seq.add("低", "重复打开", "连续两次打开不叠加多层遮罩"),
],
),
(
"异常验证点",
[seq.add("中", "保存失败", "接口失败时弹窗不自动关闭且列表保持原状")],
),
(
"集成验证点",
[seq.add("低", "弹窗字段与列表列一致", "弹窗内关键字段与列表展示字段命名一致")],
),
],
)
def nav_section(p: dict, seq: Seq) -> str:
name = p["name"]
path = p["path"]
return section(
"页面访问与导航",
[
(
"功能验证点",
[
seq.add(
"高",
"页面主区加载完成",
f"携带有效登录态进入 `{path}`(菜单或直链),{p['ready']};左侧菜单与顶栏 Tab 高亮为{name},不误入同组其它产品管理页",
),
seq.add(
"中",
"同组产品管理菜单可切换",
"在 scope 所列产品管理 26 个子菜单中切换 2~3 个后 URL 与内容对应,再返回本页状态正确",
),
],
),
(
"边界验证点",
[
seq.add(
"低",
"已登录直接打开 URL",
f"携带有效登录态打开 `{path}`,筛选区与列表(或左右分栏)可见,无报错遮罩,地址仍为该路径",
)
],
),
(
"异常验证点",
[
"- 无(未登录/会话过期/无菜单权限引用用户管理 `TP_USER_USR_005`~`007`;本模块自动化不单独写「菜单进入」「未登录访问拦截」用例,见 ui-test-point-rules 3.5)",
],
),
(
"集成验证点",
[
seq.add(
"中",
"顶栏院区与筛选默认一致",
"顶栏当前院区与页面院区/库区树默认范围不矛盾(允许按权限裁剪)",
)
],
),
],
)
def sibling_diff_section(seq: Seq, p: dict) -> str:
note = p["sibling_diff"]
return section(
"与同组产品管理页差异",
[
(
"功能验证点",
[
seq.add(
"中",
"本页独有列与能力",
f"{note};与同组它页切换后列集/工具栏差异可辨识",
)
],
),
(
"边界验证点",
[
seq.add(
"低",
"直链不误展示它页列",
f"仅打开本页 URL `{p['path']}` 时不应出现它页独有工具栏(如错页 Tab)",
)
],
),
(
"异常验证点",
[
seq.add(
"中",
"菜单高亮与 URL 一致",
"左侧高亮菜单名、顶栏 Tab 与地址栏路径同属本页,非兄弟页 URL",
)
],
),
(
"集成验证点",
[
seq.add(
"低",
"跨页数据不串行",
"从本页切至它页再返回,本页筛选/列表恢复为本页数据域,不残留它页查询条件(以现网为准)",
)
],
),
],
)
def write_ops_chunks(seq: Seq, p: dict) -> list[str]:
return [write_op_section(seq, op) for op in (p.get("write_ops") or [])]
def quick_filter_32(seq: Seq, *, include_combo: bool) -> str:
"""ui-test-point-rules 3.2 原文:快捷区重置 / 无抽屉时全量可见组合。"""
func = [
seq.add(
"中",
"快捷区重置恢复默认",
"已填字段(及维度勾选)恢复默认;列表是否刷新按产品规则,未定则标待评审",
),
]
if include_combo:
func.insert(
0,
seq.add(
"高",
"全量可见条件组合查询",
"只填当前结果里有值的条件;结果同时满足已填条件",
),
)
return section(
"快捷筛选查询",
[
("功能验证点", func),
(
"边界验证点",
[
seq.add("中", "超长文本关键字", "名称/编号类字段输入较长关键字,查询被接受或截断,页面不崩溃"),
seq.add("低", "仅空格关键字", "按空查询或提示,不报错"),
],
),
("异常验证点", [seq.add("中", "查询接口失败", "失败有错误提示,不白屏")]),
(
"集成验证点",
[seq.add("中", "筛选结果与分页总数一致", "查询后「共 N 条」与本次查询匹配条数一致")],
),
],
)
def advanced_filter_32(seq: Seq) -> str:
return section(
"高级筛选抽屉",
[
(
"功能验证点",
[
seq.add("高", "点击高级筛选打开抽屉", "标题为「高级筛选」;关键字段可交互"),
seq.add("中", "抽屉内重置恢复默认", "抽屉字段恢复默认,快捷区不被清空"),
seq.add(
"高",
"全量查询条件组合查询",
"快捷区 +(有则)维度勾选 + 抽屉全填样本非空字段;抽样首行同时满足全部已填条件",
),
],
),
("边界验证点", none_merged("高级筛选字段边界见快捷区同类规则")),
("异常验证点", none_merged("高级筛选接口失败归入查询接口失败")),
(
"集成验证点",
[
seq.add(
"中",
"高级筛选带入",
"快捷区同名字段带入抽屉且不手改,与快捷区一致(可选,见 rules 3.3)",
)
],
),
],
)
def pagination_32(seq: Seq, *, page_key: str) -> str:
func = [
seq.add("高", "有数据时总条数", "「共 N 条」与本次查询匹配条数一致"),
seq.add("高", "切换至第 2 页", "页码高亮且首行更换;不足 2 页则该点不适用"),
]
if detect_page_size_changer(page_key):
func.append(
seq.add(
"中",
"切换每页条数",
"默认档位(常见 50)→「20 条/页」;共 N 条不变;当前页行数=min(N,20) 且与切换前不同(见 3.7)",
)
)
return section(
"分页",
[
("功能验证点", func),
(
"边界验证点",
[
seq.add("中", "首页上一页不可用", "第 1 页时「上一页」禁用或无效"),
seq.add("中", "末页下一页不可用", "最后一页时「下一页」禁用或无效"),
],
),
("异常验证点", [seq.add("低", "跳至非数字页码", "非法输入不跳转异常页")]),
(
"集成验证点",
[seq.add("中", "翻页不丢失筛选", "带筛选翻页后条件保持,新页仍满足原筛选")],
),
],
)
def list_32(
seq: Seq,
columns: str,
row_ops: str,
*,
page_key: str,
has_tabs: bool = False,
tab_labels: str | None = None,
include_row_action: bool = True,
) -> str:
parts: list[str] = []
if has_tabs:
parts.append(status_tab_section(seq, tab_labels))
caps = list_table_caps(page_key)
func = [seq.add("高", "列表核心列完整", f"主表展示 {columns}")]
if caps["sortable"]:
func.append(
seq.add(
"中",
"各可排序列升序降序",
"每个带排序器的列均可升序、再降序;指示器(▲/▼ 或 aria-sort)与方向一致;列表仍有数据",
)
)
if caps["column_settings"]:
func.append(
seq.add(
"低",
"列设置隐藏后再恢复",
"打开列设置 → 取消勾选非核心列 → 表头隐藏且勾选同步 → 再勾选后表头恢复",
)
)
boundary = [
seq.add(
"中",
"无匹配空列表",
"不存在的关键字查询后表头仍在,0 行或空态;有汇总时为 0 或「-」",
),
]
if needs_horizontal_scroll(page_key):
boundary.append(
seq.add(
"中",
"横向滚动与列对齐",
"远侧列可进入视口;表头列数=数据列数;对齐无重叠。列名以本页为准",
)
)
integration: list[str] = []
if include_row_action and row_ops and row_ops.strip() not in ("—", "-"):
integration.append(
seq.add(
"高",
"行内操作可达",
f"首行存在数据时,行操作 {row_ops} 可点击并进入预期弹窗/详情(只读冒烟)",
)
)
parts.append(
section(
"列表",
[
("功能验证点", func),
("边界验证点", boundary),
("异常验证点", none_merged("列表加载失败见查询接口失败")),
(
"集成验证点",
integration or none_merged("行内操作见行内操作 H3 或本页无行操作"),
),
],
)
)
parts.append(pagination_32(seq, page_key=page_key))
return "\n---\n\n".join(parts)
def export_print_32(seq: Seq, *, page_key: str) -> list[str]:
caps = toolbar_caps(page_key)
chunks: list[str] = []
if caps["export"]:
chunks.append(
section(
"导出",
[
(
"功能验证点",
[
seq.add(
"高",
"当前查询条件下导出",
"先查询再导出;无报错。默认不校验文件内容",
)
],
),
(
"边界验证点",
[seq.add("低", "无数据时导出", "空列表导出不报错或有明确提示")],
),
("异常验证点", none_merged("导出失败见写操作失败提示")),
("集成验证点", none_merged("导入模板与导出关系见写操作点")),
],
)
)
if caps["print"]:
chunks.append(
section(
"打印",
[
(
"功能验证点",
[
seq.add(
"高",
"按当前条件打印",
"能拉起对话框/预览/toast;无 error",
)
],
),
("边界验证点", none_merged("无数据打印以现网为准")),
("异常验证点", none_merged("打印失败见写操作失败提示")),
("集成验证点", none_merged("批量打印见打印 H3")),
],
)
)
if caps["import_excel"]:
chunks.append(
section(
"Excel 导入",
[
(
"功能验证点",
[
seq.add(
"中",
"Excel 导入入口可触发",
"点「Excel导入」/「导入excel」可打开导入流程,无未捕获异常(不校验文件字段级)",
)
],
),
("边界验证点", none_merged("空文件/格式错误以现网提示为准")),
(
"异常验证点",
[seq.add("中", "导入失败提示", "导入失败有明确错误信息,列表不误增")],
),
("集成验证点", none_merged("导入后与列表刷新以现网为准")),
],
)
)
return chunks
def standard_chunks(p: dict, seq: Seq) -> list[str]:
"""H3 顺序对齐 rules 3.1:筛选 → 展开 → 高级筛选 → 列表 → 分页 → 导出/打印 → 写操作。"""
chunks: list[str] = []
if p.get("layout") == "left_tree":
chunks.append(tree_section(seq, tree_label=p.get("tree_label", "分类")))
advanced = page_has_advanced_drawer(p["pageKey"])
chunks.append(quick_filter_32(seq, include_combo=not advanced))
if detect_expand(p["pageKey"]) or p.get("filter_collapsed"):
chunks.append(filter_collapse_section(seq))
if advanced:
chunks.append(advanced_filter_32(seq))
row_ops = p.get("row_ops", "详情")
split_row = row_ops_section(seq, row_ops)
include_row = p.get("row_action", True) and split_row is None
chunks.append(
list_32(
seq,
p.get("columns", "主表核心列(以现网为准)"),
row_ops,
page_key=p["pageKey"],
has_tabs=p.get("has_tabs", False),
tab_labels=p.get("tab_labels"),
include_row_action=include_row,
)
)
if split_row:
chunks.append(split_row)
chunks.extend(export_print_32(seq, page_key=p["pageKey"]))
chunks.extend(write_ops_chunks(seq, p))
return chunks
def quality(p: dict, tp_count: int, el_count: int) -> str:
caps = list_table_caps(p["pageKey"])
sort_note = (
"有 sorter:列表 H3 已写「各可排序列升序降序」;下游按 3.6 每列一条"
if caps["sortable"]
else "页面无列头 sorter(自检)"
)
colset_note = (
"有列设置:列表 H3 已写「列设置隐藏后再恢复」"
if caps["column_settings"]
else "页面无列设置齿轮(自检)"
)
hscroll_note = (
"宽表已写「横向滚动与列对齐」"
if needs_horizontal_scroll(p["pageKey"])
else "页面无宽表启发式,未写横向滚动(自检)"
)
pagesize_note = (
"有每页条数下拉:已写「切换每页条数」"
if detect_page_size_changer(p["pageKey"])
else "页面无 size-changer,未写切换每页条数(自检)"
)
return f"""## 质量自检摘要
| 项 | 结论 |
|----|------|
| 范围 | 在范围内:{p['cover']}。排除:断网/弱网、导出文件内容、弹窗全字段落库、登录/改密通用流程(见 `{SCOPE}`)。未声称 100% 全覆盖。 |
| 范围外未覆盖 | {SCOPE_OUTSIDE_COMMON} |
| 覆盖 | 已写功能域见正文 H3;共 **{tp_count}** 条原子测试点;扫描元素约 {el_count} 项 |
| 分类 | 功能 / 边界 / 异常 / 集成四类均已按功能点拆分 |
| 去重 | 登录态过期、无菜单权限、改密引用 `TP_USER_USR_006`/`007`,本页不展开。编号前缀 `{p['prefix']}` |
| 待评审项 | 写操作冒烟不提交脏数据;筛选收起后字段是否保留;部分详情层标题可能受顶栏 Tab 干扰需现网复核;{sort_note};{colset_note};{hscroll_note};{pagesize_note} |
| 功能点拆分 | 未使用「筛选与查询 / 列表与分页」;快捷与高级筛选分开;有展开则单开查询条件展开收起;列表→分页→导出;写操作 H3 用业务名 |
| 报表能力 | 整页渲染,非统计报表 iframe 第四节清单 |
| 列排序下游 | {sort_note};建议 `audit-sort-columns-live` 维护 `data/spd-admin-sort-columns.json` |
| 分页每页条数 | {pagesize_note} |
| 下游 | 可转 testcases/MD、CSV;扫描 `npm run discover:scan-prodm`;overlay `npm run discover:scan-prodm-overlays`;测试点 `npm run generate:testpoints-prodm` |
"""
def render_testpoint(p: dict, scan: dict) -> tuple[str, int]:
spec = f"UI-SPEC-SPD-{p['specId']}"
seq = Seq(p["prefix"], spec)
nav = nav_section(p, seq)
body = "\n---\n\n".join(standard_chunks(p, seq) + [sibling_diff_section(seq, p)])
md = f"""# 基础管理-产品管理-{p['name']} — 测试点分析
**版本**:01
**定稿日期**:{TODAY}
**关联需求**:`knowledge-base/08-UI专项规范库/{spec}.md`
**页面 URL**:`{scan['url']}`
**元素扫描**:`data/scan/{p['pageKey']}-elements.json`({scan['elementCount']} 项)
**定位器**:`locators/spd/{p['pageKey']}.locator.yaml`
**本次生成范围**:`{SCOPE}`
---
## 基础设置 · 产品管理 · {p['name']}
{nav}
---
{body}
---
{quality(p, seq.n, scan['elementCount'])}
"""
return md, seq.n
def patch_ui_spec(p: dict, tp_file: str) -> None:
path = KB / f"UI-SPEC-SPD-{p['specId']}.md"
if not path.exists():
print(f"WARN missing UI-SPEC {path.name}")
return
text = path.read_text(encoding="utf-8")
text = re.sub(r'update_time: "[^"]*"', f'update_time: "{TODAY}"', text, count=1)
if 'related_tc: ""' in text:
text = text.replace('related_tc: ""', f'related_tc: "testpoint/{tp_file}"', 1)
else:
text = re.sub(r'related_tc: "[^"]*"', f'related_tc: "testpoint/{tp_file}"', text, count=1)
path.write_text(text, encoding="utf-8")
def patch_index(counts: dict[str, int], scans: dict[str, dict]) -> None:
text = INDEX.read_text(encoding="utf-8")
text = re.sub(r'update_time: "[^"]*"', f'update_time: "{TODAY}"', text, count=1)
for p in PAGES:
sid = p["specId"]
el = scans[sid]["elementCount"]
tp_n = counts[sid]
prefix = p["prefix"].replace("TP_", "`TP_") + "`"
row = (
f"| {p['name']} | {sid} | `{p['path']}` | "
f"已扫描 · `{p['pageKey']}` · **{el}** 项 · 测试点 **{tp_n}** 条 · {prefix} |"
)
text = re.sub(
rf"\| {re.escape(p['name'])} \| {sid} \| `{re.escape(p['path'])}` \| .+ \|",
row,
text,
count=1,
)
if "generate:testpoints-prodm" not in text:
text = text.replace(
"npm run discover:scan-prodm-overlays # 真环境补扫弹窗/抽屉/详情定位器 → 合并 JSON/YAML\n",
"npm run discover:scan-prodm-overlays # 真环境补扫弹窗/抽屉/详情定位器 → 合并 JSON/YAML\n"
"npm run generate:testpoints-prodm # 生成产品管理测试点 MD\n",
)
INDEX.write_text(text, encoding="utf-8")
def main() -> None:
filter_keys = set(sys.argv[1:])
pages = [p for p in PAGES if not filter_keys or p["pageKey"] in filter_keys]
if not pages:
raise SystemExit("No pages matched. Available: " + ", ".join(p["pageKey"] for p in PAGES))
OUT_DIR.mkdir(parents=True, exist_ok=True)
counts: dict[str, int] = {}
scans: dict[str, dict] = {}
for p in pages:
scan = load_scan(p["pageKey"])
md, n = render_testpoint(p, scan)
out = OUT_DIR / p["file"]
out.write_text(md, encoding="utf-8")
patch_ui_spec(p, p["file"])
counts[p["specId"]] = n
scans[p["specId"]] = scan
print(f"OK {p['specId']}\t{p['name']}\t{scan['elementCount']} el\t{n} TP\t{out.name}")
for p in PAGES:
if p["specId"] not in scans:
scans[p["specId"]] = load_scan(p["pageKey"])
if p["specId"] not in counts:
tp_file = OUT_DIR / p["file"]
counts[p["specId"]] = (
len(re.findall(r"\*\*TP_[A-Z0-9_]+_\d{3}\*\*", tp_file.read_text(encoding="utf-8")))
if tp_file.exists()
else 0
)
patch_index(counts, scans)
print("done", sum(counts.values()), "TP total across", len(PAGES), "pages")
if __name__ == "__main__":
main()
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
* node scripts/json-to-locator-yaml.mjs --all-new * node scripts/json-to-locator-yaml.mjs --all-new
* node scripts/json-to-locator-yaml.mjs --all-deptm * node scripts/json-to-locator-yaml.mjs --all-deptm
* node scripts/json-to-locator-yaml.mjs --all-iface * node scripts/json-to-locator-yaml.mjs --all-iface
* node scripts/json-to-locator-yaml.mjs --all-prodm
*/ */
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
...@@ -100,6 +101,32 @@ const NEW_PAGES = { ...@@ -100,6 +101,32 @@ const NEW_PAGES = {
'iface-mat-push': { pageTitle: '基础设置 > 接口管理 > 物资字典推送记录', module: 'spd-basic' }, 'iface-mat-push': { pageTitle: '基础设置 > 接口管理 > 物资字典推送记录', module: 'spd-basic' },
'iface-req-query': { pageTitle: '基础设置 > 接口管理 > 请领单接口查询', module: 'spd-basic' }, 'iface-req-query': { pageTitle: '基础设置 > 接口管理 > 请领单接口查询', module: 'spd-basic' },
'iface-campus-code': { pageTitle: '基础设置 > 接口管理 > 院区对码', module: 'spd-basic' }, 'iface-campus-code': { pageTitle: '基础设置 > 接口管理 > 院区对码', module: 'spd-basic' },
'prodm-check-reinit': { pageTitle: '基础设置 > 产品管理 > 产品核对重新发起', module: 'spd-basic' },
'prodm-maintain': { pageTitle: '基础设置 > 产品管理 > 产品维护', module: 'spd-basic' },
'prodm-material': { pageTitle: '基础设置 > 产品管理 > 物料信息管理', module: 'spd-basic' },
'prodm-first-audit': { pageTitle: '基础设置 > 产品管理 > 产品初审核对', module: 'spd-basic' },
'prodm-supply': { pageTitle: '基础设置 > 产品管理 > 产品供应关系维护', module: 'spd-basic' },
'prodm-price-check': { pageTitle: '基础设置 > 产品管理 > 产品物价核对', module: 'spd-basic' },
'prodm-campus': { pageTitle: '基础设置 > 产品管理 > 院区产品设置', module: 'spd-basic' },
'prodm-unit-pkg': { pageTitle: '基础设置 > 产品管理 > 院区单元包设置', module: 'spd-basic' },
'prodm-center-limit-ai': { pageTitle: '基础设置 > 产品管理 > 中心库上下限建议(AI)', module: 'spd-basic' },
'prodm-req-channel': { pageTitle: '基础设置 > 产品管理 > 请领出库渠道设置', module: 'spd-basic' },
'prodm-center-limit': { pageTitle: '基础设置 > 产品管理 > 中心库上下限建议', module: 'spd-basic' },
'prodm-settle': { pageTitle: '基础设置 > 产品管理 > 结算点设置', module: 'spd-basic' },
'prodm-price-adj': { pageTitle: '基础设置 > 产品管理 > 调价管理', module: 'spd-basic' },
'prodm-info-change': { pageTitle: '基础设置 > 产品管理 > 产品信息变更', module: 'spd-basic' },
'prodm-change-detail': { pageTitle: '基础设置 > 产品管理 > 变更单明细查询', module: 'spd-basic' },
'prodm-peripheral-add': { pageTitle: '基础设置 > 产品管理 > 外围系统产品新增', module: 'spd-basic' },
'prodm-peripheral-chg': { pageTitle: '基础设置 > 产品管理 > 外围系统产品变更管理', module: 'spd-basic' },
'prodm-di-query': { pageTitle: '基础设置 > 产品管理 > 产品DI查询', module: 'spd-basic' },
'prodm-stockout': { pageTitle: '基础设置 > 产品管理 > 缺货目录维护', module: 'spd-basic' },
'prodm-center-zone': { pageTitle: '基础设置 > 产品管理 > 中心库库区产品设置', module: 'spd-basic' },
'prodm-stockout-detail': { pageTitle: '基础设置 > 产品管理 > 缺货目录明细', module: 'spd-basic' },
'prodm-material-chg': { pageTitle: '基础设置 > 产品管理 > 物料信息变更', module: 'spd-basic' },
'prodm-di-change': { pageTitle: '基础设置 > 产品管理 > 产品DI变更', module: 'spd-basic' },
'prodm-qual-rule': { pageTitle: '基础设置 > 产品管理 > 产品资质齐全规则', module: 'spd-basic' },
'prodm-qual-overview': { pageTitle: '基础设置 > 产品管理 > 产品资质一览', module: 'spd-basic' },
'prodm-material-chg-detail': { pageTitle: '基础设置 > 产品管理 > 物料变更单明细查询', module: 'spd-basic' },
}; };
const GROUPS = { const GROUPS = {
...@@ -161,6 +188,34 @@ const GROUPS = { ...@@ -161,6 +188,34 @@ const GROUPS = {
'iface-req-query', 'iface-req-query',
'iface-campus-code', 'iface-campus-code',
], ],
'--all-prodm': [
'prodm-check-reinit',
'prodm-maintain',
'prodm-material',
'prodm-first-audit',
'prodm-supply',
'prodm-price-check',
'prodm-campus',
'prodm-unit-pkg',
'prodm-center-limit-ai',
'prodm-req-channel',
'prodm-center-limit',
'prodm-settle',
'prodm-price-adj',
'prodm-info-change',
'prodm-change-detail',
'prodm-peripheral-add',
'prodm-peripheral-chg',
'prodm-di-query',
'prodm-stockout',
'prodm-center-zone',
'prodm-stockout-detail',
'prodm-material-chg',
'prodm-di-change',
'prodm-qual-rule',
'prodm-qual-overview',
'prodm-material-chg-detail',
],
'--all-new': Object.keys(NEW_PAGES), '--all-new': Object.keys(NEW_PAGES),
}; };
...@@ -186,7 +241,7 @@ async function main() { ...@@ -186,7 +241,7 @@ async function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (!args.length) { if (!args.length) {
console.error( console.error(
'Usage: node scripts/json-to-locator-yaml.mjs <json-path|--all-extended|--all-basic|--all-user|--all-admit|--all-vendor|--all-surgery|--all-warehouse|--all-flow|--all-deptm|--all-iface|--all-new>', 'Usage: node scripts/json-to-locator-yaml.mjs <json-path|--all-extended|--all-basic|--all-user|--all-admit|--all-vendor|--all-surgery|--all-warehouse|--all-flow|--all-deptm|--all-iface|--all-prodm|--all-new>',
); );
process.exit(1); process.exit(1);
} }
......
...@@ -253,11 +253,22 @@ const SCAN_SCRIPT = `(() => { ...@@ -253,11 +253,22 @@ const SCAN_SCRIPT = `(() => {
} }
function sectionOf(el) { function sectionOf(el) {
const detail = el.closest('.put-page'); // SPD 自定义详情页(产品详情 / 调价详情 / 审批页等)
if (detail) { const customDetail = el.closest('.detail-spin-page, .approve-page, .xc-layout-tb.xc-page, .put-page');
const title = detail.querySelector('.ant-descriptions-title, .ant-page-header-heading-title'); if (customDetail) {
const t = title ? norm(title.textContent) : ''; const titleEl = customDetail.querySelector(
return t ? 'detail:' + t : 'detail'; '.ant-page-header-heading-title, .ant-descriptions-title, .modal-detail-body .title, h1, h2, h3',
);
let t = titleEl ? norm(titleEl.textContent) : '';
if (!t) {
const tab = [...document.querySelectorAll('.ant-tabs-tab-active')].map((n) => norm(n.textContent)).find(Boolean);
if (tab && /基本信息|供应信息|详情|审批/.test(tab)) t = tab;
}
if (!t) {
const m = (customDetail.innerText || '').match(/单号[::]\\s*\\S+/);
if (m) t = norm(m[0]);
}
return t ? 'detail:' + t.slice(0, 40) : 'detail';
} }
const modal = el.closest('.ant-modal'); const modal = el.closest('.ant-modal');
if (modal) { if (modal) {
...@@ -819,7 +830,18 @@ function buildYaml(scan) { ...@@ -819,7 +830,18 @@ function buildYaml(scan) {
key = 'tab_' + slugify(el.text || baseName); key = 'tab_' + slugify(el.text || baseName);
} }
} }
if (section.includes('panel:高级筛选')) key = `drawer_${key}`; if (section.includes('panel:') || section.includes('::panel:')) {
if (!key.startsWith('drawer_')) key = `drawer_${key}`;
}
if (section.includes('modal:') || section.includes('::modal')) {
if (!key.startsWith('modal_')) key = `modal_${key}`;
}
if (section.includes('detail:') || section.includes('::detail')) {
if (!key.startsWith('detail_')) key = `detail_${key}`;
}
if (section.includes('confirm') || section.endsWith('::confirm')) {
if (!key.startsWith('confirm_')) key = `confirm_${key}`;
}
if (section.includes('pagination')) key = `pagination_${key}`; if (section.includes('pagination')) key = `pagination_${key}`;
let finalKey; let finalKey;
...@@ -849,7 +871,8 @@ function buildYaml(scan) { ...@@ -849,7 +871,8 @@ function buildYaml(scan) {
const rec = el.recommended; const rec = el.recommended;
const pw = toPlaywrightExpr(rec, frameLabel); const pw = toPlaywrightExpr(rec, frameLabel);
lines.push(` ${finalKey}:`); lines.push(` ${finalKey}:`);
lines.push(` description: ${JSON.stringify(el.text || el.label || el.type)}`); // Prefer form-item label over placeholder/text so YAML stays human-readable for filters.
lines.push(` description: ${JSON.stringify(el.label || el.text || el.placeholder || el.type)}`);
lines.push(` element_type: ${el.type}`); lines.push(` element_type: ${el.type}`);
lines.push(` section: ${JSON.stringify(section.replace(/token=[^&]+/g, 'token=${token}'))}`); lines.push(` section: ${JSON.stringify(section.replace(/token=[^&]+/g, 'token=${token}'))}`);
if (frameLabel !== 'main') { if (frameLabel !== 'main') {
...@@ -929,7 +952,11 @@ export function writeYamlFromScanJson(jsonPath, config = {}) { ...@@ -929,7 +952,11 @@ export function writeYamlFromScanJson(jsonPath, config = {}) {
return { yamlOut, elementCount: scan.elementCount }; return { yamlOut, elementCount: scan.elementCount };
} }
async function scanPageOrFrames(page, frameLabel = 'main') { export function elementFingerprint(el) {
return [el.section, el.type, el.id || '', el.role || '', el.label || '', el.text || '', el.placeholder || ''].join('|');
}
export async function scanPageOrFrames(page, frameLabel = 'main') {
const merged = { const merged = {
url: page.url(), url: page.url(),
title: await page.title(), title: await page.title(),
...@@ -1036,13 +1063,15 @@ async function clickQueryIfPresent(reportFrame) { ...@@ -1036,13 +1063,15 @@ async function clickQueryIfPresent(reportFrame) {
} }
function fingerprint(el) { function fingerprint(el) {
return [el.section, el.type, el.id || '', el.role || '', el.label || '', el.text || '', el.placeholder || ''].join('|'); return elementFingerprint(el);
} }
async function closeScanOverlays(page) { async function closeScanOverlays(page) {
for (let i = 0; i < 6; i += 1) { for (let i = 0; i < 6; i += 1) {
const back = page const back = page
.locator('.put-page, .ant-drawer-open, .ant-modal-wrap, .ant-modal') .locator(
'.put-page, .detail-spin-page, .approve-page, .xc-layout-tb.xc-page, .ant-drawer-open, .ant-modal-wrap, .ant-modal',
)
.getByRole('button', { name: /返\s*回|取\s*消|关\s*闭/ }) .getByRole('button', { name: /返\s*回|取\s*消|关\s*闭/ })
.first(); .first();
if (await back.isVisible().catch(() => false)) { if (await back.isVisible().catch(() => false)) {
...@@ -1050,7 +1079,12 @@ async function closeScanOverlays(page) { ...@@ -1050,7 +1079,12 @@ async function closeScanOverlays(page) {
await page.waitForTimeout(400); await page.waitForTimeout(400);
continue; continue;
} }
const backText = page.locator('a,button,span').filter({ hasText: /^返\s*回$/ }).last(); const backText = page
.locator(
'.put-page button, .detail-spin-page button, .approve-page button, .xc-layout-tb.xc-page button, .ant-page-header-back, a,button,span',
)
.filter({ hasText: /返\s*回/ })
.last();
if (await backText.isVisible().catch(() => false)) { if (await backText.isVisible().catch(() => false)) {
await backText.click({ timeout: 4_000, force: true }).catch(() => undefined); await backText.click({ timeout: 4_000, force: true }).catch(() => undefined);
await page.waitForTimeout(400); await page.waitForTimeout(400);
...@@ -1072,9 +1106,11 @@ async function closeScanOverlays(page) { ...@@ -1072,9 +1106,11 @@ async function closeScanOverlays(page) {
continue; continue;
} }
const closedByScript = await page.evaluate(() => { const closedByScript = await page.evaluate(() => {
const layer = document.querySelector('.put-page'); const layer = document.querySelector('.put-page, .detail-spin-page, .approve-page, .xc-layout-tb.xc-page');
if (!layer) return false; if (!layer) return false;
const btn = [...layer.querySelectorAll('button,a,span')].find((el) => /返\s*回|关\s*闭/.test((el.textContent || '').trim())); const btn = [...layer.querySelectorAll('button,a,span')].find((el) =>
/返\s*回|关\s*闭/.test((el.textContent || '').trim()),
);
if (!btn) return false; if (!btn) return false;
btn.click(); btn.click();
return true; return true;
......
#!/usr/bin/env node
/**
* Batch scan: 基础设置 > 产品管理 (UI-SPEC PRODM-001~026).
* 先按菜单点击解析真实 URL,再扫描交互元素,最后由 JSON 提炼 locator YAML。
*
* 输出:
* data/scan/prodm-*-elements.json
* locators/spd/prodm-*.locator.yaml
* data/scan/prodm-menu-urls.json
* data/scan/prodm-scan-summary.json
*
* Usage:
* node scripts/scan-prodm-batch.mjs
* node scripts/scan-prodm-batch.mjs prodm-maintain prodm-price-adj
*
* 只重算 YAML(不重新扫描):
* node scripts/json-to-locator-yaml.mjs --all-prodm
*/
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';
import { runScan, writeYamlFromScanJson } from './scan-interactive-elements.mjs';
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_DIR = path.join(ROOT, 'data/scan');
const LOC_DIR = path.join(ROOT, 'locators/spd');
const URL_MAP_OUT = path.join(OUT_DIR, 'prodm-menu-urls.json');
const SUMMARY_OUT = path.join(OUT_DIR, 'prodm-scan-summary.json');
if (existsSync(path.join(ROOT, '.env'))) {
process.loadEnvFile(path.join(ROOT, '.env'));
}
const COMMON_WAIT = [
'.ant-form-item input',
'.ant-form-item .ant-select',
'.ant-form',
'.ant-table',
'.ant-btn',
'.ant-pagination',
'.ant-tree',
'.ant-list-item',
'.ant-menu-item',
];
function listPhases() {
return [
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
(page) => openNamedButton(page, /新\s*增|添\s*加|导\s*入|导\s*出|批\s*量/, 'toolbarOpened'),
(page) => openRowAction(page, /详\s*情|查\s*看|编\s*辑|日\s*志/, 'rowActionOpened'),
];
}
function leftTreePhases() {
return [
expandFirstTreeNode,
selectLeftFirstItem,
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
(page) => openNamedButton(page, /新\s*增|添\s*加|导\s*入|导\s*出|批\s*量/, 'toolbarOpened'),
(page) => openRowAction(page, /详\s*情|查\s*看|编\s*辑|日\s*志/, 'rowActionOpened'),
];
}
const MENUS = [
{
pageKey: 'prodm-check-reinit',
menu: '产品核对重新发起',
pageTitle: '基础设置 > 产品管理 > 产品核对重新发起',
specId: 'PRODM-001',
skipQueryClick: false,
interactivePhases: [
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
(page) => openNamedButton(page, /批量重新审核|导\s*出/, 'toolbarOpened'),
(page) => openRowAction(page, /编\s*辑/, 'editDialogOpened'),
],
},
{
pageKey: 'prodm-maintain',
menu: '产品维护',
pageTitle: '基础设置 > 产品管理 > 产品维护',
specId: 'PRODM-002',
skipQueryClick: true,
interactivePhases: leftTreePhases(),
},
{
pageKey: 'prodm-material',
menu: '物料信息管理',
pageTitle: '基础设置 > 产品管理 > 物料信息管理',
specId: 'PRODM-003',
skipQueryClick: true,
interactivePhases: leftTreePhases(),
},
{
pageKey: 'prodm-first-audit',
menu: '产品初审核对',
pageTitle: '基础设置 > 产品管理 > 产品初审核对',
specId: 'PRODM-004',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-supply',
menu: '产品供应关系维护',
pageTitle: '基础设置 > 产品管理 > 产品供应关系维护',
specId: 'PRODM-005',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-price-check',
menu: '产品物价核对',
pageTitle: '基础设置 > 产品管理 > 产品物价核对',
specId: 'PRODM-006',
skipQueryClick: false,
interactivePhases: [
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
waitTableAndPager,
(page) => openNamedButton(page, /导\s*出|批\s*量/, 'toolbarOpened'),
(page) => openRowAction(page, /详\s*情|审\s*核|编\s*辑/, 'rowActionOpened'),
],
},
{
pageKey: 'prodm-campus',
menu: '院区产品设置',
pageTitle: '基础设置 > 产品管理 > 院区产品设置',
specId: 'PRODM-007',
skipQueryClick: true,
interactivePhases: leftTreePhases(),
},
{
pageKey: 'prodm-unit-pkg',
menu: '院区单元包设置',
pageTitle: '基础设置 > 产品管理 > 院区单元包设置',
specId: 'PRODM-008',
skipQueryClick: true,
interactivePhases: leftTreePhases(),
},
{
pageKey: 'prodm-center-limit-ai',
menu: '中心库上下限建议(AI)',
pageTitle: '基础设置 > 产品管理 > 中心库上下限建议(AI)',
specId: 'PRODM-009',
skipQueryClick: false,
interactivePhases: [
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
waitTableAndPager,
(page) => openNamedButton(page, /批量调整|导\s*出|新\s*增/, 'toolbarOpened'),
(page) => openRowAction(page, /详\s*情|编\s*辑|日\s*志/, 'rowActionOpened'),
],
},
{
pageKey: 'prodm-req-channel',
menu: '请领出库渠道设置',
pageTitle: '基础设置 > 产品管理 > 请领出库渠道设置',
specId: 'PRODM-010',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-center-limit',
menu: '中心库上下限建议',
pageTitle: '基础设置 > 产品管理 > 中心库上下限建议',
specId: 'PRODM-011',
skipQueryClick: false,
interactivePhases: [
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
waitTableAndPager,
(page) => openNamedButton(page, /批量调整|导\s*出|新\s*增/, 'toolbarOpened'),
(page) => openRowAction(page, /详\s*情|编\s*辑|日\s*志/, 'rowActionOpened'),
],
},
{
pageKey: 'prodm-settle',
menu: '结算点设置',
pageTitle: '基础设置 > 产品管理 > 结算点设置',
specId: 'PRODM-012',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-price-adj',
menu: '调价管理',
pageTitle: '基础设置 > 产品管理 > 调价管理',
specId: 'PRODM-013',
skipQueryClick: false,
interactivePhases: [
expandCollapsedFilters,
openFirstFilterDropdown,
openAdvancedFilter,
(page) => openNamedButton(page, /新\s*增|导入excel|导\s*入/, 'toolbarOpened'),
(page) => openRowAction(page, /详\s*情|编\s*辑|再次调价|打\s*印/, 'rowActionOpened'),
],
},
{
pageKey: 'prodm-info-change',
menu: '产品信息变更',
pageTitle: '基础设置 > 产品管理 > 产品信息变更',
specId: 'PRODM-014',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-change-detail',
menu: '变更单明细查询',
pageTitle: '基础设置 > 产品管理 > 变更单明细查询',
specId: 'PRODM-015',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-peripheral-add',
menu: '外围系统产品新增',
pageTitle: '基础设置 > 产品管理 > 外围系统产品新增',
specId: 'PRODM-016',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-peripheral-chg',
menu: '外围系统产品变更管理',
pageTitle: '基础设置 > 产品管理 > 外围系统产品变更管理',
specId: 'PRODM-017',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-di-query',
menu: '产品DI查询',
pageTitle: '基础设置 > 产品管理 > 产品DI查询',
specId: 'PRODM-018',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-stockout',
menu: '缺货目录维护',
pageTitle: '基础设置 > 产品管理 > 缺货目录维护',
specId: 'PRODM-019',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-center-zone',
menu: '中心库库区产品设置',
pageTitle: '基础设置 > 产品管理 > 中心库库区产品设置',
specId: 'PRODM-020',
skipQueryClick: true,
interactivePhases: leftTreePhases(),
},
{
pageKey: 'prodm-stockout-detail',
menu: '缺货目录明细',
pageTitle: '基础设置 > 产品管理 > 缺货目录明细',
specId: 'PRODM-021',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-material-chg',
menu: '物料信息变更',
pageTitle: '基础设置 > 产品管理 > 物料信息变更',
specId: 'PRODM-022',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-di-change',
menu: '产品DI变更',
pageTitle: '基础设置 > 产品管理 > 产品DI变更',
specId: 'PRODM-023',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-qual-rule',
menu: '产品资质齐全规则',
pageTitle: '基础设置 > 产品管理 > 产品资质齐全规则',
specId: 'PRODM-024',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-qual-overview',
menu: '产品资质一览',
pageTitle: '基础设置 > 产品管理 > 产品资质一览',
specId: 'PRODM-025',
skipQueryClick: false,
interactivePhases: listPhases(),
},
{
pageKey: 'prodm-material-chg-detail',
menu: '物料变更单明细查询',
pageTitle: '基础设置 > 产品管理 > 物料变更单明细查询',
specId: 'PRODM-026',
skipQueryClick: false,
interactivePhases: listPhases(),
},
];
async function closeOverlays(page) {
await page.keyboard.press('Escape').catch(() => undefined);
await page.waitForTimeout(200);
const footerCancel = page
.locator('.ant-modal .ant-modal-footer button, .ant-drawer .ant-drawer-footer button')
.filter({ hasText: /取\s*消|关\s*闭/ })
.last();
if (await footerCancel.isVisible().catch(() => false)) {
await footerCancel.click({ timeout: 8_000 }).catch(() => undefined);
await page.waitForTimeout(400);
}
for (let i = 0; i < 3; i += 1) {
const popCancel = page
.locator('.ant-popconfirm button, .ant-popover-open button, .ant-modal-confirm button')
.filter({ hasText: /取\s*消/ })
.last();
if (await popCancel.isVisible().catch(() => false)) {
await popCancel.click({ timeout: 5_000 }).catch(() => undefined);
await page.waitForTimeout(300);
continue;
}
const modalClose = page.locator('.ant-modal-wrap .ant-modal-close, .ant-drawer-close').last();
if (await modalClose.isVisible().catch(() => false)) {
await modalClose.click({ timeout: 5_000 }).catch(() => undefined);
await page.waitForTimeout(400);
continue;
}
break;
}
}
function firstDataRow(page) {
return page.locator('.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)').first();
}
async function layerOpened(page) {
const modal = await page.locator('.ant-modal, .ant-drawer-open').first().isVisible().catch(() => false);
if (modal) return true;
return page.locator('.put-page').first().isVisible().catch(() => false);
}
async function expandCollapsedFilters(page) {
await closeOverlays(page);
const btn = page.getByRole('button', { name: /展\s*开/ }).first();
if (await btn.isVisible().catch(() => false)) {
await btn.click({ timeout: 8_000 }).catch(() => undefined);
await page.waitForTimeout(800);
return { filterExpanded: true };
}
return { filterExpanded: false, skipRescan: true };
}
async function waitTableAndPager(page) {
await closeOverlays(page);
await page
.locator('.ant-table-tbody tr.ant-table-row, .ant-pagination')
.first()
.waitFor({ state: 'visible', timeout: 20_000 })
.catch(() => undefined);
await page.locator('.ant-pagination').first().scrollIntoViewIfNeeded().catch(() => undefined);
await page.waitForTimeout(800);
const hasPager = await page.locator('.ant-pagination').first().isVisible().catch(() => false);
const hasRows = await page
.locator('.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)')
.first()
.isVisible()
.catch(() => false);
return { tableReady: hasRows, pagerReady: hasPager, skipRescan: !(hasRows || hasPager) };
}
async function openFirstFilterDropdown(page) {
await closeOverlays(page);
const select = page.locator('.ant-form .ant-select, .ant-form-item .ant-select').first();
if (!(await select.isVisible().catch(() => false))) return { dropdownOpened: false, skipRescan: true };
await select.click({ timeout: 10_000 });
await page
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
.first()
.waitFor({ state: 'visible', timeout: 8_000 })
.catch(() => undefined);
await page.waitForTimeout(500);
return { dropdownOpened: true };
}
async function openAdvancedFilter(page) {
await closeOverlays(page);
const btn = page.getByText(/高级筛选/).first();
if (!(await btn.isVisible().catch(() => false))) return { advancedFilterOpened: false, skipRescan: true };
await btn.click({ timeout: 10_000 }).catch(() => undefined);
await page
.locator('.ant-drawer-open, .ant-modal')
.first()
.waitFor({ state: 'visible', timeout: 8_000 })
.catch(() => undefined);
await page.waitForTimeout(600);
return { advancedFilterOpened: true };
}
async function expandFirstTreeNode(page) {
await closeOverlays(page);
const switcher = page.locator('.ant-tree-switcher:not(.ant-tree-switcher-noop)').first();
if (!(await switcher.isVisible().catch(() => false))) return { treeExpanded: false, skipRescan: true };
await switcher.click({ timeout: 8_000 }).catch(() => undefined);
await page.waitForTimeout(700);
return { treeExpanded: true };
}
async function selectLeftFirstItem(page) {
await closeOverlays(page);
const candidates = [
page.locator('.ant-tree .ant-tree-title').first(),
page.locator('.ant-menu-inline .ant-menu-item:not(.ant-menu-item-selected)').first(),
page.locator('.ant-list-item').first(),
page.locator('.ant-table-tbody tr.ant-table-row td').first(),
];
for (const loc of candidates) {
if (await loc.isVisible().catch(() => false)) {
await loc.click({ timeout: 10_000 }).catch(() => undefined);
await page.waitForTimeout(800);
return { leftItemSelected: true };
}
}
return { leftItemSelected: false, skipRescan: true };
}
async function openNamedButton(page, pattern, flag) {
await closeOverlays(page);
const btn = page.getByRole('button', { name: pattern }).first();
if (!(await btn.isVisible().catch(() => false))) return { [flag]: false, skipRescan: true };
await btn.click({ timeout: 12_000, force: true });
await page
.locator('.ant-modal, .ant-drawer-open, .put-page')
.first()
.waitFor({ state: 'visible', timeout: 8_000 })
.catch(() => undefined);
await page.waitForTimeout(600);
const opened = await layerOpened(page);
return { [flag]: opened, skipRescan: !opened };
}
async function openRowAction(page, pattern, flag) {
await closeOverlays(page);
const row = firstDataRow(page);
if (!(await row.isVisible().catch(() => false))) return { [flag]: false, skipRescan: true };
let action = row.getByRole('button', { name: pattern }).first();
if (!(await action.isVisible().catch(() => false))) {
action = row.locator('a,button,span').filter({ hasText: pattern }).first();
}
if (!(await action.isVisible().catch(() => false))) return { [flag]: false, skipRescan: true };
const before = page.url();
await action.click({ timeout: 8_000, force: true });
await page.waitForTimeout(1600);
const moved = page.url() !== before && !page.url().includes('/login');
if (moved) return { [flag]: true, navigated: true, returnUrl: before };
const opened = await layerOpened(page);
return { [flag]: Boolean(opened), skipRescan: !opened };
}
async function ensureAuthStorage() {
mkdirSync(path.dirname(AUTH_STATE), { recursive: true });
const user = process.env.TEST_USER;
const pass = process.env.TEST_PASS;
if (!user || !pass) {
throw new Error('未配置 TEST_USER/TEST_PASS,请复制 .env.example 为 .env 后填写测试账号');
}
const browser = await chromium.launch({ headless: true, channel: 'chrome' });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
locale: 'zh-CN',
storageState: existsSync(AUTH_STATE) ? AUTH_STATE : undefined,
});
const page = await context.newPage();
await page.goto(`${BASE}/spd/home/index`, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.waitForTimeout(2000);
if (!page.url().includes('/login')) {
await context.storageState({ path: AUTH_STATE });
await browser.close();
console.log('Auth state OK:', AUTH_STATE);
return;
}
await page.goto(`${BASE}/spd/login`, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.locator('#username, input[name="username"], input[placeholder*="用户"]').first().fill(user);
await page.locator('#password, input[type="password"]').first().fill(pass);
await Promise.all([
page.waitForURL((url) => url.pathname.includes('/spd/home'), { timeout: 120_000 }),
page.getByRole('button', { name: /登\s*录/ }).click(),
]).catch(async () => {
for (let i = 0; i < 60; i += 1) {
if (page.url().includes('/spd/home')) return;
await page.waitForTimeout(1000);
}
throw new Error(`登录后未进入首页,当前 URL: ${page.url()}`);
});
await page.waitForTimeout(1500);
await context.storageState({ path: AUTH_STATE });
await browser.close();
console.log('Logged in and saved auth:', AUTH_STATE);
}
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function expandGroupMenu(page) {
await page.getByRole('menuitem', { name: '基础设置' }).click({ timeout: 20_000 });
await page.waitForTimeout(500);
const title = page.locator('.ant-menu-submenu-title').filter({ hasText: /^产品管理$/ }).first();
if (await title.isVisible().catch(() => false)) {
const expanded = await title
.locator('xpath=..')
.evaluate((el) => el.classList.contains('ant-menu-submenu-open'))
.catch(() => false);
if (!expanded) await title.click({ timeout: 10_000 });
await page.waitForTimeout(400);
}
}
async function discoverMenuUrls(menus) {
const browser = await chromium.launch({ headless: true, channel: 'chrome' });
const context = await browser.newContext({
storageState: AUTH_STATE,
viewport: { width: 1920, height: 1080 },
locale: 'zh-CN',
});
const page = await context.newPage();
const urls = {};
for (const item of menus) {
await page.goto(`${BASE}/spd/home/index`, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.waitForTimeout(1500);
if (page.url().includes('/login')) {
throw new Error('登录态失效,无法打开产品管理菜单');
}
await expandGroupMenu(page);
await page
.locator('.ant-menu-item')
.filter({ hasText: new RegExp(`^\\s*${escapeRegExp(item.menu)}\\s*$`) })
.first()
.click({ timeout: 20_000 });
await page.waitForTimeout(2000);
const url = page.url().split('?')[0];
if (url.includes('/login') || url.endsWith('/spd/home/index')) {
throw new Error(`未能进入 ${item.menu},当前 URL: ${page.url()}`);
}
urls[item.pageKey] = url;
console.log(`URL ${item.menu}: ${url}`);
}
await browser.close();
mkdirSync(OUT_DIR, { recursive: true });
writeFileSync(URL_MAP_OUT, JSON.stringify({ base: BASE, scannedAt: new Date().toISOString(), urls }, null, 2), 'utf8');
console.log('Wrote URL map:', URL_MAP_OUT);
return urls;
}
function pageCfg(targetUrl, item) {
return {
targetUrl,
useIframe: false,
iframeMatch: '',
iframeLabel: 'main',
module: 'spd-basic',
pageKey: item.pageKey,
pageTitle: item.pageTitle,
jsonOut: path.join(OUT_DIR, `${item.pageKey}-elements.json`),
yamlOut: path.join(LOC_DIR, `${item.pageKey}.locator.yaml`),
jsonSourceName: `${item.pageKey}-elements.json`,
waitSelectors: COMMON_WAIT,
waitSelector: '.ant-form-item input, .ant-table, .ant-btn, .ant-form, .ant-tree, .ant-list-item',
waitUntil: 'domcontentloaded',
expandFirstRow: false,
openAdvancedFilter: false,
skipQueryClick: item.skipQueryClick === true,
interactivePhases: item.interactivePhases,
};
}
async function main() {
const filter = process.argv.slice(2);
const menus = filter.length ? MENUS.filter((p) => filter.includes(p.pageKey)) : MENUS;
if (!menus.length) {
console.error('No pages matched. Available:', MENUS.map((p) => p.pageKey).join(', '));
process.exit(1);
}
await ensureAuthStorage();
const urls = await discoverMenuUrls(menus);
const summary = [];
for (const item of menus) {
const targetUrl = urls[item.pageKey];
console.log(`\n=== Scanning ${item.pageKey} ${targetUrl} ===`);
try {
const result = await runScan(pageCfg(targetUrl, item));
const yaml = writeYamlFromScanJson(result.jsonOut, {
pageKey: item.pageKey,
pageTitle: item.pageTitle,
module: 'spd-basic',
jsonSourceName: `${item.pageKey}-elements.json`,
iframeMatch: '',
yamlOut: path.join(LOC_DIR, `${item.pageKey}.locator.yaml`),
});
summary.push({
page: item.pageKey,
menu: item.menu,
specId: item.specId,
ok: true,
url: targetUrl,
elementCount: result.elementCount,
jsonOut: result.jsonOut,
yamlOut: yaml.yamlOut,
});
} catch (err) {
console.error(`FAILED ${item.pageKey}:`, err);
summary.push({
page: item.pageKey,
menu: item.menu,
specId: item.specId,
ok: false,
error: String(err),
});
}
}
console.log('\n========== SUMMARY ==========');
for (const row of summary) {
if (row.ok) {
console.log(`✓ ${row.page}: ${row.elementCount} elements ${row.url}`);
console.log(` JSON: ${row.jsonOut}`);
console.log(` YAML: ${row.yamlOut}`);
} else {
console.log(`✗ ${row.page}: ${row.error}`);
}
}
mkdirSync(OUT_DIR, { recursive: true });
writeFileSync(SUMMARY_OUT, JSON.stringify(summary, null, 2), 'utf8');
if (summary.some((r) => !r.ok)) process.exit(1);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
#!/usr/bin/env node
/**
* 真环境打开产品管理弹窗/抽屉/详情页,扫描交互元素并合并进既有 JSON / YAML。
*
* 输出:
* data/scan/prodm-*-elements.json (追加 overlay 元素)
* locators/spd/prodm-*.locator.yaml (重生成)
* data/scan/prodm-overlay-locator-scan.json
*
* Usage:
* node scripts/scan-prodm-overlays-locators.mjs
* node scripts/scan-prodm-overlays-locators.mjs prodm-maintain prodm-campus
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
import {
PAGES,
closeOverlays,
findAction,
preparePage,
waitLayer,
} from './capture-prodm-overlays.mjs';
import {
elementFingerprint,
scanPageOrFrames,
writeYamlFromScanJson,
} from './scan-interactive-elements.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const AUTH_STATE = path.join(ROOT, 'auth/user.json');
const OUT_DIR = path.join(ROOT, 'data/scan');
const LOC_DIR = path.join(ROOT, 'locators/spd');
const OUT_SUMMARY = path.join(OUT_DIR, 'prodm-overlay-locator-scan.json');
if (existsSync(path.join(ROOT, '.env'))) {
process.loadEnvFile(path.join(ROOT, '.env'));
}
const PAGE_TITLES = Object.fromEntries(
PAGES.map((p) => [p.pageKey, `基础设置 > 产品管理 > ${p.menu}`]),
);
function isOverlaySection(section = '') {
return /(^|::)(modal|drawer|panel|detail|confirm)(:|$)/.test(section);
}
function stripPriorOverlayElements(elements) {
return elements.filter((el) => !isOverlaySection(el.section || ''));
}
async function openAndScan(page, locator, labelHint) {
if (!locator || !(await locator.count().catch(() => 0))) {
return { ok: false, reason: 'not-visible', label: labelHint, added: 0 };
}
const box = await locator.boundingBox().catch(() => null);
if (!box || box.width <= 0 || box.height <= 0) {
return { ok: false, reason: 'not-visible', label: labelHint, added: 0 };
}
if (await locator.isDisabled().catch(() => false)) {
return { ok: false, reason: 'disabled', label: labelHint, added: 0 };
}
const text = ((await locator.innerText().catch(() => '')) || labelHint).replace(/\s+/g, '');
if (/^导出|^导出Excel|^打印|^打印产品码$/i.test(text)) {
return { ok: false, reason: 'skip-export-print', label: text, added: 0 };
}
const beforeUrl = page.url();
await locator.click({ timeout: 10_000, force: true }).catch(async () => {
await locator.click({ timeout: 10_000 });
});
await page.waitForTimeout(1500);
const layer = await waitLayer(page, 8_000);
const navigated = page.url() !== beforeUrl && !page.url().includes('/login');
const hasDetailUi = await page.evaluate(
() =>
!!document.querySelector('.detail-spin-page, .approve-page, .put-page, .modal-detail-body') &&
/返\s*回/.test(document.body.innerText || ''),
);
if (!layer.kind && !navigated && !hasDetailUi) {
const pop = page.locator('.ant-popconfirm, .ant-popover-open, .ant-modal-confirm').first();
if (!(await pop.isVisible().catch(() => false))) {
await closeOverlays(page);
return { ok: false, reason: 'no-overlay', label: text, added: 0 };
}
}
const kind = layer.kind || (navigated || hasDetailUi ? 'detail' : 'overlay');
const title = layer.title || text;
const snap = await scanPageOrFrames(page);
const overlayEls = snap.elements
.filter((el) => isOverlaySection(el.section || ''))
.map((el) => ({
...el,
overlayTrigger: text,
overlayKind: kind,
overlayTitle: title,
}));
if (navigated) {
await page.goto(beforeUrl, { waitUntil: 'domcontentloaded', timeout: 120_000 }).catch(() => undefined);
await page.waitForTimeout(1500);
} else {
await closeOverlays(page);
const stillDetail = await page.evaluate(
() =>
!!document.querySelector('.detail-spin-page, .approve-page') &&
/返\s*回/.test(document.body.innerText || ''),
);
if (stillDetail) {
await page.goto(beforeUrl, { waitUntil: 'domcontentloaded', timeout: 120_000 }).catch(() => undefined);
await page.waitForTimeout(1500);
}
}
return {
ok: true,
kind,
title,
label: text,
elements: overlayEls,
added: overlayEls.length,
};
}
async function scanPageOverlays(browser, item) {
const jsonPath = path.join(OUT_DIR, `${item.pageKey}-elements.json`);
const yamlPath = path.join(LOC_DIR, `${item.pageKey}.locator.yaml`);
const result = {
pageKey: item.pageKey,
menu: item.menu,
path: item.path,
overlays: [],
skipped: [],
errors: [],
beforeCount: 0,
afterCount: 0,
addedCount: 0,
jsonOut: jsonPath,
yamlOut: yamlPath,
};
if (!existsSync(jsonPath)) {
result.errors.push(`missing base scan JSON: ${jsonPath}`);
return result;
}
const scan = JSON.parse(readFileSync(jsonPath, 'utf8'));
result.beforeCount = scan.elementCount || (scan.elements || []).length;
// 去掉旧 overlay 段,避免重复堆积;保留主界面元素
scan.elements = stripPriorOverlayElements(scan.elements || []);
const seen = new Set(scan.elements.map(elementFingerprint));
scan.overlayScans = [];
const context = await browser.newContext({
storageState: AUTH_STATE,
viewport: { width: 1920, height: 1080 },
locale: 'zh-CN',
});
const page = await context.newPage();
try {
await preparePage(page, item);
const actions = [
...item.toolbar.map((pattern) => ({ pattern, scope: 'page' })),
{ pattern: /高级筛选/, scope: 'page' },
...item.row.map((pattern) => ({ pattern, scope: 'row' })),
];
for (const { pattern, scope } of actions) {
await closeOverlays(page);
if (!(await page.locator('.ant-table, .ant-form, .ant-tree').first().isVisible().catch(() => false))) {
await preparePage(page, item);
}
const loc = await findAction(page, pattern, scope);
const r = await openAndScan(page, loc, String(pattern));
if (!r.ok) {
result.skipped.push(r);
continue;
}
let added = 0;
for (const el of r.elements || []) {
const key = elementFingerprint(el);
if (seen.has(key)) continue;
seen.add(key);
scan.elements.push(el);
added += 1;
}
const entry = {
ok: true,
kind: r.kind,
title: r.title,
label: r.label,
scanned: r.added,
added,
};
result.overlays.push(entry);
scan.overlayScans.push(entry);
}
} catch (err) {
result.errors.push(String(err));
}
scan.elements.forEach((el, i) => {
el.index = i + 1;
});
scan.elementCount = scan.elements.length;
scan.overlayLocatorScannedAt = new Date().toISOString();
result.afterCount = scan.elementCount;
result.addedCount = result.overlays.reduce((n, o) => n + (o.added || 0), 0);
writeFileSync(jsonPath, JSON.stringify(scan, null, 2), 'utf8');
writeYamlFromScanJson(jsonPath, {
pageKey: item.pageKey,
pageTitle: PAGE_TITLES[item.pageKey] || item.menu,
module: 'spd-basic',
jsonSourceName: `${item.pageKey}-elements.json`,
iframeMatch: '',
yamlOut: yamlPath,
});
await context.close();
return result;
}
async function main() {
if (!existsSync(AUTH_STATE)) throw new Error(`missing ${AUTH_STATE}`);
const filter = process.argv.slice(2);
const pages = filter.length ? PAGES.filter((p) => filter.includes(p.pageKey)) : PAGES;
if (!pages.length) {
console.error('No pages matched:', PAGES.map((p) => p.pageKey).join(', '));
process.exit(1);
}
mkdirSync(OUT_DIR, { recursive: true });
mkdirSync(LOC_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true, channel: 'chrome' });
const summary = [];
for (const item of pages) {
process.stdout.write(`Overlay-locators ${item.pageKey} ... `);
const row = await scanPageOverlays(browser, item);
summary.push(row);
console.log(
`+${row.addedCount} els overlays=${row.overlays.length} skipped=${row.skipped.length} errors=${row.errors.length} total=${row.afterCount}`,
);
for (const o of row.overlays) {
console.log(` + ${o.kind}: ${o.title || o.label} scanned=${o.scanned} added=${o.added}`);
}
for (const e of row.errors) console.log(` ! ${e}`);
}
await browser.close();
writeFileSync(
OUT_SUMMARY,
JSON.stringify({ scannedAt: new Date().toISOString(), summary }, null, 2),
'utf8',
);
const totalAdded = summary.reduce((n, s) => n + (s.addedCount || 0), 0);
const okPages = summary.filter((s) => !s.errors.length).length;
console.log('\n========== OVERLAY LOCATOR SUMMARY ==========');
console.log(`pages=${summary.length} ok=${okPages} overlayElementsAdded=${totalAdded}`);
console.log('Wrote', OUT_SUMMARY);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
#!/usr/bin/env python3 #!/usr/bin/env python3
"""将 docs/ 下页面截图同步至 knowledge-base/08-UI专项规范库(统计报表、准入、厂商、用户、库房、流程、科室管理、接口管理等)。""" """将 docs/ 下页面截图同步至 knowledge-base/08-UI专项规范库(统计报表、准入、厂商、产品管理、用户、库房、流程、科室管理、接口管理等)。"""
from __future__ import annotations from __future__ import annotations
...@@ -383,8 +383,10 @@ USER_PAGES: list[dict] = [ ...@@ -383,8 +383,10 @@ USER_PAGES: list[dict] = [
( (
"产品管理", "产品管理",
"产品核对重新发起、产品维护、物料信息管理、产品初审核对、产品供应关系维护、产品物价核对、" "产品核对重新发起、产品维护、物料信息管理、产品初审核对、产品供应关系维护、产品物价核对、"
"院区产品设置、院区单元包设置、中心库上下限建议(AI)、请领出库渠道设置、结算点设置、" "院区产品设置、院区单元包设置、中心库上下限建议(AI)、请领出库渠道设置、中心库上下限建议、"
"调价管理、产品信息变更、变更单明细查询、外围系统产品新增、外围系统产品变更管理、产品查询 等", "结算点设置、调价管理、产品信息变更、变更单明细查询、外围系统产品新增、外围系统产品变更管理、"
"产品DI查询、缺货目录维护、中心库库区产品设置、缺货目录明细、物料信息变更、产品DI变更、"
"产品资质齐全规则、产品资质一览、物料变更单明细查询",
), ),
], ],
}, },
...@@ -624,6 +626,309 @@ FLOW_PAGES: list[dict] = [ ...@@ -624,6 +626,309 @@ FLOW_PAGES: list[dict] = [
}, },
] ]
# 基础设置 → 产品管理(docs/基础管理-产品管理-*;菜单父 path=/goods)
PRODM_PAGES: list[dict] = [
{
"id": "PRODM-001",
"name": "产品核对重新发起",
"folder": "基础管理-产品管理-产品核对重新发起",
"path": "/spd/basic/goods/paltform/product/check",
"scan_key": "prodm-check-reinit",
"filters": "物料名称、规格型号、器械注册人、器械注册证号、耗材类型、默认销售商、新增日期、是否收费、医保编码(标准/20位/27位)",
"status_tabs": "全部、初审驳回、物价驳回(以截图计数为准)",
"toolbar": "**批量重新审核**、**导出**;列设置",
"list_note": "列含序号、物料编号、是否植入、物料名称、器械注册人、规格型号、单位、耗材类型、销售商;行操作 **编辑**;支持行勾选",
},
{
"id": "PRODM-002",
"name": "产品维护",
"folder": "基础管理-产品管理-产品维护",
"path": "/spd/basic/goods/paltform/product/calatog",
"scan_key": "prodm-maintain",
"filters": "物料名称、物料编号、器械注册人、价格区间;**高级筛选**",
"status_tabs": "无独立状态 Tab",
"toolbar": "**添加**、**导出**、**导入扩展属性**(提示:导入模板以本页导出表为准);列设置",
"list_note": (
"左右分栏:左侧耗材分类树(可搜分类、**新增分类**),右侧产品列表;"
"列含 HIS编码、阳采ID、规格型号、器械注册人、单位、耗材类型、品牌、器械注册证号;行操作 **详情**"
),
},
{
"id": "PRODM-003",
"name": "物料信息管理",
"folder": "基础管理-产品管理-物料信息管理",
"path": "/spd/basic/goods/paltform/product/zrcalatog",
"scan_key": "prodm-material",
"filters": "物料名称/编号、器械注册人、价格区间;**高级筛选**",
"status_tabs": "无独立状态 Tab",
"toolbar": "**添加**、**导出**、**导入扩展属性**、**导入Excel获取望海信息**;列设置",
"list_note": (
"左右分栏:左侧耗材分类树,右侧物料列表;"
"列含 HIS编码、阳采ID、规格型号、单位、耗材类型、品牌、医保相关编码/通用名;行操作 **详情**"
),
},
{
"id": "PRODM-004",
"name": "产品初审核对",
"folder": "基础管理-产品管理-产品初审核对",
"path": "/spd/basic/goods/paltform/product/first",
"scan_key": "prodm-first-audit",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品初审核对列表与行内审核/详情操作以截图为准",
},
{
"id": "PRODM-005",
"name": "产品供应关系维护",
"folder": "基础管理-产品管理-产品供应关系维护",
"path": "/spd/basic/goods/paltform/purchases/calatog",
"scan_key": "prodm-supply",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品与供应商/销售商供应关系列表与维护操作以截图为准",
},
{
"id": "PRODM-006",
"name": "产品物价核对",
"folder": "基础管理-产品管理-产品物价核对",
"path": "/spd/basic/goods/paltform/product/priceCheck",
"scan_key": "prodm-price-check",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品物价核对列表与核对/驳回相关操作以截图为准",
},
{
"id": "PRODM-007",
"name": "院区产品设置",
"folder": "基础管理-产品管理-院区产品设置",
"path": "/spd/basic/goods/paltform/area/calatog",
"scan_key": "prodm-campus",
"filters": "物料名称/编号、耗材类型、默认供应商",
"status_tabs": "无独立状态 Tab",
"toolbar": "**导入医院产品**、**一键导入**、**导出Excel**、**打印产品码**、**下发科室**、**批量调整**;列设置",
"list_note": (
"左右分栏:左侧请领下发库区树(院区→库区),右侧当前库区产品列表;"
"列含规格型号、耗材类型、默认上架货位、单位、库存上限/下限、是否维护单元含量、状态;"
"行操作 **详情**、**编辑**、**日志**;支持行勾选"
),
},
{
"id": "PRODM-008",
"name": "院区单元包设置",
"folder": "基础管理-产品管理-院区单元包设置",
"path": "/spd/basic/goods/paltform/area/unit",
"scan_key": "prodm-unit-pkg",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "院区单元包配置列表与维护操作以截图为准",
},
{
"id": "PRODM-009",
"name": "中心库上下限建议(AI)",
"folder": "基础管理-产品管理-中心库上下限建议(AI)",
"path": "/spd/basic/goods/paltform/product/centerLimitAi",
"scan_key": "prodm-center-limit-ai",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "AI 上下限建议列表与采纳/维护操作以截图为准;勿与「中心库上下限建议」页混淆",
},
{
"id": "PRODM-010",
"name": "请领出库渠道设置",
"folder": "基础管理-产品管理-请领出库渠道设置",
"path": "/spd/basic/goods/platform/apply/rules",
"scan_key": "prodm-req-channel",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "请领出库渠道规则列表与维护操作以截图为准",
},
{
"id": "PRODM-011",
"name": "中心库上下限建议",
"folder": "基础管理-产品管理-中心库上下限建议",
"path": "/spd/basic/goods/paltform/product/centerLimit",
"scan_key": "prodm-center-limit",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "中心库上下限建议列表与采纳/维护操作以截图为准;勿与「中心库上下限建议(AI)」页混淆",
},
{
"id": "PRODM-012",
"name": "结算点设置",
"folder": "基础管理-产品管理-结算点设置",
"path": "/spd/basic/goods/platform/settle/rules",
"scan_key": "prodm-settle",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "结算点规则列表与维护操作以截图为准",
},
{
"id": "PRODM-013",
"name": "调价管理",
"folder": "基础管理-产品管理-调价管理",
"path": "/spd/basic/goods/platform/price",
"scan_key": "prodm-price-adj",
"filters": "物料名称(编号/名称/拼音)、制单时间、销售商;**高级筛选**",
"status_tabs": "无独立状态 Tab(状态在列表列:已生效/暂存/驳回/待审核等)",
"toolbar": "**新增**、**导入excel**;列设置",
"list_note": (
"列含序号、单号、制单时间、计划生效时间、实际生效时间、备注、状态、销售商、供应商;"
"行操作 **再次调价**、**详情**、**打印**、**编辑**(视状态)"
),
},
{
"id": "PRODM-014",
"name": "产品信息变更",
"folder": "基础管理-产品管理-产品信息变更",
"path": "/spd/basic/goods/platform/product-info",
"scan_key": "prodm-info-change",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品信息变更单列表与申请/审批相关操作以截图为准",
},
{
"id": "PRODM-015",
"name": "变更单明细查询",
"folder": "基础管理-产品管理-变更单明细查询",
"path": "/spd/basic/goods/platform/change",
"scan_key": "prodm-change-detail",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "变更单明细列表与详情操作以截图为准;勿与「物料变更单明细查询」混淆",
},
{
"id": "PRODM-016",
"name": "外围系统产品新增",
"folder": "基础管理-产品管理-外围系统产品新增",
"path": "/spd/basic/goods/interface/peripheralSystem",
"scan_key": "prodm-peripheral-add",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "外围系统产品新增列表与处理操作以截图为准;勿与接口管理「外围产品变更」混淆",
},
{
"id": "PRODM-017",
"name": "外围系统产品变更管理",
"folder": "基础管理-产品管理-外围系统产品变更管理",
"path": "/spd/basic/goods/interface/peripheralChange",
"scan_key": "prodm-peripheral-chg",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "外围系统产品变更管理列表与处理操作以截图为准;勿与接口管理「外围产品变更」混淆",
},
{
"id": "PRODM-018",
"name": "产品DI查询",
"folder": "基础管理-产品管理-产品DI查询",
"path": "/spd/basic/goods/platform/diSearch",
"scan_key": "prodm-di-query",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品 DI 查询列表与详情以截图为准;勿与「产品DI变更」混淆",
},
{
"id": "PRODM-019",
"name": "缺货目录维护",
"folder": "基础管理-产品管理-缺货目录维护",
"path": "/spd/basic/goods/paltform/stockoutManagement",
"scan_key": "prodm-stockout",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "缺货目录维护列表与增删改操作以截图为准;勿与「缺货目录明细」混淆",
},
{
"id": "PRODM-020",
"name": "中心库库区产品设置",
"folder": "基础管理-产品管理-中心库库区产品设置",
"path": "/spd/basic/goods/platform/area/product",
"scan_key": "prodm-center-zone",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "中心库库区与产品绑定列表以截图为准;勿与「院区产品设置」「科室库区产品设置」混淆",
},
{
"id": "PRODM-021",
"name": "缺货目录明细",
"folder": "基础管理-产品管理-缺货目录明细",
"path": "/spd/basic/goods/paltform/stockoutList",
"scan_key": "prodm-stockout-detail",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "缺货目录明细查询列表以截图为准;勿与「缺货目录维护」混淆",
},
{
"id": "PRODM-022",
"name": "物料信息变更",
"folder": "基础管理-产品管理-物料信息变更",
"path": "/spd/basic/goods/platform/zr-product-info",
"scan_key": "prodm-material-chg",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "物料信息变更单列表与申请/审批相关操作以截图为准;勿与「产品信息变更」混淆",
},
{
"id": "PRODM-023",
"name": "产品DI变更",
"folder": "基础管理-产品管理-产品DI变更",
"path": "/spd/basic/goods/platform/dichange",
"scan_key": "prodm-di-change",
"filters": "以截图为准",
"status_tabs": "以截图状态 Tab 为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品 DI 变更单列表与维护操作以截图为准;勿与「产品DI查询」混淆",
},
{
"id": "PRODM-024",
"name": "产品资质齐全规则",
"folder": "基础管理-产品管理-产品资质齐全规则",
"path": "/spd/basic/goods/product/rules",
"scan_key": "prodm-qual-rule",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品资质齐全规则配置列表与维护操作以截图为准",
},
{
"id": "PRODM-025",
"name": "产品资质一览",
"folder": "基础管理-产品管理-产品资质一览",
"path": "/spd/basic/goods/product/overview",
"scan_key": "prodm-qual-overview",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "产品资质一览列表与详情以截图为准",
},
{
"id": "PRODM-026",
"name": "物料变更单明细查询",
"folder": "基础管理-产品管理-物料变更单明细查询",
"path": "/spd/basic/goods/platform/zrchange",
"scan_key": "prodm-material-chg-detail",
"filters": "以截图为准",
"status_tabs": "以截图为准(若有)",
"toolbar": "以截图工具栏为准",
"list_note": "物料变更单明细列表与详情以截图为准;勿与「变更单明细查询」混淆",
},
]
# 基础设置 → 科室管理(docs/基础管理-科室管理-*) # 基础设置 → 科室管理(docs/基础管理-科室管理-*)
DEPTM_PAGES: list[dict] = [ DEPTM_PAGES: list[dict] = [
{ {
...@@ -891,6 +1196,7 @@ PAGES: list[dict] = ( ...@@ -891,6 +1196,7 @@ PAGES: list[dict] = (
STAT_PAGES STAT_PAGES
+ ADMIT_PAGES + ADMIT_PAGES
+ VENDOR_PAGES + VENDOR_PAGES
+ PRODM_PAGES
+ USER_PAGES + USER_PAGES
+ WH_PAGES + WH_PAGES
+ FLOW_PAGES + FLOW_PAGES
...@@ -903,6 +1209,99 @@ def png_files(folder: Path) -> list[Path]: ...@@ -903,6 +1209,99 @@ def png_files(folder: Path) -> list[Path]:
return sorted(p for p in folder.iterdir() if p.suffix.lower() == ".png") return sorted(p for p in folder.iterdir() if p.suffix.lower() == ".png")
def render_prodm_spec(page: dict, spec_id: str) -> str:
title = f"产品管理-{page['name']}页面说明"
menu = f"**基础设置** → **产品管理** → **{page['name']}**"
docs_path = f"docs/{page['folder']}/"
kb_dir = f"UI-SPEC-SPD-{spec_id}"
scan_key = page.get("scan_key", "")
page_url = (
f"`http://spdtest.cmic.com.cn:8080{page['path']}`"
if page.get("path")
else f"待元素扫描补充(菜单名:`{page['name']}`)"
)
scan_row = (
f"| 元素扫描 JSON | `data/scan/{scan_key}-elements.json` |\n| 定位器 YAML | `locators/spd/{scan_key}.locator.yaml` |\n"
if scan_key
else ""
)
extra_imgs = ""
src_dir = DOCS / page["folder"]
if src_dir.exists():
n_extra = max(0, len(png_files(src_dir)) - 1)
for i in range(2, n_extra + 2):
extra_imgs += f"\n\n![{page['name']}-补充截图{i - 1}]({kb_dir}/page-main-{i}.png)\n"
return f"""---
title: {title}
module: 基础设置
status: 生效
related_br: ""
related_api: ""
related_jrn: ""
related_tc: ""
related_def: ""
create_time: "{TODAY}"
update_time: "{TODAY}"
author: ""
---
# 产品管理 - {page['name']}
> 来源:`{docs_path}` 页面截图。用于 UI 自动化定位、功能测试与产品主数据/核对/变更验收参考。
> 同模块索引见 [`INDEX.md`](./INDEX.md)。
## 文档与截图
| 项 | 路径 |
|----|------|
| 截图原件 | `{docs_path}` |
| 知识库副本 | `{kb_dir}/page-main.png` |
## 页面路径
{menu}
| 项 | 值 |
|----|-----|
| 主框架 URL | {page_url} |
| iframe 选择器 | 无(整页渲染,非 iframe 报表) |
{scan_row}| 测试环境示例 | `http://spdtest.cmic.com.cn:8080/spd/form/` |
## 主界面
![{page['name']}主界面]({kb_dir}/page-main.png)
{extra_imgs}
### 筛选区
- 字段:{page.get('filters', '以截图为准')}
- 操作:**查询**、**重置**;部分页面含 **收起/展开**、**高级筛选**
### 状态 Tab(若有)
- {page.get('status_tabs', '以截图为准')}
### 列表与工具栏
- {page.get('toolbar', '以截图工具栏为准')}
- {page.get('list_note', '主表列与行内操作以截图为准')}
- 底部分页:「共 N 条」、页码与每页条数切换
## 自动化建议
1. 完成专项元素扫描后,补充 `data/scan/`、`locators/spd/` 与 Page Object。
2. 菜单导航:`page.getByRole('menuitem', {{ name: '{page['name']}', exact: true }}).click()`(注意与同组近似菜单区分,如「中心库上下限建议」与「中心库上下限建议(AI)」、产品/物料变更明细)。
3. 左右分栏页(产品维护、物料信息管理、院区产品设置等)先选中左侧分类/库区树节点,再断言右侧列表范围。
## 测试关注点
1. 筛选 + 高级筛选组合后,列表条数与底部分页「共 N 条」一致。
2. 分类树/院区库区树切换后,右侧列表数据范围与所选节点一致,不串用上一节点数据。
3. 核对/调价/变更/导入类写操作与当前勾选行、筛选条件一致;失败时有明确提示。
4. 同组易混页面(产品 vs 物料、DI 查询 vs 变更、缺货维护 vs 明细、外围系统 vs 接口管理外围产品)URL 与菜单高亮须对应,勿串页断言。
"""
def render_deptm_spec(page: dict, spec_id: str) -> str: def render_deptm_spec(page: dict, spec_id: str) -> str:
title = f"科室管理-{page['name']}页面说明" title = f"科室管理-{page['name']}页面说明"
menu = f"**基础设置** → **科室管理** → **{page['name']}**" menu = f"**基础设置** → **科室管理** → **{page['name']}**"
...@@ -1550,6 +1949,8 @@ author: "" ...@@ -1550,6 +1949,8 @@ author: ""
def render_spec(page: dict, spec_id: str) -> str: def render_spec(page: dict, spec_id: str) -> str:
if spec_id.startswith("IFACE-"): if spec_id.startswith("IFACE-"):
return render_iface_spec(page, spec_id) return render_iface_spec(page, spec_id)
if spec_id.startswith("PRODM-"):
return render_prodm_spec(page, spec_id)
if spec_id.startswith("DEPTM-"): if spec_id.startswith("DEPTM-"):
return render_deptm_spec(page, spec_id) return render_deptm_spec(page, spec_id)
if spec_id.startswith("ADMIT-"): if spec_id.startswith("ADMIT-"):
......
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-CLM-001,基础设置 · 产品管理,中心库上下限建议 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-CLM-002,基础设置 · 产品管理,中心库上下限建议 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-CLM-003,基础设置 · 产品管理,中心库上下限建议 · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-CLM-004,基础设置 · 产品管理,中心库上下限建议 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):建议上下限、现上下限、状态等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-CLM-005,基础设置 · 产品管理,中心库上下限建议 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-CLM-006,基础设置 · 产品管理,中心库上下限建议 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-CLM-007,基础设置 · 产品管理,中心库上下限建议 · 行内操作,行内「调整」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「调整」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「调整」可达;关闭后回到列表。,
PRODM-CLM-008,基础设置 · 产品管理,中心库上下限建议 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-CLM-009,基础设置 · 产品管理,中心库上下限建议 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-CLM-010,基础设置 · 产品管理,中心库上下限建议 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-CLM-011,基础设置 · 产品管理,中心库上下限建议 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-CLM-012,基础设置 · 产品管理,中心库上下限建议 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-CLM-013,基础设置 · 产品管理,中心库上下限建议 · 打印,按当前条件打印,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 点击「打 印」或「打印产品码」等页面级打印入口
3. 断言可拉起对话框/预览/toast,无 error",打印流程可启动。,
PRODM-CLM-014,基础设置 · 产品管理,中心库上下限建议 · 全部调整,打开「全部调整」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 点击「全部调整」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「全部调整」层打开;关键控件可见。,
PRODM-CLM-015,基础设置 · 产品管理,中心库上下限建议 · 全部调整,「全部调整」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 打开「全部调整」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CLM-016,基础设置 · 产品管理,中心库上下限建议 · 全部调整,「全部调整」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「全部调整」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
PRODM-CLM-017,基础设置 · 产品管理,中心库上下限建议 · 批量调整,打开「批量调整」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 点击「批量调整」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「批量调整」层打开;关键控件可见。,
PRODM-CLM-018,基础设置 · 产品管理,中心库上下限建议 · 批量调整,「批量调整」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit),"1. 确认已加载 storageState(test_1)
2. 打开「批量调整」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CLM-019,基础设置 · 产品管理,中心库上下限建议 · 批量调整,「批量调整」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议」权限;已进入中心库上下限建议页(/spd/basic/goods/paltform/product/centerLimit);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「批量调整」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-CLA-001,基础设置 · 产品管理,中心库上下限建议(AI) · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-CLA-002,基础设置 · 产品管理,中心库上下限建议(AI) · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-CLA-003,基础设置 · 产品管理,中心库上下限建议(AI) · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-CLA-004,基础设置 · 产品管理,中心库上下限建议(AI) · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):建议上下限、现上下限、采纳状态等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-CLA-005,基础设置 · 产品管理,中心库上下限建议(AI) · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-CLA-006,基础设置 · 产品管理,中心库上下限建议(AI) · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-CLA-007,基础设置 · 产品管理,中心库上下限建议(AI) · 行内操作,行内「调整」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「调整」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「调整」可达;关闭后回到列表。,
PRODM-CLA-008,基础设置 · 产品管理,中心库上下限建议(AI) · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-CLA-009,基础设置 · 产品管理,中心库上下限建议(AI) · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-CLA-010,基础设置 · 产品管理,中心库上下限建议(AI) · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-CLA-011,基础设置 · 产品管理,中心库上下限建议(AI) · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-CLA-012,基础设置 · 产品管理,中心库上下限建议(AI) · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-CLA-013,基础设置 · 产品管理,中心库上下限建议(AI) · 打印,按当前条件打印,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 点击「打 印」或「打印产品码」等页面级打印入口
3. 断言可拉起对话框/预览/toast,无 error",打印流程可启动。,
PRODM-CLA-014,基础设置 · 产品管理,中心库上下限建议(AI) · 全部调整,打开「全部调整」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 点击「全部调整」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「全部调整」层打开;关键控件可见。,
PRODM-CLA-015,基础设置 · 产品管理,中心库上下限建议(AI) · 全部调整,「全部调整」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 打开「全部调整」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CLA-016,基础设置 · 产品管理,中心库上下限建议(AI) · 全部调整,「全部调整」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「全部调整」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
PRODM-CLA-017,基础设置 · 产品管理,中心库上下限建议(AI) · 批量调整,打开「批量调整」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 点击「批量调整」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「批量调整」层打开;关键控件可见。,
PRODM-CLA-018,基础设置 · 产品管理,中心库上下限建议(AI) · 批量调整,「批量调整」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 打开「批量调整」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CLA-019,基础设置 · 产品管理,中心库上下限建议(AI) · 批量调整,「批量调整」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「批量调整」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
PRODM-CLA-020,基础设置 · 产品管理,中心库上下限建议(AI) · 采纳,打开「采纳」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 点击「采纳」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「采纳」层打开;关键控件可见。,
PRODM-CLA-021,基础设置 · 产品管理,中心库上下限建议(AI) · 采纳,「采纳」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi),"1. 确认已加载 storageState(test_1)
2. 打开「采纳」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CLA-022,基础设置 · 产品管理,中心库上下限建议(AI) · 采纳,「采纳」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库上下限建议(AI)」权限;已进入中心库上下限建议(AI)页(/spd/basic/goods/paltform/product/centerLimitAi);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「采纳」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-CZO-001,基础设置 · 产品管理,中心库库区产品设置 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-CZO-002,基础设置 · 产品管理,中心库库区产品设置 · 高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(不写死业务数据,见 rules 3.4)
3. 快捷区填入样本中有值的可见字段
4. 高级筛选抽屉填入样本中所有非空字段(文本直接填、下拉选同值;禁用字段记录默认)
5. 「查询/筛选」后抽样首行;首行同时满足样本中全部已填字段",查询正常无报错;查询条件写值正常;查询结果与查询条件一致。,
PRODM-CZO-003,基础设置 · 产品管理,中心库库区产品设置 · 高级筛选抽屉,点击高级筛选打开抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 点击「高级筛选」
3. 断言抽屉标题为「高级筛选」,关键字段可交互
4. 关闭抽屉,快捷区条件不被误清空(以现网为准)",高级筛选抽屉可打开并关闭。,
PRODM-CZO-004,基础设置 · 产品管理,中心库库区产品设置 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):库区、产品、上下限/货位等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-CZO-005,基础设置 · 产品管理,中心库库区产品设置 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-CZO-006,基础设置 · 产品管理,中心库库区产品设置 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-CZO-007,基础设置 · 产品管理,中心库库区产品设置 · 列表,列设置隐藏后再恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);列表表头已加载,"1. 确认已加载 storageState(test_1)
2. 默认查询后点击表格工具栏列设置(齿轮)
3. 取消勾选一列非核心字段并关闭面板,断言表头隐藏
4. 再次打开列设置勾选恢复并关闭,断言表头恢复",勾选状态与表头一致。,
PRODM-CZO-008,基础设置 · 产品管理,中心库库区产品设置 · 列表,横向滚动与列对齐,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);列表有数据且列超出视口,否则 skip,"1. 确认已加载 storageState(test_1)
2. 宽表场景下横向滚动使远侧列进入视口
3. 断言表头列数与数据列数一致、对齐无重叠",远侧列可进入视口;表头与数据列对齐。,
PRODM-CZO-009,基础设置 · 产品管理,中心库库区产品设置 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-CZO-010,基础设置 · 产品管理,中心库库区产品设置 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-CZO-011,基础设置 · 产品管理,中心库库区产品设置 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-CZO-012,基础设置 · 产品管理,中心库库区产品设置 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-CZO-013,基础设置 · 产品管理,中心库库区产品设置 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-CZO-014,基础设置 · 产品管理,中心库库区产品设置 · 添加中心库库区产品,打开「添加中心库库区产品」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 点击「添加中心库库区产品」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「添加中心库库区产品」层打开;关键控件可见。,
PRODM-CZO-015,基础设置 · 产品管理,中心库库区产品设置 · 添加中心库库区产品,「添加中心库库区产品」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product),"1. 确认已加载 storageState(test_1)
2. 打开「添加中心库库区产品」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CZO-016,基础设置 · 产品管理,中心库库区产品设置 · 添加中心库库区产品,「添加中心库库区产品」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 中心库库区产品设置」权限;已进入中心库库区产品设置页(/spd/basic/goods/platform/area/product);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「添加中心库库区产品」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-DIC-001,基础设置 · 产品管理,产品DI变更 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-DIC-002,基础设置 · 产品管理,产品DI变更 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-DIC-003,基础设置 · 产品管理,产品DI变更 · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-DIC-004,基础设置 · 产品管理,产品DI变更 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):DI 变更单号、状态、产品等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-DIC-005,基础设置 · 产品管理,产品DI变更 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-DIC-006,基础设置 · 产品管理,产品DI变更 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-DIC-007,基础设置 · 产品管理,产品DI变更 · 行内操作,行内「日志」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「日志」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「日志」可达;关闭后回到列表。,
PRODM-DIC-008,基础设置 · 产品管理,产品DI变更 · 列表,列设置隐藏后再恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);列表表头已加载,"1. 确认已加载 storageState(test_1)
2. 默认查询后点击表格工具栏列设置(齿轮)
3. 取消勾选一列非核心字段并关闭面板,断言表头隐藏
4. 再次打开列设置勾选恢复并关闭,断言表头恢复",勾选状态与表头一致。,
PRODM-DIC-009,基础设置 · 产品管理,产品DI变更 · 列表,横向滚动与列对齐,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);列表有数据且列超出视口,否则 skip,"1. 确认已加载 storageState(test_1)
2. 宽表场景下横向滚动使远侧列进入视口
3. 断言表头列数与数据列数一致、对齐无重叠",远侧列可进入视口;表头与数据列对齐。,
PRODM-DIC-010,基础设置 · 产品管理,产品DI变更 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-DIC-011,基础设置 · 产品管理,产品DI变更 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-DIC-012,基础设置 · 产品管理,产品DI变更 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-DIC-013,基础设置 · 产品管理,产品DI变更 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI变更」权限;已进入产品DI变更页(/spd/basic/goods/platform/dichange);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-DIQ-001,基础设置 · 产品管理,产品DI查询 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-DIQ-002,基础设置 · 产品管理,产品DI查询 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-DIQ-003,基础设置 · 产品管理,产品DI查询 · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-DIQ-004,基础设置 · 产品管理,产品DI查询 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):DI、产品、注册相关列(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-DIQ-005,基础设置 · 产品管理,产品DI查询 · 列表,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内操作可达;关闭后回到列表。,
PRODM-DIQ-006,基础设置 · 产品管理,产品DI查询 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-DIQ-007,基础设置 · 产品管理,产品DI查询 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-DIQ-008,基础设置 · 产品管理,产品DI查询 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-DIQ-009,基础设置 · 产品管理,产品DI查询 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-DIQ-010,基础设置 · 产品管理,产品DI查询 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品DI查询」权限;已进入产品DI查询页(/spd/basic/goods/platform/diSearch),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-SUP-001,基础设置 · 产品管理,产品供应关系维护 · 左侧分类树,树默认有选中或可展开节点,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 断言左侧分类树可见且有选中或可展开节点
3. 读取选中节点名称 `${sampleTreeNode}`
4. 断言右侧列表/表单与当前节点范围一致或为空态",树有选中;右侧不展示其它节点数据为成功态。,
PRODM-SUP-002,基础设置 · 产品管理,产品供应关系维护 · 左侧分类树,切换树节点刷新右侧,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 记录当前节点与右侧首行(若有)
3. 点选另一树节点(仅 1 个则 skip)
4. 断言右侧数据范围切换,不残留上一节点首行",切换节点后右侧刷新。,
PRODM-SUP-003,基础设置 · 产品管理,产品供应关系维护 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-SUP-004,基础设置 · 产品管理,产品供应关系维护 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-SUP-005,基础设置 · 产品管理,产品供应关系维护 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):产品、供应商/销售商、供应关系字段(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-SUP-006,基础设置 · 产品管理,产品供应关系维护 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-SUP-007,基础设置 · 产品管理,产品供应关系维护 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-SUP-008,基础设置 · 产品管理,产品供应关系维护 · 列表,列设置隐藏后再恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);列表表头已加载,"1. 确认已加载 storageState(test_1)
2. 默认查询后点击表格工具栏列设置(齿轮)
3. 取消勾选一列非核心字段并关闭面板,断言表头隐藏
4. 再次打开列设置勾选恢复并关闭,断言表头恢复",勾选状态与表头一致。,
PRODM-SUP-009,基础设置 · 产品管理,产品供应关系维护 · 列表,销售商(自定义)列表列升序降序切换,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);至少 2 条数据,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询后列表有数据
3. 横向滚动使「销售商(自定义)列表」列表头可见(宽表时)
4. 第一次点击该列排序控件并校验升序指示器
5. 第二次点击并校验降序指示器",排序指示器与方向一致;列表仍有数据。,
PRODM-SUP-010,基础设置 · 产品管理,产品供应关系维护 · 列表,横向滚动与列对齐,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);列表有数据且列超出视口,否则 skip,"1. 确认已加载 storageState(test_1)
2. 宽表场景下横向滚动使远侧列进入视口
3. 断言表头列数与数据列数一致、对齐无重叠",远侧列可进入视口;表头与数据列对齐。,
PRODM-SUP-011,基础设置 · 产品管理,产品供应关系维护 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-SUP-012,基础设置 · 产品管理,产品供应关系维护 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-SUP-013,基础设置 · 产品管理,产品供应关系维护 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-SUP-014,基础设置 · 产品管理,产品供应关系维护 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-SUP-015,基础设置 · 产品管理,产品供应关系维护 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品供应关系维护」权限;已进入产品供应关系维护页(/spd/basic/goods/paltform/purchases/calatog),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-ICH-001,基础设置 · 产品管理,产品信息变更 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-ICH-002,基础设置 · 产品管理,产品信息变更 · 高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(不写死业务数据,见 rules 3.4)
3. 快捷区填入样本中有值的可见字段
4. 高级筛选抽屉填入样本中所有非空字段(文本直接填、下拉选同值;禁用字段记录默认)
5. 「查询/筛选」后抽样首行;首行同时满足样本中全部已填字段",查询正常无报错;查询条件写值正常;查询结果与查询条件一致。,
PRODM-ICH-003,基础设置 · 产品管理,产品信息变更 · 高级筛选抽屉,点击高级筛选打开抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 点击「高级筛选」
3. 断言抽屉标题为「高级筛选」,关键字段可交互
4. 关闭抽屉,快捷区条件不被误清空(以现网为准)",高级筛选抽屉可打开并关闭。,
PRODM-ICH-004,基础设置 · 产品管理,产品信息变更 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):单号、制单时间、状态、变更摘要等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-ICH-005,基础设置 · 产品管理,产品信息变更 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-ICH-006,基础设置 · 产品管理,产品信息变更 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-ICH-007,基础设置 · 产品管理,产品信息变更 · 行内操作,行内「日志」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「日志」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「日志」可达;关闭后回到列表。,
PRODM-ICH-008,基础设置 · 产品管理,产品信息变更 · 列表,列设置隐藏后再恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);列表表头已加载,"1. 确认已加载 storageState(test_1)
2. 默认查询后点击表格工具栏列设置(齿轮)
3. 取消勾选一列非核心字段并关闭面板,断言表头隐藏
4. 再次打开列设置勾选恢复并关闭,断言表头恢复",勾选状态与表头一致。,
PRODM-ICH-009,基础设置 · 产品管理,产品信息变更 · 列表,横向滚动与列对齐,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);列表有数据且列超出视口,否则 skip,"1. 确认已加载 storageState(test_1)
2. 宽表场景下横向滚动使远侧列进入视口
3. 断言表头列数与数据列数一致、对齐无重叠",远侧列可进入视口;表头与数据列对齐。,
PRODM-ICH-010,基础设置 · 产品管理,产品信息变更 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-ICH-011,基础设置 · 产品管理,产品信息变更 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-ICH-012,基础设置 · 产品管理,产品信息变更 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-ICH-013,基础设置 · 产品管理,产品信息变更 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-ICH-014,基础设置 · 产品管理,产品信息变更 · 打印,按当前条件打印,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 点击「打 印」或「打印产品码」等页面级打印入口
3. 断言可拉起对话框/预览/toast,无 error",打印流程可启动。,
PRODM-ICH-015,基础设置 · 产品管理,产品信息变更 · 新增,打开「新增」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 点击「新增」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「新增」层打开;关键控件可见。,
PRODM-ICH-016,基础设置 · 产品管理,产品信息变更 · 新增,「新增」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info),"1. 确认已加载 storageState(test_1)
2. 打开「新增」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-ICH-017,基础设置 · 产品管理,产品信息变更 · 新增,「新增」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品信息变更」权限;已进入产品信息变更页(/spd/basic/goods/platform/product-info);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「新增」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-FAU-001,基础设置 · 产品管理,产品初审核对 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-FAU-002,基础设置 · 产品管理,产品初审核对 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-FAU-003,基础设置 · 产品管理,产品初审核对 · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-FAU-004,基础设置 · 产品管理,产品初审核对 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):物料编号、名称、规格、注册人、审核状态等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-FAU-005,基础设置 · 产品管理,产品初审核对 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-FAU-006,基础设置 · 产品管理,产品初审核对 · 行内操作,行内「审核」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「审核」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「审核」可达;关闭后回到列表。,
PRODM-FAU-007,基础设置 · 产品管理,产品初审核对 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-FAU-008,基础设置 · 产品管理,产品初审核对 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-FAU-009,基础设置 · 产品管理,产品初审核对 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-FAU-010,基础设置 · 产品管理,产品初审核对 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-FAU-011,基础设置 · 产品管理,产品初审核对 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-FAU-012,基础设置 · 产品管理,产品初审核对 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-FAU-013,基础设置 · 产品管理,产品初审核对 · 产品初审核对,打开「产品初审核对」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 点击「产品初审核对」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「产品初审核对」层打开;关键控件可见。,
PRODM-FAU-014,基础设置 · 产品管理,产品初审核对 · 产品初审核对,「产品初审核对」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first),"1. 确认已加载 storageState(test_1)
2. 打开「产品初审核对」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-FAU-015,基础设置 · 产品管理,产品初审核对 · 产品初审核对,「产品初审核对」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品初审核对」权限;已进入产品初审核对页(/spd/basic/goods/paltform/product/first);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「产品初审核对」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-CHK-001,基础设置 · 产品管理,产品核对重新发起 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-CHK-002,基础设置 · 产品管理,产品核对重新发起 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-CHK-003,基础设置 · 产品管理,产品核对重新发起 · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-CHK-004,基础设置 · 产品管理,产品核对重新发起 · 状态 Tab,状态 Tab 与列表一致,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 依次切换 Tab:全部、初审驳回、物价驳回(以现网为准)
3. 每次切换后断言列表状态列与 Tab 语义一致或为空态",Tab 与列表一致;无报错。,
PRODM-CHK-005,基础设置 · 产品管理,产品核对重新发起 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):物料编号、是否植入、物料名称、器械注册人、规格型号、单位、耗材类型、销售商等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-CHK-006,基础设置 · 产品管理,产品核对重新发起 · 列表,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内操作可达;关闭后回到列表。,
PRODM-CHK-007,基础设置 · 产品管理,产品核对重新发起 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-CHK-008,基础设置 · 产品管理,产品核对重新发起 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-CHK-009,基础设置 · 产品管理,产品核对重新发起 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-CHK-010,基础设置 · 产品管理,产品核对重新发起 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-CHK-011,基础设置 · 产品管理,产品核对重新发起 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-CHK-012,基础设置 · 产品管理,产品核对重新发起 · 重新发起审核,打开「重新发起审核」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 点击「重新发起审核」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「重新发起审核」层打开;关键控件可见。,
PRODM-CHK-013,基础设置 · 产品管理,产品核对重新发起 · 重新发起审核,「重新发起审核」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check),"1. 确认已加载 storageState(test_1)
2. 打开「重新发起审核」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-CHK-014,基础设置 · 产品管理,产品核对重新发起 · 重新发起审核,「重新发起审核」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品核对重新发起」权限;已进入产品核对重新发起页(/spd/basic/goods/paltform/product/check);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「重新发起审核」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-PRC-001,基础设置 · 产品管理,产品物价核对 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-PRC-002,基础设置 · 产品管理,产品物价核对 · 快捷筛选查询,全量可见条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(只填当前结果里有值的条件)
3. 快捷区可见非空条件全部填入后查询
4. 抽样首行同时满足全部已填条件;无未捕获异常",查询正常无报错;查询结果与查询条件一致。,
PRODM-PRC-003,基础设置 · 产品管理,产品物价核对 · 查询条件展开收起,展开后更多条件可见,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 点击「展开」
3. 断言额外筛选字段进入可视区且可填写
4. 按钮文案变为「收起」",展开后更多条件可见并可交互。,
PRODM-PRC-004,基础设置 · 产品管理,产品物价核对 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):物料、物价/收费相关列、审核状态等(以现网为准)
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-PRC-005,基础设置 · 产品管理,产品物价核对 · 行内操作,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「详情」可达;关闭后回到列表。,
PRODM-PRC-006,基础设置 · 产品管理,产品物价核对 · 行内操作,行内「审核」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「审核」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「审核」可达;关闭后回到列表。,
PRODM-PRC-007,基础设置 · 产品管理,产品物价核对 · 行内操作,行内「编辑」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「编辑」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内「编辑」可达;关闭后回到列表。,
PRODM-PRC-008,基础设置 · 产品管理,产品物价核对 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-PRC-009,基础设置 · 产品管理,产品物价核对 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-PRC-010,基础设置 · 产品管理,产品物价核对 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-PRC-011,基础设置 · 产品管理,产品物价核对 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-PRC-012,基础设置 · 产品管理,产品物价核对 · 批量审核,打开「批量审核」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 点击「批量审核」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「批量审核」层打开;关键控件可见。,
PRODM-PRC-013,基础设置 · 产品管理,产品物价核对 · 批量审核,「批量审核」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 打开「批量审核」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-PRC-014,基础设置 · 产品管理,产品物价核对 · 批量审核,「批量审核」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「批量审核」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
PRODM-PRC-015,基础设置 · 产品管理,产品物价核对 · 批量是否收费,打开「批量是否收费」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 点击「批量是否收费」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「批量是否收费」层打开;关键控件可见。,
PRODM-PRC-016,基础设置 · 产品管理,产品物价核对 · 批量是否收费,「批量是否收费」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck),"1. 确认已加载 storageState(test_1)
2. 打开「批量是否收费」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-PRC-017,基础设置 · 产品管理,产品物价核对 · 批量是否收费,「批量是否收费」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品物价核对」权限;已进入产品物价核对页(/spd/basic/goods/paltform/product/priceCheck);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「批量是否收费」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
PRODM-MNT-001,基础设置 · 产品管理,产品维护 · 左侧耗材分类树,树默认有选中或可展开节点,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 断言左侧耗材分类树可见且有选中或可展开节点
3. 读取选中节点名称 `${sampleTreeNode}`
4. 断言右侧列表/表单与当前节点范围一致或为空态",树有选中;右侧不展示其它节点数据为成功态。,
PRODM-MNT-002,基础设置 · 产品管理,产品维护 · 左侧耗材分类树,切换树节点刷新右侧,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 记录当前节点与右侧首行(若有)
3. 点选另一树节点(仅 1 个则 skip)
4. 断言右侧数据范围切换,不残留上一节点首行",切换节点后右侧刷新。,
PRODM-MNT-003,基础设置 · 产品管理,产品维护 · 快捷筛选查询,快捷区重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 任意填写快捷区可见字段后点击「重 置」
3. 断言条件恢复默认;列表是否刷新按产品规则(未定则记待评审)",重置后条件恢复默认;页面无报错。,
PRODM-MNT-004,基础设置 · 产品管理,产品维护 · 高级筛选抽屉,全量查询条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);默认查询至少 1 条,否则 skip,"1. 确认已加载 storageState(test_1)
2. 默认查询 → 从首行提取全量样本(不写死业务数据,见 rules 3.4)
3. 快捷区填入样本中有值的可见字段
4. 高级筛选抽屉填入样本中所有非空字段(文本直接填、下拉选同值;禁用字段记录默认)
5. 「查询/筛选」后抽样首行;首行同时满足样本中全部已填字段",查询正常无报错;查询条件写值正常;查询结果与查询条件一致。,
PRODM-MNT-005,基础设置 · 产品管理,产品维护 · 高级筛选抽屉,点击高级筛选打开抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 点击「高级筛选」
3. 断言抽屉标题为「高级筛选」,关键字段可交互
4. 关闭抽屉,快捷区条件不被误清空(以现网为准)",高级筛选抽屉可打开并关闭。,
PRODM-MNT-006,基础设置 · 产品管理,产品维护 · 列表,列表核心列完整,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 断言表头含核心列(抽样):HIS编码、阳采ID、规格型号、器械注册人、单位、耗材类型、品牌、器械注册证号等
3. 若有数据,抽样首行关键字段非空",核心列完整;列表结构正确。,
PRODM-MNT-007,基础设置 · 产品管理,产品维护 · 列表,行内「详情」可达(只读冒烟),高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);列表至少 1 条,"1. 确认已加载 storageState(test_1)
2. 首行有数据时点击「详情」
3. 断言进入预期弹窗/抽屉/详情,可见取消/关闭/返回
4. 关闭弹层或点返回,列表仍可见",行内操作可达;关闭后回到列表。,
PRODM-MNT-008,基础设置 · 产品管理,产品维护 · 列表,列设置隐藏后再恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);列表表头已加载,"1. 确认已加载 storageState(test_1)
2. 默认查询后点击表格工具栏列设置(齿轮)
3. 取消勾选一列非核心字段并关闭面板,断言表头隐藏
4. 再次打开列设置勾选恢复并关闭,断言表头恢复",勾选状态与表头一致。,
PRODM-MNT-009,基础设置 · 产品管理,产品维护 · 列表,横向滚动与列对齐,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);列表有数据且列超出视口,否则 skip,"1. 确认已加载 storageState(test_1)
2. 宽表场景下横向滚动使远侧列进入视口
3. 断言表头列数与数据列数一致、对齐无重叠",远侧列可进入视口;表头与数据列对齐。,
PRODM-MNT-010,基础设置 · 产品管理,产品维护 · 列表,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 名称/编号类关键字填入 `__NOT_EXIST_PRODM__99999` 后查询
3. 断言表头仍在,0 行或空态,分页总数为 0",无匹配不报错;不展示过期成功态数据。,
PRODM-MNT-011,基础设置 · 产品管理,产品维护 · 分页,有数据时总条数,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 默认查询后读取「共 N 条」
3. 断言 N 与本次查询匹配条数规则一致(有数据或 0)",「共 N 条」与查询结果一致。,
PRODM-MNT-012,基础设置 · 产品管理,产品维护 · 分页,切换至第 2 页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);总条数大于一页,否则 skip,"1. 确认已加载 storageState(test_1)
2. 记录第 1 页首行关键字段
3. 点击页码「2」或「下一页」
4. 断言页码高亮正确,首行数据与第 1 页不同",翻页成功;数据更换。,
PRODM-MNT-013,基础设置 · 产品管理,产品维护 · 分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);N>10 且切换前可见行数 > min(N,20),否则 skip(见 3.7)","1. 确认已加载 storageState(test_1)
2. 记录「共 N 条」与当前页可见行数
3. 打开每页条数下拉,选择「20 条/页」
4. 断言分页器显示 20 条/页;N 不变;当前页行数=min(N,20) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
PRODM-MNT-014,基础设置 · 产品管理,产品维护 · 导出,当前查询条件下导出,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 先执行一次查询
3. 点击「导 出」或「导出」或「导出Excel」
4. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
PRODM-MNT-015,基础设置 · 产品管理,产品维护 · 分类维护,打开「分类维护」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 点击「分类维护」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「分类维护」层打开;关键控件可见。,
PRODM-MNT-016,基础设置 · 产品管理,产品维护 · 分类维护,「分类维护」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 打开「分类维护」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-MNT-017,基础设置 · 产品管理,产品维护 · 分类维护,「分类维护」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「分类维护」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
PRODM-MNT-018,基础设置 · 产品管理,产品维护 · 添加,打开「添加」对应弹窗/抽屉,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 点击「添加」或等价入口
3. 断言弹窗/抽屉/详情可见,关键字段可交互
4. 可见取消/关闭/返回",「添加」层打开;关键控件可见。,
PRODM-MNT-019,基础设置 · 产品管理,产品维护 · 添加,「添加」取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog),"1. 确认已加载 storageState(test_1)
2. 打开「添加」,任意填写非空后点击「取 消」或关闭
3. 断言弹层关闭,列表行数与打开前一致(抽样)",取消后列表无新增/无修改。,
PRODM-MNT-020,基础设置 · 产品管理,产品维护 · 添加,「添加」必填为空拦截,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState;具备「基础设置 → 产品管理 → 产品维护」权限;已进入产品维护页(/spd/basic/goods/paltform/product/calatog);该层存在必填提交,否则 skip,"1. 确认已加载 storageState(test_1)
2. 打开「添加」,必填项留空后点击「确 定」
3. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
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