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

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

修复 auth 写入锁 stale PID 导致 setup 空等超时;主辅账号 setup 串行并延长超时,login 依赖 setup。同步用户/厂商/准入等 CSV·MD·spec·PO,移除 stat 调试 spec,补充离线全量脚本与 gate-stamp 目录哈希修复。
parent 45178f79
...@@ -21,7 +21,6 @@ import { ...@@ -21,7 +21,6 @@ import {
import { env } from './config/env.config'; import { env } from './config/env.config';
import { applyPlaywrightBrowsersPath } from './config/playwright-browsers-path'; import { applyPlaywrightBrowsersPath } from './config/playwright-browsers-path';
import { timeouts } from './config/timeouts.config'; import { timeouts } from './config/timeouts.config';
import { AUTH_STATE_PATH } from './utils/path.util';
applyPlaywrightBrowsersPath(); applyPlaywrightBrowsersPath();
...@@ -35,10 +34,8 @@ function resolveGrepInvert(): RegExp | undefined { ...@@ -35,10 +34,8 @@ function resolveGrepInvert(): RegExp | undefined {
return new RegExp(parts.join('|')); return new RegExp(parts.join('|'));
} }
/** 调试/演示专用 *.stat-*.spec.ts 与主 spec 重复;CI 默认不跑,本地按文件执行不受影响。 */ /** 历史调试文件 *.stat-*.spec.ts 与主 spec 重复,一律不收集。 */
function resolveDebugSpecIgnore(): RegExp[] { function resolveDebugSpecIgnore(): RegExp[] {
if (process.env.INCLUDE_DEBUG_SPECS === '1') return [];
if (!isCi() && process.env.EXCLUDE_DEBUG_SPECS !== '1') return [];
return [/\.stat-[^/]+\.spec\.ts$/]; return [/\.stat-[^/]+\.spec\.ts$/];
} }
...@@ -97,10 +94,12 @@ export default defineConfig({ ...@@ -97,10 +94,12 @@ export default defineConfig({
{ {
name: 'setup', name: 'setup',
testMatch: /auth\.setup\.ts/, testMatch: /auth\.setup\.ts/,
timeout: timeouts.authSetup,
}, },
{ {
name: 'login', name: 'login',
testMatch: /login\.spec\.ts/, testMatch: /login\.spec\.ts/,
dependencies: ['setup'],
use: { use: {
browserName: 'chromium', browserName: 'chromium',
storageState: { cookies: [], origins: [] }, storageState: { cookies: [], origins: [] },
...@@ -112,8 +111,7 @@ export default defineConfig({ ...@@ -112,8 +111,7 @@ export default defineConfig({
testIgnore: [/auth\.setup\.ts/, /login\.spec\.ts/, ...resolveDebugSpecIgnore()], testIgnore: [/auth\.setup\.ts/, /login\.spec\.ts/, ...resolveDebugSpecIgnore()],
use: { use: {
browserName: 'chromium', browserName: 'chromium',
// 每条用例独立 context,从该文件注入登录态(互不污染) // storageState 由 fixtures/index.ts 按 worker 注入(支持双账号)
storageState: AUTH_STATE_PATH,
}, },
}, },
], ],
......
#!/usr/bin/env node
/** 临时诊断:查看角色管理页真实 DOM 结构(角色列表 / 权限树容器) */
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
import { existsSync } from 'node:fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
if (existsSync(path.join(ROOT, '.env'))) process.loadEnvFile(path.join(ROOT, '.env'));
const BASE = process.env.BASE_URL || 'http://spdtest.cmic.com.cn:8080';
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
storageState: path.join(ROOT, 'auth/user.json'),
viewport: { width: 1920, height: 1080 },
});
const page = await ctx.newPage();
await page.goto(`${BASE}/spd/basic/user/platform/role`, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForTimeout(6000);
const info = await page.evaluate(() => {
const out = {};
out.title = document.title;
out.containers = [...document.querySelectorAll('[class*="role"], [class*="Role"], .ant-tree, .ant-table, .ant-list, [class*="perm"], [class*="menu"]')]
.slice(0, 60)
.map((el) => ({
tag: el.tagName,
cls: el.className && String(el.className).slice(0, 90),
children: el.children.length,
text: (el.textContent || '').trim().slice(0, 60),
}));
out.treeCount = document.querySelectorAll('.ant-tree').length;
out.treeNodeCount = document.querySelectorAll('.ant-tree-treenode').length;
out.tableRows = document.querySelectorAll('.ant-table-tbody tr').length;
out.bodyLen = document.body.innerHTML.length;
return out;
});
console.log(JSON.stringify(info, null, 1));
await browser.close();
#!/usr/bin/env node
/** 临时诊断 v2:定位角色管理页左侧角色列表与右侧权限树的真实结构 */
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
import { existsSync } from 'node:fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
if (existsSync(path.join(ROOT, '.env'))) process.loadEnvFile(path.join(ROOT, '.env'));
const BASE = process.env.BASE_URL || 'http://spdtest.cmic.com.cn:8080';
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
storageState: path.join(ROOT, 'auth/user.json'),
viewport: { width: 1920, height: 1080 },
});
const page = await ctx.newPage();
await page.goto(`${BASE}/spd/basic/user/platform/role`, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForTimeout(6000);
const info = await page.evaluate(() => {
const trees = [...document.querySelectorAll('.ant-tree')].map((t, i) => {
const r = t.getBoundingClientRect();
const visible = r.width > 0 && r.height > 0;
return {
i,
visible,
rect: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
treenodes: t.querySelectorAll('.ant-tree-treenode').length,
firstTexts: [...t.querySelectorAll('.ant-tree-title')].slice(0, 6).map((n) => (n.textContent || '').trim().slice(0, 18)),
cls: String(t.className).slice(0, 80),
};
});
// 左侧角色区域的备选容器
const roleCandidates = [...document.querySelectorAll('.ant-table, .ant-list, .ant-tabs, [class*="role"], [class*="user"]')]
.filter((el) => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0 && r.x < 700;
})
.slice(0, 12)
.map((el) => {
const r = el.getBoundingClientRect();
return {
tag: el.tagName,
cls: String(el.className).slice(0, 90),
rect: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
text: (el.textContent || '').trim().slice(0, 50),
};
});
// 左侧区域所有 ant-tree-treenode 的可见性抽样
const allNodes = [...document.querySelectorAll('.ant-tree-treenode')];
const visibleNodes = allNodes.filter((n) => {
const r = n.getBoundingClientRect();
return r.width > 0 && r.height > 0;
});
const sample = visibleNodes.slice(0, 8).map((n) => {
const r = n.getBoundingClientRect();
return {
rect: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
text: (n.textContent || '').trim().slice(0, 20),
checkbox: !!n.querySelector('.ant-tree-checkbox'),
ownCheckbox: !!n.querySelector('.ant-tree-checkbox:not(.ant-tree-checkbox-indeterminate)'),
};
});
return { title: document.title, trees, totalNodes: allNodes.length, visibleNodes: visibleNodes.length, sample, roleCandidates };
});
console.log(JSON.stringify(info, null, 1));
await browser.close();
#!/usr/bin/env node
/** 临时诊断 v3:确认 .ant-tree 内 checkbox 是否命中交互选择器,以及左侧角色列表结构 */
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
import { existsSync } from 'node:fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
if (existsSync(path.join(ROOT, '.env'))) process.loadEnvFile(path.join(ROOT, '.env'));
const BASE = process.env.BASE_URL || 'http://spdtest.cmic.com.cn:8080';
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
storageState: path.join(ROOT, 'auth/user.json'),
viewport: { width: 1920, height: 1080 },
});
const page = await ctx.newPage();
await page.goto(`${BASE}/spd/basic/user/platform/role`, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForTimeout(6000);
const info = await page.evaluate(() => {
const INTERACTIVE =
'a[href],button,input,select,textarea,[role="button"],[role="link"],[role="tab"],' +
'[role="menuitem"],[role="checkbox"],[role="radio"],[role="combobox"],[role="switch"],' +
'[role="slider"],[contenteditable="true"],[tabindex]:not([tabindex="-1"]),' +
'.ant-btn,.ant-select,.ant-picker,.ant-input,.ant-checkbox-wrapper,.ant-radio-wrapper,' +
'.ant-pagination,.ant-pagination-item,.ant-pagination-next,.ant-pagination-prev,' +
'.ant-pagination-options-size-changer,.ant-pagination-options-quick-jumper input,' +
'.ant-table-column-sorter,.ant-dropdown-trigger,.ant-tabs-tab,.ant-drawer-close,' +
'.ant-modal-close,.advance-filter,.ant-cascader,.ant-drawer-footer .ant-btn,' +
'.ant-table-row-expand-icon,.ant-table-row-expand-icon-cell';
const hits = document.querySelectorAll(INTERACTIVE);
const inTree = [...hits].filter((el) => el.closest('.ant-tree'));
const treeClasses = [...new Set(inTree.slice(0, 10).map((el) => el.tagName + '.' + String(el.className).slice(0, 50)))];
// 左侧:x < 560 的可见可交互元素(角色列表区域)
const left = [...hits].filter((el) => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0 && r.x < 560 && !el.closest('.ant-menu');
});
const leftSample = left.slice(0, 15).map((el) => {
const r = el.getBoundingClientRect();
return {
tag: el.tagName,
cls: String(el.className).slice(0, 60),
rect: [Math.round(r.x), Math.round(r.y)],
text: (el.textContent || '').trim().slice(0, 24),
};
});
return {
totalHits: hits.length,
inTreeHits: inTree.length,
treeClasses,
leftCount: left.length,
leftSample,
};
});
console.log(JSON.stringify(info, null, 1));
await browser.close();
import path from 'node:path';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
import { existsSync } from 'node:fs';
const ROOT = process.cwd();
if (existsSync(path.join(ROOT, '.env'))) process.loadEnvFile(path.join(ROOT, '.env'));
const BASE = process.env.BASE_URL;
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ storageState: path.join(ROOT,'auth/user.json'), viewport:{width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto(`${BASE}/spd/basic/user/platform/role`, { waitUntil:'domcontentloaded', timeout:120000 });
await page.waitForTimeout(6000);
const info = await page.evaluate(() => {
const node = document.querySelector('.ant-tree-treenode');
const cb = document.querySelector('.ant-tree-checkbox');
const sw = document.querySelector('.ant-tree-switcher');
return {
nodeCls: node && String(node.className),
cbCls: cb && String(cb.className),
cbOuter: cb && cb.outerHTML.slice(0,200),
swCls: sw && String(sw.className),
titleTag: document.querySelector('.ant-tree-title')?.tagName,
currentRole: [...document.querySelectorAll('*')].filter(e=>/当前角色|角色名称/.test(e.textContent||'') && e.children.length===0).slice(0,3).map(e=>e.textContent.trim()),
};
});
console.log(JSON.stringify(info,null,1));
await browser.close();
import path from 'node:path';
import './playwright-browsers-path.mjs';
import { chromium } from 'playwright';
import { existsSync } from 'node:fs';
const ROOT = process.cwd();
if (existsSync(path.join(ROOT, '.env'))) process.loadEnvFile(path.join(ROOT, '.env'));
const BASE = process.env.BASE_URL;
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ storageState: path.join(ROOT,'auth/user.json'), viewport:{width:1920,height:1080} });
const page = await ctx.newPage();
await page.goto(`${BASE}/spd/basic/user/platform/role`, { waitUntil:'domcontentloaded', timeout:120000 });
await page.waitForTimeout(6000);
const info = await page.evaluate(() => {
const tree = document.querySelector('.menu-role-tree');
const all = [...tree.querySelectorAll('.ant-tree-treenode')];
const classes = all.slice(0,6).map(n=>n.className);
const sample = all.slice(0,12).map(n=>({
cls: (n.className.match(/level-\d+/)||['?'])[0],
text: (n.querySelector('.ant-tree-title')?.textContent||'').trim().slice(0,14),
}));
return { hasLevelClass: /level-/.test(all[1]?.className||''), classes, sample };
});
console.log(JSON.stringify(info,null,1));
await browser.close();
...@@ -67,7 +67,7 @@ PAGES = [ ...@@ -67,7 +67,7 @@ PAGES = [
spec="UI-SPEC-SPD-USER-001", spec="UI-SPEC-SPD-USER-001",
spec_file="basic-user.spec.ts", spec_file="basic-user.spec.ts",
spec_ready=True, spec_ready=True,
tp_total=82, tp_total=84,
write_note=( write_note=(
"新增/编辑只打开后取消,不提交保存;授权只打开后取消;" "新增/编辑只打开后取消,不提交保存;授权只打开后取消;"
"重置密码只验证确认框后取消,不确认改密。禁止写死生产账号。" "重置密码只验证确认框后取消,不确认改密。禁止写死生产账号。"
...@@ -241,7 +241,7 @@ PAGES = [ ...@@ -241,7 +241,7 @@ PAGES = [
auto="USER-USR-016", auto="USER-USR-016",
), ),
case( case(
17, "列表与分页", "功能验证点", "P1", "打开列设置面板", 25, 17, "列表与分页", "功能验证点", "P2", "打开列设置面板", 83,
[ [
"点击「查 询」等待列表稳定", "点击「查 询」等待列表稳定",
"点击表格工具栏列设置(齿轮)图标", "点击表格工具栏列设置(齿轮)图标",
...@@ -251,7 +251,7 @@ PAGES = [ ...@@ -251,7 +251,7 @@ PAGES = [
pre="默认查询至少 1 条", auto="USER-USR-017", pre="默认查询至少 1 条", auto="USER-USR-017",
), ),
case( case(
18, "列表与分页", "功能验证点", "P1", "列设置取消勾选后列表列恢复", 25, 18, "列表与分页", "功能验证点", "P2", "列设置取消勾选后列表列恢复", 84,
[ [
"点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭", "点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭",
"断言该列表头隐藏;再次打开列设置勾选恢复并关闭", "断言该列表头隐藏;再次打开列设置勾选恢复并关闭",
...@@ -296,7 +296,7 @@ PAGES = [ ...@@ -296,7 +296,7 @@ PAGES = [
spec="UI-SPEC-SPD-USER-002", spec="UI-SPEC-SPD-USER-002",
spec_file="basic-user-union.spec.ts", spec_file="basic-user-union.spec.ts",
spec_ready=True, spec_ready=True,
tp_total=58, tp_total=60,
write_note=( write_note=(
"新增/编辑只打开后取消,不提交保存;授权与改密规则同用户管理,本页不重复真实写操作。" "新增/编辑只打开后取消,不提交保存;授权与改密规则同用户管理,本页不重复真实写操作。"
"禁止写死医院名称。" "禁止写死医院名称。"
...@@ -306,171 +306,124 @@ PAGES = [ ...@@ -306,171 +306,124 @@ PAGES = [
"查询后从首行读取 `${sampleLoginName}`、`${sampleUserName}`、`${sampleHospital}`、`${samplePhone}`;" "查询后从首行读取 `${sampleLoginName}`、`${sampleUserName}`、`${sampleHospital}`、`${samplePhone}`;"
"禁止写死扫描到的医院名;mock:`__NOT_EXIST_USER__99999`。" "禁止写死扫描到的医院名;mock:`__NOT_EXIST_USER__99999`。"
), ),
skip="授权/改密持久化(引用 TP_USER_USR)、真实跨院保存、Excel 导入文件、接口失败、会话过期", skip="未登录与菜单进入、姓名/登录账号关键字与所属医院单筛、高级筛选按状态(扫描无该字段)、授权/改密持久化(引用 TP_USER_USR)、真实跨院保存、Excel 导入文件、接口失败、会话过期",
cases=[ cases=[
case( case(
1, "页面进入", "功能验证点", "P0", "菜单进入医共体用户管理页", 1, 1, "筛选与查询", "功能验证点", "P0", "默认查询列表有数据或空态明确", 9,
[
"按菜单「基础设置 → 用户管理 → 医共体用户管理」进入,或 `openPath` 打开 `/spd/basic/user/platform/personCom`",
"断言 URL 含 `/spd/basic/user/platform/personCom`",
"断言所属医院筛选、工具栏、列表列头与分页可见",
],
"进入医共体用户管理页;不误入用户管理或角色管理。",
auto="USER-UNI-001",
),
case(
2, "页面进入", "异常验证点", "P0", "未登录访问拦截", 5,
[
"清除登录态后直接打开 `/spd/basic/user/platform/personCom`",
"断言跳转登录或不展示用户业务列表",
],
"未登录不可见医共体用户数据。",
),
case(
3, "筛选与查询", "功能验证点", "P0", "默认查询列表有数据或空态明确", 9,
[ [
"点击「查 询」并等待列表稳定", "点击「查 询」并等待列表稳定",
"断言出现数据行,或空态且分页总数为 0", "断言出现数据行,或空态且分页总数为 0",
], ],
"查询无报错;有数据时「共 N 条」N≥1。", "查询无报错;有数据时「共 N 条」N≥1。",
auto="USER-UNI-002",
), ),
case( case(
4, "筛选与查询", "功能验证点", "P0", "姓名或登录账号关键字查询", 10, 2, "筛选与查询", "功能验证点", "P0", "姓名、所属医院、联系方式、科室、角色等条件组合查询", 13,
[ [
"从首行读取 `${sampleUserName}` 或 `${sampleLoginName}`", "点击「查 询」后读取首行 `${sampleUserName}`、`${sampleLoginName}`、`${sampleHospital}`、`${samplePhone}`、`${sampleDept}`、`${sampleRole}`(科室或联系方式为 `--`/空时换一行)",
"在「姓名」框填入关键字,点击「查 询」", "填写姓名、所属医院,展开高级筛选填写「联系方式」、选择科室与角色后点「筛 选」(无则点「查 询」)",
"断言结果行姓名或登录账号匹配", "断言结果非空;首行登录账号与采样行一致;可见行姓名/所属医院/联系方式/科室/角色与已填条件一致",
],
"查询结果与关键字匹配。",
pre="默认查询至少 1 条", auto="USER-UNI-003",
),
case(
5, "筛选与查询", "功能验证点", "P0", "所属医院筛选", 11,
[
"记录当前默认 `${sampleHospital}` 与分页总数",
"切换「所属医院」为另一可选项(仅 1 家则 skip)",
"点击「查 询」",
"断言结果行「所属医院」均为所选值,「共 N 条」可与默认不同",
],
"列表仅该院用户;总数随医院变化。",
auto="USER-UNI-004",
),
case(
6, "筛选与查询", "功能验证点", "P0", "姓名+医院+科室+角色+状态组合查询", 13,
[
"点击「查 询」后读取首行 `${sampleUserName}`、`${sampleLoginName}`、`${sampleHospital}`、`${sampleDept}`、`${sampleRole}`、`${sampleStatus}`(科室为 `--` 时换一行)",
"填写姓名、所属医院、科室、角色,展开高级筛选选择 `${sampleStatus}` 后点「筛 选」(无则点「查 询」)",
"断言结果非空;首行登录账号与采样行一致;可见行姓名/所属医院/科室/角色/状态与已填条件一致",
], ],
"组合查询无报错;结果与采样条件一致且同时满足全部已填条件。", "组合查询无报错;结果与采样条件一致且同时满足全部已填条件。",
pre="默认查询至少 1 条", auto="USER-UNI-006", pre="默认查询至少 1 条",
), ),
case( case(
7, "筛选与查询", "功能验证点", "P1", "重置恢复默认医院", 14, 3, "筛选与查询", "功能验证点", "P1", "重置恢复默认医院", 14,
[ [
"更改所属医院或姓名后点击「重 置」", "更改所属医院或姓名后点击「重 置」",
"断言所属医院恢复进入页时的默认值,列表刷新", "断言所属医院恢复进入页时的默认值,列表刷新",
], ],
"医院等条件恢复默认。", "医院等条件恢复默认。",
auto="USER-UNI-006",
), ),
case( case(
8, "筛选与查询", "功能验证点", "P1", "展开高级筛选", 15, 4, "筛选与查询", "功能验证点", "P1", "展开高级筛选", 15,
[ [
"点击「高级筛选」", "点击「高级筛选」",
"断言可见所属医院、联系方式、科室、角色及「筛 选」", "断言可见所属医院、联系方式、科室、角色及「筛 选」",
], ],
"高级筛选含医院维度字段。", "高级筛选含医院维度字段。",
auto="USER-UNI-007",
), ),
case( case(
9, "筛选与查询", "异常验证点", "P1", "无匹配空态(mock反例)", 21, 5, "筛选与查询", "异常验证点", "P1", "无匹配空态(mock反例)", 21,
[ [
"姓名框填入 `__NOT_EXIST_USER__99999`,点击「查 询」", "姓名框填入 `__NOT_EXIST_USER__99999`,点击「查 询」",
"断言空态且分页总数为 0", "断言空态且分页总数为 0",
], ],
"无匹配不报错;总数为 0。", "无匹配不报错;总数为 0。",
auto="USER-UNI-008",
), ),
case( case(
10, "列表与分页", "功能验证点", "P0", "列表列顺序含所属医院", 24, 6, "列表与分页", "功能验证点", "P0", "列表列顺序含所属医院", 24,
[ [
"断言列顺序为登录账号、姓名、联系方式、所属医院、所属科室、医院员工编号、角色、科室任职数、操作", "断言列顺序为登录账号、姓名、联系方式、所属医院、所属科室、医院员工编号、角色、科室任职数、操作",
"勿用用户管理页列断言(用户管理无所属医院且姓名在登录账号前)", "勿用用户管理页列断言(用户管理无所属医院且姓名在登录账号前)",
], ],
"列顺序与用户管理页不同;含所属医院。", "列顺序与用户管理页不同;含所属医院。",
auto="USER-UNI-009",
), ),
case( case(
11, "列表与分页", "功能验证点", "P1", "分页翻至第 2 页", 26, 7, "列表与分页", "功能验证点", "P1", "分页翻至第 2 页", 26,
[ [
"记录第 1 页首行登录账号", "记录第 1 页首行登录账号",
"翻到第 2 页", "翻到第 2 页",
"断言数据更换且所属医院筛选仍保持", "断言数据更换且所属医院筛选仍保持",
], ],
"翻页成功;医院条件不丢失。", "翻页成功;医院条件不丢失。",
pre="总条数大于一页,否则 skip", auto="USER-UNI-010", pre="总条数大于一页,否则 skip",
), ),
case( case(
12, "列表与分页", "集成验证点", "P1", "与用户管理页同一用户字段一致", 33, 8, "列表与分页", "集成验证点", "P1", "与用户管理页同一用户字段一致", 33,
[ [
"记录本页首行 `${sampleLoginName}`、姓名、所属科室、角色", "记录本页首行 `${sampleLoginName}`、姓名、所属科室、角色",
"打开用户管理页,按登录账号查询同一用户", "打开用户管理页,按登录账号查询同一用户",
"断言姓名、角色、所属科室一致;本页仅多「所属医院」", "断言姓名、角色、所属科室一致;本页仅多「所属医院」",
], ],
"同一登录账号跨页字段一致。", "同一登录账号跨页字段一致。",
pre="两页都能查到同一账号,否则 skip", auto="USER-UNI-011", pre="两页都能查到同一账号,否则 skip",
), ),
case( case(
13, "新增与编辑", "功能验证点", "P0", "打开新增弹窗", 36, 9, "新增与编辑", "功能验证点", "P0", "打开新增弹窗", 36,
[ [
"点击「新 增」", "点击「新 增」",
"断言弹出用户表单,含账号、姓名、联系方式及取消/确定", "断言弹出用户表单,含账号、姓名、联系方式及取消/确定",
], ],
"新增弹窗可见。", "新增弹窗可见。",
auto="USER-UNI-012",
), ),
case( case(
14, "新增与编辑", "功能验证点", "P1", "新增取消不落库", 38, 10, "新增与编辑", "功能验证点", "P1", "新增取消不落库", 38,
[ [
"打开新增,账号填 `${uniqLogin}` 后点「取 消」", "打开新增,账号填 `${uniqLogin}` 后点「取 消」",
"当前医院下列表查询该账号,断言不存在", "当前医院下列表查询该账号,断言不存在",
], ],
"取消后当前医院列表不出现该账号。", "取消后当前医院列表不出现该账号。",
auto="USER-UNI-013",
), ),
case( case(
15, "新增与编辑", "异常验证点", "P0", "必填为空拦截", 46, 11, "新增与编辑", "异常验证点", "P0", "必填为空拦截", 46,
[ [
"打开新增,账号或姓名留空后点「确 定」", "打开新增,账号或姓名留空后点「确 定」",
"断言被拦截,弹窗仍打开,不落库", "断言被拦截,弹窗仍打开,不落库",
], ],
"必填为空不可保存。", "必填为空不可保存。",
auto="USER-UNI-014",
), ),
case( case(
16, "新增与编辑", "功能验证点", "P0", "打开编辑并回填", 39, 12, "新增与编辑", "功能验证点", "P0", "打开编辑并回填", 39,
[ [
"点击首行「编辑」", "点击首行「编辑」",
"断言弹窗回填与该行登录账号、姓名、所属医院一致", "断言弹窗回填与该行登录账号、姓名、所属医院一致",
"点「取 消」", "点「取 消」",
], ],
"编辑回填正确;本条不保存。", "编辑回填正确;本条不保存。",
pre="列表至少 1 条", auto="USER-UNI-016", pre="列表至少 1 条",
), ),
case( case(
17, "筛选与查询", "功能验证点", "P0", "高级筛选按状态过滤", 42, 13, "筛选与查询", "功能验证点", "P0", "高级筛选按科室过滤", 16,
[ [
"展开高级筛选,状态下拉选择「启用」或「禁用」", "点击「查 询」后读取首行 `${sampleDept}`(空或 `--` 时换一行)",
"点击「筛 选」", "展开高级筛选,选择该科室后点「筛 选」",
"断言列表仅保留对应状态用户(无匹配则空态且总数为 0)", "断言结果非空或空态明确;有数据时可见行「所属科室」与所选科室一致",
], ],
"按状态过滤成功;无报错。", "按科室过滤成功;无报错;结果所属科室与条件一致。",
auto="USER-UNI-017", pre="默认查询至少 1 条且所属科室非空/--,否则 skip",
), ),
case( case(
18, "列表与分页", "功能验证点", "P1", "切换20条每页页面正常且列表同步", 27, 14, "列表与分页", "功能验证点", "P1", "切换20条每页页面正常且列表同步", 27,
[ [
"点击「查 询」,记录分页「共 N 条」与当前页可见行数", "点击「查 询」,记录分页「共 N 条」与当前页可见行数",
"分页区切换每页条数为「20 条/页」", "分页区切换每页条数为「20 条/页」",
...@@ -478,20 +431,19 @@ PAGES = [ ...@@ -478,20 +431,19 @@ PAGES = [
], ],
"切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。", "切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。",
pre="默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip", pre="默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip",
auto="USER-UNI-018",
), ),
case( case(
19, "列表与分页", "功能验证点", "P1", "打开列设置面板", 25, 15, "列表与分页", "功能验证点", "P2", "打开列设置面板", 59,
[ [
"点击「查 询」等待列表稳定", "点击「查 询」等待列表稳定",
"点击表格工具栏列设置(齿轮)图标", "点击表格工具栏列设置(齿轮)图标",
"断言弹出面板含「列展示」「字段设置」「重置」,关闭面板", "断言弹出面板含「列展示」「字段设置」「重置」,关闭面板",
], ],
"列设置面板可打开;关键项可见;关闭后列表正常。", "列设置面板可打开;关键项可见;关闭后列表正常。",
pre="默认查询至少 1 条", auto="USER-UNI-019", pre="默认查询至少 1 条",
), ),
case( case(
20, "列表与分页", "功能验证点", "P1", "列设置取消勾选后列表列恢复", 25, 16, "列表与分页", "功能验证点", "P2", "列设置取消勾选后列表列恢复", 60,
[ [
"点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭", "点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭",
"断言该列表头隐藏;再次打开列设置勾选恢复并关闭", "断言该列表头隐藏;再次打开列设置勾选恢复并关闭",
...@@ -499,10 +451,9 @@ PAGES = [ ...@@ -499,10 +451,9 @@ PAGES = [
], ],
"取消勾选后表头隐藏;再勾选后表头恢复;无报错遮罩。", "取消勾选后表头隐藏;再勾选后表头恢复;无报错遮罩。",
pre="默认查询至少 1 条;存在可 toggle 的非核心列,否则 skip", pre="默认查询至少 1 条;存在可 toggle 的非核心列,否则 skip",
auto="USER-UNI-020",
), ),
case( case(
21, "授权", "功能验证点", "P0", "打开授权后取消不生效", 51, 17, "授权", "功能验证点", "P0", "打开授权后取消不生效", 51,
[ [
"点击首行「授权」", "点击首行「授权」",
"断言打开用户角色授权层,可见可勾选菜单/功能", "断言打开用户角色授权层,可见可勾选菜单/功能",
...@@ -510,10 +461,10 @@ PAGES = [ ...@@ -510,10 +461,10 @@ PAGES = [
"再次打开同一用户授权,断言勾选与操作前一致", "再次打开同一用户授权,断言勾选与操作前一致",
], ],
"授权层可打开;取消后勾选不落库。", "授权层可打开;取消后勾选不落库。",
pre="列表至少 1 条", auto="USER-UNI-021", pre="列表至少 1 条",
), ),
case( case(
22, "授权", "功能验证点", "P0", "授权弹框账号与所选行一致", 51, 18, "授权", "功能验证点", "P0", "授权弹框账号与所选行一致", 51,
[ [
"点击「查 询」,任选列表一条登录账号非空的行,记录 `${sampleLoginName}`、`${sampleUserName}`", "点击「查 询」,任选列表一条登录账号非空的行,记录 `${sampleLoginName}`、`${sampleUserName}`",
"点击该行「授权」", "点击该行「授权」",
...@@ -521,10 +472,10 @@ PAGES = [ ...@@ -521,10 +472,10 @@ PAGES = [
"关闭弹框", "关闭弹框",
], ],
"用户角色授权弹框打开;弹框账号与所选列表行一致。", "用户角色授权弹框打开;弹框账号与所选列表行一致。",
pre="列表至少 1 条且登录账号非空", auto="USER-UNI-022", pre="列表至少 1 条且登录账号非空",
), ),
case( case(
23, "授权", "功能验证点", "P1", "授权弹框添加移除角色行", 51, 19, "授权", "功能验证点", "P1", "授权弹框添加移除角色行", 51,
[ [
"点击「查 询」,任选列表一条数据行,点击该行「授权」", "点击「查 询」,任选列表一条数据行,点击该行「授权」",
"断言弹出用户角色授权弹框", "断言弹出用户角色授权弹框",
...@@ -534,7 +485,6 @@ PAGES = [ ...@@ -534,7 +485,6 @@ PAGES = [
], ],
"添加后列表下方新增一行;移除后新增行被删除;未保存不落库。", "添加后列表下方新增一行;移除后新增行被删除;未保存不落库。",
pre="列表至少 1 条;授权弹框含「添加/移除」(非仅权限树模式),否则 skip", pre="列表至少 1 条;授权弹框含「添加/移除」(非仅权限树模式),否则 skip",
auto="USER-UNI-023",
), ),
], ],
), ),
......
...@@ -391,8 +391,8 @@ def union_body(seq: Seq) -> str: ...@@ -391,8 +391,8 @@ def union_body(seq: Seq) -> str:
seq.add("高", "联系方式筛选", "按「联系方式」关键字查询,结果联系方式匹配"), seq.add("高", "联系方式筛选", "按「联系方式」关键字查询,结果联系方式匹配"),
seq.add( seq.add(
"高", "高",
"姓名+医院+科室+角色+状态组合查询", "姓名、所属医院、联系方式、科室、角色等条件组合查询",
"多条件同时生效,结果同时满足(USER-UNI-006)", "多条件同时生效,结果同时满足(USER-UNI-002)",
), ),
seq.add("中", "重置恢复默认", "点「重置」后医院等条件恢复默认(含默认医院),列表刷新"), seq.add("中", "重置恢复默认", "点「重置」后医院等条件恢复默认(含默认医院),列表刷新"),
seq.add("中", "展开高级筛选", "点「高级筛选」展开后可见所属医院、联系方式、科室、角色及「筛选」"), seq.add("中", "展开高级筛选", "点「高级筛选」展开后可见所属医院、联系方式、科室、角色及「筛选」"),
...@@ -480,7 +480,7 @@ def union_body(seq: Seq) -> str: ...@@ -480,7 +480,7 @@ def union_body(seq: Seq) -> str:
seq.add("高", "打开编辑并回填", "行内「编辑」打开「编辑用户」,回填与列表行一致"), seq.add("高", "打开编辑并回填", "行内「编辑」打开「编辑用户」,回填与列表行一致"),
seq.add("高", "修改后确定落库", "修改后确定,本页列表与再次编辑回看均为新值"), seq.add("高", "修改后确定落库", "修改后确定,本页列表与再次编辑回看均为新值"),
seq.add("中", "编辑取消不保存", "修改后点取消,列表与再次打开仍为修改前值"), seq.add("中", "编辑取消不保存", "修改后点取消,列表与再次打开仍为修改前值"),
seq.add("中", "启用禁用规则同用户管理", "编辑弹窗启用/禁用后,按状态筛选能命中;禁用登录受限同 TP_USER_USR"), seq.add("中", "启用禁用规则同用户管理", "编辑弹窗可见启用/禁用;保存后仍能按姓名/账号查到该用户;禁用登录受限同 TP_USER_USR。扫描未见高级筛选「状态」字段,不按状态筛选写点"),
seq.add("中", "签名规则同用户管理", "编辑弹窗签名上传/展示规则同 TP_USER_USR,本页不重复展开"), seq.add("中", "签名规则同用户管理", "编辑弹窗签名上传/展示规则同 TP_USER_USR,本页不重复展开"),
seq.add("中", "Excel 导入入口可达", "「Excel导入」可打开导入流程"), seq.add("中", "Excel 导入入口可达", "「Excel导入」可打开导入流程"),
], ],
...@@ -677,8 +677,13 @@ def role_body(seq: Seq) -> str: ...@@ -677,8 +677,13 @@ def role_body(seq: Seq) -> str:
def quality(p: dict, count: int, tp_count: int) -> str: def quality(p: dict, count: int, tp_count: int) -> str:
kind = p["kind"] kind = p["kind"]
domains = {
"user": "页面访问与导航、筛选与查询、列表与分页、新增用户、编辑用户、授权、重置密码",
"union": "页面访问与导航、筛选与查询、列表与分页、新增与编辑、授权与重置密码",
"role": "页面访问与导航、左侧角色列表、右侧菜单权限树、角色增删改",
}[kind]
if kind == "user": if kind == "user":
cover = "导航、筛选查询、列表分页、新增/编辑(含签名与启用禁用)、授权、重置密码、权限与登录异常、同模块衔接" cover = "导航、筛选查询、列表分页、列设置、新增/编辑(含签名与启用禁用)、授权、重置密码、权限与登录异常、同模块衔接"
pending = ( pending = (
"① 新增弹窗扫描未稳定打开,字段以编辑弹窗与 UI-SPEC 为准 " "① 新增弹窗扫描未稳定打开,字段以编辑弹窗与 UI-SPEC 为准 "
"② 授权层扫描未采集到,勾选控件以现网为准 " "② 授权层扫描未采集到,勾选控件以现网为准 "
...@@ -687,13 +692,22 @@ def quality(p: dict, count: int, tp_count: int) -> str: ...@@ -687,13 +692,22 @@ def quality(p: dict, count: int, tp_count: int) -> str:
"⑤ UI-SPEC 截图未见「查询」,现网扫描有「查询」,测试点以扫描为准 " "⑤ UI-SPEC 截图未见「查询」,现网扫描有「查询」,测试点以扫描为准 "
"⑥ 列表序号可勾选但无批量工具栏,按无批量处理" "⑥ 列表序号可勾选但无批量工具栏,按无批量处理"
) )
downstream = (
"可转结构化 MD/CSV(`npm run generate:md-user`);扫描 `npm run discover:scan-user`;"
"Playwright `tests/spd/basic-user.spec.ts` + `pages/spd/basic-user.page.ts`"
)
elif kind == "union": elif kind == "union":
cover = "导航、所属医院筛选、列顺序差异、跨院不覆盖/不串改;启用禁用/签名/任职/授权持久化引用 TP_USER_USR,本页不重复展开" cover = "导航、所属医院筛选、列顺序差异、跨院不覆盖/不串改、列设置;启用禁用/签名/任职/授权持久化引用 TP_USER_USR,本页不重复展开"
pending = ( pending = (
"① 新增/授权弹窗扫描未稳定打开,以现网为准 " "① 新增/授权弹窗扫描未稳定打开,以现网为准 "
"② 跨院登录账号按拒绝且不覆盖写点,若产品允许共用需改判定 " "② 跨院登录账号按拒绝且不覆盖写点,若产品允许共用需改判定 "
"③ 所属医院默认值与顶栏约定以现网为准 " "③ 所属医院默认值与顶栏约定以现网为准 "
"④ 签名/启用禁用/任职明细/改密后登录同用户管理对应 TP,本页只保留医院维度" "④ 签名/启用禁用/任职明细/改密后登录同用户管理对应 TP,本页只保留医院维度 "
"⑤ UI-SPEC 测试关注点写了「状态」筛选,现网扫描仅在编辑弹窗有启用/禁用、无高级筛选「状态」;按扫描写点,自动化 `USER-UNI-013` 改为按科室过滤(TP_USER_UNI_016)"
)
downstream = (
"可转结构化 MD/CSV(`npm run generate:md-user`);扫描 `npm run discover:scan-user`;"
"Playwright `tests/spd/basic-user-union.spec.ts` + `pages/spd/basic-user-union.page.ts`"
) )
else: else:
cover = "导航(左右分栏无分页)、左栏角色搜索、右栏权限树勾选联动/保存/未保存切角色、角色增删改与用户页下拉同步" cover = "导航(左右分栏无分页)、左栏角色搜索、右栏权限树勾选联动/保存/未保存切角色、角色增删改与用户页下拉同步"
...@@ -703,15 +717,21 @@ def quality(p: dict, count: int, tp_count: int) -> str: ...@@ -703,15 +717,21 @@ def quality(p: dict, count: int, tp_count: int) -> str:
"③ 未保存切角色按「提示后丢弃、不静默写入」写点,若自动保存需改判定 " "③ 未保存切角色按「提示后丢弃、不静默写入」写点,若自动保存需改判定 "
"④ 本页无授权弹窗、无分页、无所属医院" "④ 本页无授权弹窗、无分页、无所属医院"
) )
downstream = (
"可转结构化 MD/CSV(`npm run generate:md-user`);扫描 `npm run discover:scan-user`;"
"Playwright `tests/spd/basic-role.spec.ts` + `pages/spd/basic-role.page.ts`"
)
return f"""## 质量自检摘要 return f"""## 质量自检摘要
| 项 | 结论 | | 项 | 结论 |
|----|------| |----|------|
| 范围 | 在范围内:{domains}。明确排除:断网/弱网、视觉像素、导出文件内容校验(未在本文件展开)。未声称需求 100% 全覆盖。 |
| 覆盖 | 已覆盖:{cover};共 **{tp_count}** 条原子测试点(未声称全覆盖,断网类与仓库其它基础管理页同样省略) | | 覆盖 | 已覆盖:{cover};共 **{tp_count}** 条原子测试点(未声称全覆盖,断网类与仓库其它基础管理页同样省略) |
| 分类 | 功能 / 边界 / 异常 / 集成四类均已按功能点拆分;无某类时保留标题并说明 | | 分类 | 功能 / 边界 / 异常 / 集成四类均已按功能点拆分;无某类时保留标题并说明 |
| 去重 | 登录拦截、会话过期与同模块其它用户管理页规则相同,本页仅保留菜单/URL/字段差异;编号前缀 `{p["prefix"]}` | | 去重 | 登录拦截、会话过期与同模块其它用户管理页规则相同,本页仅保留菜单/URL/字段差异;编号前缀 `{p["prefix"]}` |
| 待评审项 | {pending} | | 待评审项 | {pending} |
| 下游 | 可转结构化 MD/CSV;扫描 `npm run discover:scan-user`;自动化建议 `pages/spd/{p["pageKey"]}.page.ts` | | 报表能力 | 非统计报表 iframe 页,不适用第四节能力清单;本页为整页渲染,无报表 iframe |
| 下游 | {downstream} |
""" """
...@@ -721,8 +741,20 @@ def render_testpoint(p: dict, scan: dict) -> tuple[str, int]: ...@@ -721,8 +741,20 @@ def render_testpoint(p: dict, scan: dict) -> tuple[str, int]:
nav = nav_section(p, scan["url"], seq) nav = nav_section(p, scan["url"], seq)
if p["kind"] == "user": if p["kind"] == "user":
body = user_body(seq) body = user_body(seq)
extra = [
seq.add("低", "打开列设置面板", "列设置齿轮可打开,含列展示/字段设置/重置;关闭后面板消失、列表仍在(自动化 `USER-USR-017`)"),
seq.add("低", "列设置隐藏后再恢复", "取消勾选非核心列后表头隐藏,再勾选恢复;勾选状态与表头一致(自动化 `USER-USR-018`)"),
]
needle = "(中)|跳至指定页|验证要点:分页「跳至」输入合法页码后跳到对应页|关联需求:UI-SPEC-SPD-USER-001"
body = body.replace(needle, needle + "\n" + "\n".join(extra), 1)
elif p["kind"] == "union": elif p["kind"] == "union":
body = union_body(seq) body = union_body(seq)
extra = [
seq.add("低", "打开列设置面板", "列设置齿轮可打开,含列展示/字段设置/重置;关闭后面板消失、列表仍在"),
seq.add("低", "列设置隐藏后再恢复", "取消勾选非核心列后表头隐藏,再勾选恢复;勾选状态与表头一致(一对多:CSV `USER-UNI-015`/`016`)"),
]
needle = "(中)|跳至指定页|验证要点:分页「跳至」输入合法页码后跳到对应页|关联需求:UI-SPEC-SPD-USER-002"
body = body.replace(needle, needle + "\n" + "\n".join(extra), 1)
else: else:
body = role_body(seq) body = role_body(seq)
md = f"""# 基础管理-用户管理-{p['name']} — 测试点分析 md = f"""# 基础管理-用户管理-{p['name']} — 测试点分析
...@@ -733,6 +765,8 @@ def render_testpoint(p: dict, scan: dict) -> tuple[str, int]: ...@@ -733,6 +765,8 @@ def render_testpoint(p: dict, scan: dict) -> tuple[str, int]:
**元素扫描**:`data/scan/{p["pageKey"]}-elements.json`({scan["elementCount"]} 项) **元素扫描**:`data/scan/{p["pageKey"]}-elements.json`({scan["elementCount"]} 项)
**定位器**:`locators/spd/{p["pageKey"]}.locator.yaml` **定位器**:`locators/spd/{p["pageKey"]}.locator.yaml`
**本次生成范围**:见本文件既有功能域(历史生成,无独立 `testpoint/scope` 文件);本次按 ui-test-point-rules 补齐文档头与质量自检,不扩大范围
--- ---
## 基础设置 · 用户管理 · {p['name']} ## 基础设置 · 用户管理 · {p['name']}
......
...@@ -76,7 +76,7 @@ PAGES = [ ...@@ -76,7 +76,7 @@ PAGES = [
"断言结果行「厂商名称」包含关键字"], "断言结果行「厂商名称」包含关键字"],
"厂商名称列与关键字匹配。", "厂商名称列与关键字匹配。",
pre="器械注册人至少 1 条且厂商名称非空", auto="VENDOR-QRY-004"), pre="器械注册人至少 1 条且厂商名称非空", auto="VENDOR-QRY-004"),
case(5, "Tab 与搜索", "功能验证点", "P1", "清空搜索恢复", 13, case(5, "Tab 与搜索", "功能验证点", "P1", "清空搜索恢复", 19,
["在已过滤状态下清空搜索框并回车", "断言分页总数不少于过滤后"], ["在已过滤状态下清空搜索框并回车", "断言分页总数不少于过滤后"],
"恢复更宽结果集。", pre="已按厂商名称过滤", auto="VENDOR-QRY-005"), "恢复更宽结果集。", pre="已按厂商名称过滤", auto="VENDOR-QRY-005"),
case(6, "Tab 与搜索", "异常验证点", "P1", "无匹配空态(mock反例)", 17, case(6, "Tab 与搜索", "异常验证点", "P1", "无匹配空态(mock反例)", 17,
...@@ -92,8 +92,12 @@ PAGES = [ ...@@ -92,8 +92,12 @@ PAGES = [
["厂商名称仅填空格并回车", "断言按空查询处理或提示,不白屏"], ["厂商名称仅填空格并回车", "断言按空查询处理或提示,不白屏"],
"不报错;行为明确。"), "不报错;行为明确。"),
case(10, "页面进入", "异常验证点", "P0", "未登录访问拦截", 5, case(10, "页面进入", "异常验证点", "P0", "未登录访问拦截", 5,
["清除登录态后直接打开厂商查询 URL", "断言跳转登录或不展示业务列表"], ["新建独立 BrowserContext,不加载 auth/user.json(test.use 空 storageState)",
"未登录不可见厂商数据。"), "直接打开厂商查询 URL(/spd/basic/mfrs/paltform/mfrsName/factory)",
"断言跳转登录或不展示业务列表"],
"未登录不可见厂商数据。",
pre="使用独立 context,storageState 为空(不加载 test_1 登录态)",
auto="VENDOR-QRY-010"),
case(11, "Tab 与搜索", "集成验证点", "P1", "厂商名称与资质管理进院供应商可对照", 18, case(11, "Tab 与搜索", "集成验证点", "P1", "厂商名称与资质管理进院供应商可对照", 18,
["记录厂商查询中某厂商名称(若有)", "打开供应商资质管理,用同源关键字检索或目视对照", ["记录厂商查询中某厂商名称(若有)", "打开供应商资质管理,用同源关键字检索或目视对照",
"断言不出现明显主数据串名(允许一方无记录)"], "断言不出现明显主数据串名(允许一方无记录)"],
...@@ -504,21 +508,32 @@ def render_md(p: dict) -> str: ...@@ -504,21 +508,32 @@ def render_md(p: dict) -> str:
return "\n".join(lines) return "\n".join(lines)
def is_unlogin(c: dict) -> bool:
return "未登录" in c["title"]
def render_csv_rows(p: dict) -> list[dict]: def render_csv_rows(p: dict) -> list[dict]:
rows = [] rows = []
# 账号密码仅存 .env / auth.setup;CSV 不写明文凭据 # 账号密码仅存 .env / auth.setup;CSV 不写明文凭据
account = "测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限" account = "测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限"
unlogin = "测试环境可访问;使用独立 context,storageState 为空(不加载 test_1 登录态)"
for c in p["cases"]: for c in p["cases"]:
case_id = c.get("auto") or f"{p['code']}-{c['seq']:03d}" case_id = c.get("auto") or f"{p['code']}-{c['seq']:03d}"
pre_parts = [account] if is_unlogin(c):
if c["seq"] > 1: pre_parts = [unlogin]
pre_parts.append(f"已进入{p['name']}页({p['path']})") if c["pre"]:
if c["pre"]: pre_parts.append(c["pre"])
pre_parts.append(c["pre"]) steps = "\n".join(f"{i}. {s}" for i, s in enumerate(c["steps"], start=1))
steps = "\n".join( else:
[f"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录"] pre_parts = [account]
+ [f"{i + 1}. {s}" for i, s in enumerate(c["steps"], start=1)] if c["seq"] > 1:
) pre_parts.append(f"已进入{p['name']}页({p['path']},无 iframe)")
if c["pre"]:
pre_parts.append(c["pre"])
steps = "\n".join(
[f"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录"]
+ [f"{i + 1}. {s}" for i, s in enumerate(c["steps"], start=1)]
)
rows.append( rows.append(
{ {
"用例编号": case_id, "用例编号": case_id,
......
...@@ -175,40 +175,36 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -175,40 +175,36 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
chunks: list[str] = [] chunks: list[str] = []
if kind == "query": if kind == "query":
func_points = [
nxt("高", "器械注册人 Tab", "切换「器械注册人」后列表为注册人数据,分页「共 N 条」与结果一致"),
nxt("高", "经销商 Tab", "切换「经销商」后列表为经销商数据,总数与器械注册人可不同"),
nxt("高", "按厂商名称搜索", "输入厂商名称关键字后结果名称匹配,无匹配时空态明确"),
nxt("高", "列表核心列", "展示序号、厂商编号、厂商名称、产地、详细地址、简称、开户银行、银行账号"),
nxt("中", "分页翻页", "总条数大于一页时可翻至第 2 页,页码高亮且首行更换;不足 2 页则该点不适用(自动化 `VENDOR-QRY-008`)"),
]
bound_points = [
nxt("中", "超长厂商名称关键字", "超长输入可查询或被截断并提示,不白屏"),
nxt("低", "仅空格关键字", "按空查询处理或提示,不报错"),
]
except_points = [
nxt("中", "查询接口失败", "失败有错误提示,列表不白屏"),
nxt("低", "无匹配空态", "总数为 0,空态文案明确"),
]
integ_points = [
nxt("中", "Tab 与分页总数一致", "当前 Tab 下可见行逻辑与「共 N 条」一致"),
]
func_points += [
nxt("中", "清空搜索恢复更宽结果", "按厂商名称过滤后清空搜索并回车,分页总数不少于过滤后(自动化 `VENDOR-QRY-005`)"),
nxt("中", "切换每页条数", "当前页行数 = min(总条数, 所选档位);总条数不变;切换 Tab 后分页重置合理;无数据则不适用"),
]
chunks.append( chunks.append(
section( section(
"Tab、搜索与列表", "Tab、搜索与列表",
[ [
( ("功能验证点", func_points),
"功能验证点", ("边界验证点", bound_points),
[ ("异常验证点", except_points),
nxt("高", "器械注册人 Tab", "切换「器械注册人」后列表为注册人数据,分页「共 N 条」与结果一致"), ("集成验证点", integ_points),
nxt("高", "经销商 Tab", "切换「经销商」后列表为经销商数据,总数与器械注册人可不同"),
nxt("高", "按厂商名称搜索", "输入厂商名称关键字后结果名称匹配,无匹配时空态明确"),
nxt("高", "列表核心列", "展示序号、厂商编号、厂商名称、产地、详细地址、简称、开户银行、银行账号"),
nxt("中", "分页与每页条数", "翻页与每页条数切换正确,切换 Tab 后分页重置合理"),
],
),
(
"边界验证点",
[
nxt("中", "超长厂商名称关键字", "超长输入可查询或被截断并提示,不白屏"),
nxt("低", "仅空格关键字", "按空查询处理或提示,不报错"),
],
),
(
"异常验证点",
[
nxt("中", "查询接口失败", "失败有错误提示,列表不白屏"),
nxt("低", "无匹配空态", "总数为 0,空态文案明确"),
],
),
(
"集成验证点",
[
nxt("中", "Tab 与分页总数一致", "当前 Tab 下可见行逻辑与「共 N 条」一致"),
],
),
], ],
) )
) )
...@@ -490,6 +486,46 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]: ...@@ -490,6 +486,46 @@ def body_for(p: dict, spec: str, start: int) -> tuple[str, int]:
return "\n".join(chunks), n return "\n".join(chunks), n
VENDOR_REVIEW = {
"query": "① 厂商名称搜索是回车还是即时过滤 ② 器械注册人与经销商分页是否独立 ③ 与供应商资质管理主数据对照口径",
"qual": "① 审批通过/驳回写操作是否纳入自动化(建议只读详情冒烟) ② 证件 Tab 切换后稳定列名 ③ 导出是否含当前 Tab 明细",
"audit": "① 审批通过/驳回写操作是否纳入自动化(建议只读详情冒烟) ② 可操作角色范围",
"diff": "① 内嵌/iframe 对比页加载失败时外壳是否假成功 ② 立即更新后列表刷新时效 ③ 导出是否含当前 Tab",
"scheme": "① 方案引用已停用指标时的拦截规则 ② 删除方案是否二次确认",
"indicator": "① 分类树搜索是即时过滤还是回车 ② 停用指标是否允许被方案继续引用",
"holiday": "① 默认假日与供应商自定义的覆盖优先级 ② 重置是否同时清掉自定义假日",
}
VENDOR_DOWNSTREAM = {
"vendor-query": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-query.spec.ts` + `pages/spd/vendor-query.page.ts`",
"vendor-qual": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-qual.spec.ts` + `pages/spd/vendor-qual.page.ts`",
"vendor-auth-first": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-auth-first.page.ts`",
"vendor-auth-final": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-auth-final.page.ts`",
"vendor-cert-first": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-cert-first.page.ts`",
"vendor-cert-final": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-audit.spec.ts` + `pages/spd/vendor-cert-final.page.ts`",
"vendor-cert-diff": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-cert-diff.spec.ts` + `pages/spd/vendor-cert-diff.page.ts`",
"vendor-eval-scheme": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-eval-scheme.spec.ts` + `pages/spd/vendor-eval-scheme.page.ts`",
"vendor-eval-indicator": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-eval-indicator.spec.ts` + `pages/spd/vendor-eval-indicator.page.ts`",
"vendor-holiday": "已落地 CSV/MD 与 Playwright:`tests/spd/vendor-holiday.spec.ts` + `pages/spd/vendor-holiday.page.ts`",
}
def h3_for(kind: str, p: dict) -> str:
if kind == "query":
return "页面访问与导航、Tab、搜索与列表"
if kind == "qual":
return "页面访问与导航、证件 Tab、效期与列表"
if kind == "audit":
return f"页面访问与导航、{p.get('doc_type', '变更')}列表与详情"
if kind == "diff":
return "页面访问与导航、差异筛选与对比结果"
if kind == "scheme":
return "页面访问与导航、方案筛选与维护"
if kind == "indicator":
return "页面访问与导航、指标分类树与指标列表"
return "页面访问与导航、默认假日与供应商自定义"
def render_testpoint(p: dict, summary: dict) -> str: def render_testpoint(p: dict, summary: dict) -> str:
row = summary[p["pageKey"]] row = summary[p["pageKey"]]
url = row["url"] url = row["url"]
...@@ -497,6 +533,12 @@ def render_testpoint(p: dict, summary: dict) -> str: ...@@ -497,6 +533,12 @@ def render_testpoint(p: dict, summary: dict) -> str:
spec = f"UI-SPEC-SPD-{p['specId']}" spec = f"UI-SPEC-SPD-{p['specId']}"
nav, last = nav_section(p, url, spec) nav, last = nav_section(p, url, spec)
body, last = body_for(p, spec, last) body, last = body_for(p, spec, last)
kind = p["kind"]
report = (
"非统计报表页,不适用第四节报表能力清单;本页含内嵌/iframe 对比区,导航与内嵌加载失败须分开断言"
if kind == "diff"
else "非统计报表 iframe 页,不适用第四节能力清单;本页为整页渲染,无报表 iframe"
)
return f"""# 基础管理-厂商管理-{p['name']} — 测试点分析 return f"""# 基础管理-厂商管理-{p['name']} — 测试点分析
**关联需求**:`knowledge-base/08-UI专项规范库/{spec}.md` **关联需求**:`knowledge-base/08-UI专项规范库/{spec}.md`
...@@ -505,6 +547,8 @@ def render_testpoint(p: dict, summary: dict) -> str: ...@@ -505,6 +547,8 @@ def render_testpoint(p: dict, summary: dict) -> str:
**元素扫描**:`data/scan/{p['pageKey']}-elements.json`({count} 项) **元素扫描**:`data/scan/{p['pageKey']}-elements.json`({count} 项)
**定位器**:`locators/spd/{p['pageKey']}.locator.yaml` **定位器**:`locators/spd/{p['pageKey']}.locator.yaml`
**本次生成范围**:见本文件既有功能域(历史生成,无独立 `testpoint/scope` 文件);本次按 ui-test-point-rules 补齐文档头与质量自检,不扩大范围
--- ---
## 基础设置 · 厂商管理 · {p['name']} ## 基础设置 · 厂商管理 · {p['name']}
...@@ -517,10 +561,15 @@ def render_testpoint(p: dict, summary: dict) -> str: ...@@ -517,10 +561,15 @@ def render_testpoint(p: dict, summary: dict) -> str:
## 质量自检摘要 ## 质量自检摘要
- 覆盖:页面访问、筛选/Tab、列表分页、关键写操作或详情、权限与登录异常、同模块衔接。 | 项 | 结论 |
- 颗粒度:一条测试点只验证一件独立事;优先级已标高/中/低。 |----|------|
- 待评审项:审批通过/驳回写操作是否自动化(建议只读详情冒烟);注册证差异对比 iframe 断言策略。 | 范围 | 在范围内:{h3_for(kind, p)}。明确排除:断网/弱网、视觉像素、导出文件内容校验(未在本文件展开)。未声称需求 100% 全覆盖。 |
- 下游:可转结构化 MD/CSV;自动化需先补 Page Object(`pages/spd/{p['pageKey']}.page.ts`)。 | 覆盖 | 已写功能域:{h3_for(kind, p)};共 **{last}** 条原子测试点(未声称全覆盖) |
| 分类 | 功能 / 边界 / 异常 / 集成四类均已按功能点拆分 |
| 去重 | 登录拦截、会话过期与同模块其它厂商管理页规则相同,本页仅保留菜单/URL/字段差异;编号前缀 `{p["prefix"]}` |
| 待评审项 | {VENDOR_REVIEW.get(kind, "见正文;无新增冲突")} |
| 报表能力 | {report} |
| 下游 | {VENDOR_DOWNSTREAM.get(p["pageKey"], f"可转结构化 MD/CSV;Playwright `pages/spd/{p['pageKey']}.page.ts`")} |
""" """
......
...@@ -950,14 +950,22 @@ def quality(p: dict, el_count: int, tp_count: int) -> str: ...@@ -950,14 +950,22 @@ def quality(p: dict, el_count: int, tp_count: int) -> str:
"② 本页无新增/导出/改审批人;写配置只在审批流程设置 " "② 本页无新增/导出/改审批人;写配置只在审批流程设置 "
"③ 详情扫描打开「采购计划详情」,其它业务类型详情标题以现网为准" "③ 详情扫描打开「采购计划详情」,其它业务类型详情标题以现网为准"
) )
domains = {
"zone": "页面访问与导航、左侧库房树、右侧库区列表与分页、库区搜索、新增库区、编辑库区、停用与启用、人员管理、货位管理、同步库房信息",
"probe": "页面访问与导航、筛选与查询、列表与分页、打开绑定货位弹窗、弹窗筛选货位、勾选货位并保存绑定",
"def": "页面访问与导航、左侧业务类型、默认审批流、自定义审批流、条件配置、保存与重置",
"inst": "页面访问与导航、筛选与查询、列表与分页、审批详情",
}[kind]
return f"""## 质量自检摘要 return f"""## 质量自检摘要
| 项 | 结论 | | 项 | 结论 |
|----|------| |----|------|
| 范围 | 在范围内:{domains}。明确排除:断网/弱网、视觉像素、导出文件内容校验(未在本文件展开)。未声称需求 100% 全覆盖。 |
| 覆盖 | 已覆盖:{cover};共 **{tp_count}** 条原子测试点(未声称全覆盖,断网类与仓库其它基础管理页同样省略) | | 覆盖 | 已覆盖:{cover};共 **{tp_count}** 条原子测试点(未声称全覆盖,断网类与仓库其它基础管理页同样省略) |
| 分类 | 功能 / 边界 / 异常 / 集成四类均已按功能点拆分;无某类时保留标题并说明 | | 分类 | 功能 / 边界 / 异常 / 集成四类均已按功能点拆分;无某类时保留标题并说明 |
| 去重 | 登录拦截、会话过期与同模块其它页规则相同,本页仅保留菜单/URL/字段差异;编号前缀 `{p["prefix"]}` | | 去重 | 登录拦截、会话过期与同模块其它页规则相同,本页仅保留菜单/URL/字段差异;编号前缀 `{p["prefix"]}` |
| 待评审项 | {pending} | | 待评审项 | {pending} |
| 报表能力 | 非统计报表 iframe 页,不适用第四节能力清单;本页为整页渲染,无报表 iframe |
| 下游 | 可转结构化 MD/CSV;扫描 `npm run {p["scan_cmd"]}`;自动化建议 `{p["po"]}` | | 下游 | 可转结构化 MD/CSV;扫描 `npm run {p["scan_cmd"]}`;自动化建议 `{p["po"]}` |
""" """
...@@ -982,6 +990,8 @@ def render_testpoint(p: dict, scan: dict) -> tuple[str, int]: ...@@ -982,6 +990,8 @@ def render_testpoint(p: dict, scan: dict) -> tuple[str, int]:
**元素扫描**:`data/scan/{p["pageKey"]}-elements.json`({scan["elementCount"]} 项) **元素扫描**:`data/scan/{p["pageKey"]}-elements.json`({scan["elementCount"]} 项)
**定位器**:`locators/spd/{p["pageKey"]}.locator.yaml` **定位器**:`locators/spd/{p["pageKey"]}.locator.yaml`
**本次生成范围**:见本文件既有功能域(历史生成,无独立 `testpoint/scope` 文件);本次按 ui-test-point-rules 补齐文档头与质量自检,不扩大范围
--- ---
## 基础设置 · {p['group']} · {p['name']} ## 基础设置 · {p['group']} · {p['name']}
......
#!/usr/bin/env bash
# 一次性 LaunchAgent:2026-10-23 19:00 执行全量(PW_WORKERS=2),无需保持终端/Cursor 在线
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LABEL="com.mytestui.full-run-20261023"
PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
RUN_SCRIPT="${ROOT}/scripts/run-full-at-1900.sh"
UID_NUM="$(id -u)"
mkdir -p "${HOME}/Library/LaunchAgents" "${ROOT}/reports" "${ROOT}/logs"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${RUN_SCRIPT}</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Month</key>
<integer>10</integer>
<key>Day</key>
<integer>23</integer>
<key>Hour</key>
<integer>19</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>${ROOT}/logs/full-run-launchagent.stdout.log</string>
<key>StandardErrorPath</key>
<string>${ROOT}/logs/full-run-launchagent.stderr.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
<key>HOME</key>
<string>${HOME}</string>
</dict>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
EOF
launchctl bootout "gui/${UID_NUM}/${LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/${UID_NUM}" "$PLIST"
launchctl enable "gui/${UID_NUM}/${LABEL}" 2>/dev/null || true
echo "已注册 LaunchAgent:${LABEL}"
echo "触发时间:2026-10-23 19:00:00(本地时区)"
echo "命令:PW_WORKERS=2 npm run test:ci:full"
echo "日志:${ROOT}/reports/scheduled-full-20261023.log"
echo "查看:launchctl print gui/${UID_NUM}/${LABEL}"
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* 凭证不入库;源码变更后 hash 失效,必须重跑。 * 凭证不入库;源码变更后 hash 失效,必须重跑。
*/ */
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
...@@ -57,7 +57,9 @@ export function hashSources(files) { ...@@ -57,7 +57,9 @@ export function hashSources(files) {
const abs = path.join(ROOT, file); const abs = path.join(ROOT, file);
hash.update(file); hash.update(file);
hash.update('\0'); hash.update('\0');
hash.update(existsSync(abs) ? readFileSync(abs) : Buffer.from('')); if (existsSync(abs) && statSync(abs).isFile()) {
hash.update(readFileSync(abs));
}
hash.update('\0'); hash.update('\0');
} }
return hash.digest('hex'); return hash.digest('hex');
......
...@@ -74,7 +74,7 @@ ${formatSuiteList()} ...@@ -74,7 +74,7 @@ ${formatSuiteList()}
--keep-archives=N 只保留最近 N 份 reports/ci-*(0=跑完即删本次归档,等价 PW_KEEP_ARCHIVES=0) --keep-archives=N 只保留最近 N 份 reports/ci-*(0=跑完即删本次归档,等价 PW_KEEP_ARCHIVES=0)
Env: Env:
SUITE / PW_WORKERS / PW_RETRIES / RUN_FULL / BASE_URL / TEST_USER / TEST_PASS SUITE / PW_WORKERS / PW_RETRIES / RUN_FULL / BASE_URL / TEST_USER / TEST_PASS / TEST_USER_2 / TEST_PASS_2
SHARDS / PW_SHARD / PW_FULLY_PARALLEL=1 / PW_MAX_FAILURES SHARDS / PW_SHARD / PW_FULLY_PARALLEL=1 / PW_MAX_FAILURES
RUN_QUARANTINE=1 只跑 config/quarantine.json 隔离项 RUN_QUARANTINE=1 只跑 config/quarantine.json 隔离项
SKIP_PREFLIGHT=1 跳过站点探活 SKIP_PREFLIGHT=1 跳过站点探活
...@@ -84,7 +84,6 @@ Env: ...@@ -84,7 +84,6 @@ Env:
PW_KEEP_TRACES=N 归档内只留最大 N 个 trace/截图附件(默认全留) PW_KEEP_TRACES=N 归档内只留最大 N 个 trace/截图附件(默认全留)
PW_KEEP_ARCHIVES=N 本机只保留最近 N 份 ci-<buildId>(默认全留;0=跑完即删) PW_KEEP_ARCHIVES=N 本机只保留最近 N 份 ci-<buildId>(默认全留;0=跑完即删)
PW_KEEP_INTERMEDIATE=1 保留分片目录与 blob-report(默认合并后删除) PW_KEEP_INTERMEDIATE=1 保留分片目录与 blob-report(默认合并后删除)
INCLUDE_DEBUG_SPECS=1 纳入 *.stat-*.spec.ts 调试用例
`); `);
} }
......
#!/usr/bin/env bash
# 2026-10-23 19:00 全量回归(PW_WORKERS=2);由 LaunchAgent 触发
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOG="$ROOT/reports/scheduled-full-20261023.log"
LABEL="com.mytestui.full-run-20261023"
PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
UID_NUM="$(id -u)"
mkdir -p "$ROOT/reports"
export PATH="/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${PATH:-}"
{
echo "[$(date '+%Y-%m-%d %H:%M:%S')] LaunchAgent 触发:PW_WORKERS=2 npm run test:ci:full"
cd "$ROOT"
export CI=1
export PW_WORKERS=2
set +e
/usr/bin/caffeinate -dims npm run test:ci:full
EC=$?
set -e
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 结束 exit=$EC"
launchctl bootout "gui/${UID_NUM}/${LABEL}" 2>/dev/null || true
rm -f "$PLIST"
} >> "$LOG" 2>&1
exit "${EC:-0}"
#!/usr/bin/env bash
# 脱离终端启动全量 CI;caffeinate 防止锁屏/空闲触发系统休眠中断用例
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG="${ROOT}/reports/full-run-${STAMP}.log"
PID_FILE="${ROOT}/reports/full-run.pid"
CAFF_PID_FILE="${ROOT}/reports/caffeinate-full.pid"
mkdir -p "${ROOT}/reports"
export PATH="/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${PATH:-}"
nohup /usr/bin/caffeinate -dims /bin/bash -c "
set -euo pipefail
cd \"${ROOT}\"
export CI=1
export PW_WORKERS=\"${PW_WORKERS:-2}\"
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] detached 全量 (caffeinate): PW_WORKERS=\$PW_WORKERS npm run test:ci:full\"
npm run test:ci:full
EC=\$?
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] 结束 exit=\$EC\"
exit \$EC
" >> "$LOG" 2>&1 < /dev/null &
echo $! > "$PID_FILE"
# 外层 bash 会立刻退出;记录 caffeinate 父进程需从 log/ pgrep 查看。另写 wait 包装:
if pgrep -f "caffeinate -dims /bin/bash -c" >/dev/null 2>&1; then
pgrep -f "caffeinate -dims /bin/bash -c" | head -1 > "$CAFF_PID_FILE" || true
fi
echo "LOG=$LOG"
echo "PID=$(cat "$PID_FILE")"
echo "说明: 已用 caffeinate -dims,锁屏不影响;请勿合盖休眠/关机。"
...@@ -18,8 +18,9 @@ ...@@ -18,8 +18,9 @@
2. 断言列表可加载;不足两页则 skip","第 2 页可加载。","" 2. 断言列表可加载;不足两页则 skip","第 2 页可加载。",""
"VENDOR-QRY-009","基础设置 · 厂商管理","厂商查询 · Tab 与搜索","仅空格关键字搜索","低","测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入厂商查询页(/spd/basic/mfrs/paltform/mfrsName/factory,无 iframe)","1. 厂商名称仅填空格并回车 "VENDOR-QRY-009","基础设置 · 厂商管理","厂商查询 · Tab 与搜索","仅空格关键字搜索","低","测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入厂商查询页(/spd/basic/mfrs/paltform/mfrsName/factory,无 iframe)","1. 厂商名称仅填空格并回车
2. 断言按空查询处理或提示,不白屏","不报错;行为明确。","" 2. 断言按空查询处理或提示,不白屏","不报错;行为明确。",""
"VENDOR-QRY-010","基础设置 · 厂商管理","厂商查询 · 页面进入","未登录访问拦截","高","测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入厂商查询页(/spd/basic/mfrs/paltform/mfrsName/factory,无 iframe)","1. 清除登录态后直接打开厂商查询 URL "VENDOR-QRY-010","基础设置 · 厂商管理","厂商查询 · 页面进入","未登录访问拦截","高","测试环境可访问;使用独立 context,storageState 为空(不加载 test_1 登录态)","1. 新建独立 BrowserContext,不加载 auth/user.json(test.use 空 storageState)
2. 断言跳转登录或不展示业务列表","未登录不可见厂商数据。","" 2. 直接打开厂商查询 URL(/spd/basic/mfrs/paltform/mfrsName/factory)
3. 断言跳转登录或不展示业务列表","未登录不可见厂商数据。",""
"VENDOR-QRY-011","基础设置 · 厂商管理","厂商查询 · Tab 与搜索","厂商名称与资质管理进院供应商可对照","中","测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入厂商查询页(/spd/basic/mfrs/paltform/mfrsName/factory,无 iframe)","1. 记录厂商查询中某厂商名称(若有) "VENDOR-QRY-011","基础设置 · 厂商管理","厂商查询 · Tab 与搜索","厂商名称与资质管理进院供应商可对照","中","测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入厂商查询页(/spd/basic/mfrs/paltform/mfrsName/factory,无 iframe)","1. 记录厂商查询中某厂商名称(若有)
2. 打开供应商资质管理,用同源关键字检索或目视对照 2. 打开供应商资质管理,用同源关键字检索或目视对照
3. 断言不出现明显主数据串名(允许一方无记录)","厂商主数据跨菜单可对照,不串名。","" 3. 断言不出现明显主数据串名(允许一方无记录)","厂商主数据跨菜单可对照,不串名。",""
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果 用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
USER-UNI-001,基础设置 · 用户管理,医共体用户管理 · 页面进入,菜单进入医共体用户管理页,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-001,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,默认查询列表有数据或空态明确,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 按菜单「基础设置 → 用户管理 → 医共体用户管理」进入,或 `openPath` 打开 `/spd/basic/user/platform/personCom`
3. 断言 URL 含 `/spd/basic/user/platform/personCom`
4. 断言所属医院筛选、工具栏、列表列头与分页可见",进入医共体用户管理页;不误入用户管理或角色管理。,
USER-UNI-002,基础设置 · 用户管理,医共体用户管理 · 页面进入,未登录访问拦截,高,测试环境可访问;使用独立 context,storageState 为空(不加载 test_1 登录态),"1. 新建独立 BrowserContext,不加载 auth/user.json
2. 清除登录态后直接打开 `/spd/basic/user/platform/personCom`
3. 断言跳转登录或不展示用户业务列表",未登录不可见医共体用户数据。,
USER-UNI-003,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,默认查询列表有数据或空态明确,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」并等待列表稳定 2. 点击「查 询」并等待列表稳定
3. 断言出现数据行,或空态且分页总数为 0",查询无报错;有数据时「共 N 条」N≥1。, 3. 断言出现数据行,或空态且分页总数为 0",查询无报错;有数据时「共 N 条」N≥1。,
USER-UNI-004,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,姓名或登录账号关键字查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-002,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,姓名、所属医院、联系方式、科室、角色等条件组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 从首行读取 `${sampleUserName}` 或 `${sampleLoginName}` 2. 点击「查 询」后读取首行 `${sampleUserName}`、`${sampleLoginName}`、`${sampleHospital}`、`${samplePhone}`、`${sampleDept}`、`${sampleRole}`(科室或联系方式为 `--`/空时换一行)
3. 在「姓名」框填入关键字,点击「查 询」 3. 填写姓名、所属医院,展开高级筛选填写「联系方式」、选择科室与角色后点「筛 选」(无则点「查 询」)
4. 断言结果行姓名或登录账号匹配",查询结果与关键字匹配。, 4. 断言结果非空;首行登录账号与采样行一致;可见行姓名/所属医院/联系方式/科室/角色与已填条件一致",组合查询无报错;结果与采样条件一致且同时满足全部已填条件。,
USER-UNI-005,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,所属医院筛选,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-003,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,重置恢复默认医院,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 记录当前默认 `${sampleHospital}` 与分页总数
3. 切换「所属医院」为另一可选项(仅 1 家则 skip)
4. 点击「查 询」
5. 断言结果行「所属医院」均为所选值,「共 N 条」可与默认不同",列表仅该院用户;总数随医院变化。,
USER-UNI-006,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,姓名+医院+科室+角色+状态组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」后读取首行 `${sampleUserName}`、`${sampleLoginName}`、`${sampleHospital}`、`${sampleDept}`、`${sampleRole}`、`${sampleStatus}`(科室为 `--` 时换一行)
3. 填写姓名、所属医院、科室、角色,展开高级筛选选择 `${sampleStatus}` 后点「筛 选」(无则点「查 询」)
4. 断言结果非空;首行登录账号与采样行一致;可见行姓名/所属医院/科室/角色/状态与已填条件一致",组合查询无报错;结果与采样条件一致且同时满足全部已填条件。,
USER-UNI-007,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,重置恢复默认医院,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 更改所属医院或姓名后点击「重 置」 2. 更改所属医院或姓名后点击「重 置」
3. 断言所属医院恢复进入页时的默认值,列表刷新",医院等条件恢复默认。, 3. 断言所属医院恢复进入页时的默认值,列表刷新",医院等条件恢复默认。,
USER-UNI-008,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,展开高级筛选,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-004,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,展开高级筛选,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「高级筛选」 2. 点击「高级筛选」
3. 断言可见所属医院、联系方式、科室、角色及「筛 选」",高级筛选含医院维度字段。, 3. 断言可见所属医院、联系方式、科室、角色及「筛 选」",高级筛选含医院维度字段。,
USER-UNI-009,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-005,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,无匹配空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 姓名框填入 `__NOT_EXIST_USER__99999`,点击「查 询」 2. 姓名框填入 `__NOT_EXIST_USER__99999`,点击「查 询」
3. 断言空态且分页总数为 0",无匹配不报错;总数为 0。, 3. 断言空态且分页总数为 0",无匹配不报错;总数为 0。,
USER-UNI-010,基础设置 · 用户管理,医共体用户管理 · 列表与分页,列表列顺序含所属医院,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-006,基础设置 · 用户管理,医共体用户管理 · 列表与分页,列表列顺序含所属医院,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 断言列顺序为登录账号、姓名、联系方式、所属医院、所属科室、医院员工编号、角色、科室任职数、操作 2. 断言列顺序为登录账号、姓名、联系方式、所属医院、所属科室、医院员工编号、角色、科室任职数、操作
3. 勿用用户管理页列断言(用户管理无所属医院且姓名在登录账号前)",列顺序与用户管理页不同;含所属医院。, 3. 勿用用户管理页列断言(用户管理无所属医院且姓名在登录账号前)",列顺序与用户管理页不同;含所属医院。,
USER-UNI-011,基础设置 · 用户管理,医共体用户管理 · 列表与分页,分页翻至第 2 页,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);总条数大于一页,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-007,基础设置 · 用户管理,医共体用户管理 · 列表与分页,分页翻至第 2 页,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);总条数大于一页,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 记录第 1 页首行登录账号 2. 记录第 1 页首行登录账号
3. 翻到第 2 页 3. 翻到第 2 页
4. 断言数据更换且所属医院筛选仍保持",翻页成功;医院条件不丢失。, 4. 断言数据更换且所属医院筛选仍保持",翻页成功;医院条件不丢失。,
USER-UNI-012,基础设置 · 用户管理,医共体用户管理 · 列表与分页,与用户管理页同一用户字段一致,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);两页都能查到同一账号,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-008,基础设置 · 用户管理,医共体用户管理 · 列表与分页,与用户管理页同一用户字段一致,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);两页都能查到同一账号,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 记录本页首行 `${sampleLoginName}`、姓名、所属科室、角色 2. 记录本页首行 `${sampleLoginName}`、姓名、所属科室、角色
3. 打开用户管理页,按登录账号查询同一用户 3. 打开用户管理页,按登录账号查询同一用户
4. 断言姓名、角色、所属科室一致;本页仅多「所属医院」",同一登录账号跨页字段一致。, 4. 断言姓名、角色、所属科室一致;本页仅多「所属医院」",同一登录账号跨页字段一致。,
USER-UNI-013,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,打开新增弹窗,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-009,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,打开新增弹窗,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「新 增」 2. 点击「新 增」
3. 断言弹出用户表单,含账号、姓名、联系方式及取消/确定",新增弹窗可见。, 3. 断言弹出用户表单,含账号、姓名、联系方式及取消/确定",新增弹窗可见。,
USER-UNI-014,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,新增取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-010,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,新增取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开新增,账号填 `${uniqLogin}` 后点「取 消」 2. 打开新增,账号填 `${uniqLogin}` 后点「取 消」
3. 当前医院下列表查询该账号,断言不存在",取消后当前医院列表不出现该账号。, 3. 当前医院下列表查询该账号,断言不存在",取消后当前医院列表不出现该账号。,
USER-UNI-015,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,必填为空拦截,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-011,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,必填为空拦截,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开新增,账号或姓名留空后点「确 定」 2. 打开新增,账号或姓名留空后点「确 定」
3. 断言被拦截,弹窗仍打开,不落库",必填为空不可保存。, 3. 断言被拦截,弹窗仍打开,不落库",必填为空不可保存。,
USER-UNI-016,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,打开编辑并回填,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-012,基础设置 · 用户管理,医共体用户管理 · 新增与编辑,打开编辑并回填,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击首行「编辑」 2. 点击首行「编辑」
3. 断言弹窗回填与该行登录账号、姓名、所属医院一致 3. 断言弹窗回填与该行登录账号、姓名、所属医院一致
4. 点「取 消」",编辑回填正确;本条不保存。, 4. 点「取 消」",编辑回填正确;本条不保存。,
USER-UNI-017,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,高级筛选按状态过滤,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-013,基础设置 · 用户管理,医共体用户管理 · 筛选与查询,高级筛选按科室过滤,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条且所属科室非空/--,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 展开高级筛选,状态下拉选择「启用」或「禁用」 2. 点击「查 询」后读取首行 `${sampleDept}`(空或 `--` 时换一行)
3. 点击「筛 选」 3. 展开高级筛选,选择该科室后点「筛 选」
4. 断言列表仅保留对应状态用户(无匹配则空态且总数为 0)",按状态过滤成功;无报错。, 4. 断言结果非空或空态明确;有数据时可见行「所属科室」与所选科室一致",按科室过滤成功;无报错;结果所属科室与条件一致。,
USER-UNI-018,基础设置 · 用户管理,医共体用户管理 · 列表与分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip","1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-014,基础设置 · 用户管理,医共体用户管理 · 列表与分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip","1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」,记录分页「共 N 条」与当前页可见行数 2. 点击「查 询」,记录分页「共 N 条」与当前页可见行数
3. 分页区切换每页条数为「20 条/页」 3. 分页区切换每页条数为「20 条/页」
4. 断言分页显示 20 条/页;总条数 N 不变;当前页行数为 min(N,20) 且与切换前不同;所属医院筛选、表头可见无报错",切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。, 4. 断言分页显示 20 条/页;总条数 N 不变;当前页行数为 min(N,20) 且与切换前不同;所属医院筛选、表头可见无报错",切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。,
USER-UNI-019,基础设置 · 用户管理,医共体用户管理 · 列表与分页,打开列设置面板,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-015,基础设置 · 用户管理,医共体用户管理 · 列表与分页,打开列设置面板,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」等待列表稳定 2. 点击「查 询」等待列表稳定
3. 点击表格工具栏列设置(齿轮)图标 3. 点击表格工具栏列设置(齿轮)图标
4. 断言弹出面板含「列展示」「字段设置」「重置」,关闭面板",列设置面板可打开;关键项可见;关闭后列表正常。, 4. 断言弹出面板含「列展示」「字段设置」「重置」,关闭面板",列设置面板可打开;关键项可见;关闭后列表正常。,
USER-UNI-020,基础设置 · 用户管理,医共体用户管理 · 列表与分页,列设置取消勾选后列表列恢复,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条;存在可 toggle 的非核心列,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-016,基础设置 · 用户管理,医共体用户管理 · 列表与分页,列设置取消勾选后列表列恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);默认查询至少 1 条;存在可 toggle 的非核心列,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭 2. 点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭
3. 断言该列表头隐藏;再次打开列设置勾选恢复并关闭 3. 断言该列表头隐藏;再次打开列设置勾选恢复并关闭
4. 断言该列表头重新可见",取消勾选后表头隐藏;再勾选后表头恢复;无报错遮罩。, 4. 断言该列表头重新可见",取消勾选后表头隐藏;再勾选后表头恢复;无报错遮罩。,
USER-UNI-021,基础设置 · 用户管理,医共体用户管理 · 授权,打开授权后取消不生效,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-017,基础设置 · 用户管理,医共体用户管理 · 授权,打开授权后取消不生效,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击首行「授权」 2. 点击首行「授权」
3. 断言打开用户角色授权层,可见可勾选菜单/功能 3. 断言打开用户角色授权层,可见可勾选菜单/功能
4. 如可勾选则改一处勾选后点取消/关闭 4. 如可勾选则改一处勾选后点取消/关闭
5. 再次打开同一用户授权,断言勾选与操作前一致",授权层可打开;取消后勾选不落库。, 5. 再次打开同一用户授权,断言勾选与操作前一致",授权层可打开;取消后勾选不落库。,
USER-UNI-022,基础设置 · 用户管理,医共体用户管理 · 授权,授权弹框账号与所选行一致,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条且登录账号非空,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-018,基础设置 · 用户管理,医共体用户管理 · 授权,授权弹框账号与所选行一致,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条且登录账号非空,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」,任选列表一条登录账号非空的行,记录 `${sampleLoginName}`、`${sampleUserName}` 2. 点击「查 询」,任选列表一条登录账号非空的行,记录 `${sampleLoginName}`、`${sampleUserName}`
3. 点击该行「授权」 3. 点击该行「授权」
4. 断言弹出用户角色授权弹框,弹框展示的账号信息与所选行登录账号、姓名一致 4. 断言弹出用户角色授权弹框,弹框展示的账号信息与所选行登录账号、姓名一致
5. 关闭弹框",用户角色授权弹框打开;弹框账号与所选列表行一致。, 5. 关闭弹框",用户角色授权弹框打开;弹框账号与所选列表行一致。,
USER-UNI-023,基础设置 · 用户管理,医共体用户管理 · 授权,授权弹框添加移除角色行,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条;授权弹框含「添加/移除」(非仅权限树模式),否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-UNI-019,基础设置 · 用户管理,医共体用户管理 · 授权,授权弹框添加移除角色行,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 医共体用户管理」权限;已进入医共体用户管理页(/spd/basic/user/platform/personCom,无 iframe);列表至少 1 条;授权弹框含「添加/移除」(非仅权限树模式),否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」,任选列表一条数据行,点击该行「授权」 2. 点击「查 询」,任选列表一条数据行,点击该行「授权」
3. 断言弹出用户角色授权弹框 3. 断言弹出用户角色授权弹框
4. 记录弹框内可「移除」的角色行数,点击「添加」 4. 记录弹框内可「移除」的角色行数,点击「添加」
5. 断言可移除行数 +1;点击新增行的「移除」 5. 断言可移除行数 +1;点击新增行的「移除」
6. 断言可移除行数恢复为添加前;关闭弹框且不保存",添加后列表下方新增一行;移除后新增行被删除;未保存不落库。, 6. 断言可移除行数恢复为添加前;关闭弹框且不保存",添加后列表下方新增一行;移除后新增行被删除;未保存不落库。,
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果 用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
USER-USR-001,基础设置 · 用户管理,用户管理 · 筛选与查询,姓名+科室+角色+状态组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-001,基础设置 · 用户管理,用户管理 · 筛选与查询,姓名+科室+角色+状态组合查询,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」后读取首行 `${sampleUserName}`、`${sampleLoginName}`、`${sampleDept}`、`${sampleRole}`、`${sampleStatus}`(角色取截断前列名;状态优先读列表「状态」列,否则按行内启用/禁用文案推断) 2. 点击「查 询」后读取首行 `${sampleUserName}`、`${sampleLoginName}`、`${sampleDept}`、`${sampleRole}`、`${sampleStatus}`(角色取截断前列名;状态优先读列表「状态」列,否则按行内启用/禁用文案推断)
3. 填写姓名、科室、角色,展开高级筛选选择 `${sampleStatus}` 后点「筛 选」(无「筛 选」则点「查 询」) 3. 填写姓名、科室、角色,展开高级筛选选择 `${sampleStatus}` 后点「筛 选」(无「筛 选」则点「查 询」)
4. 断言结果非空;首行登录账号与采样行一致;可见行姓名/科室/角色/状态与已填条件一致",组合查询无报错;查询结果与首行采样条件一致且同时满足全部已填条件。, 4. 断言结果非空;首行登录账号与采样行一致;可见行姓名/科室/角色/状态与已填条件一致",组合查询无报错;查询结果与首行采样条件一致且同时满足全部已填条件。,
USER-USR-002,基础设置 · 用户管理,用户管理 · 筛选与查询,重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-002,基础设置 · 用户管理,用户管理 · 筛选与查询,重置恢复默认,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 填写姓名等筛选条件后点击「重 置」 2. 填写姓名等筛选条件后点击「重 置」
3. 断言筛选字段恢复默认,列表按默认规则刷新",条件恢复默认;列表刷新。, 3. 断言筛选字段恢复默认,列表按默认规则刷新",条件恢复默认;列表刷新。,
USER-USR-003,基础设置 · 用户管理,用户管理 · 筛选与查询,展开高级筛选,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-003,基础设置 · 用户管理,用户管理 · 筛选与查询,展开高级筛选,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「高级筛选」 2. 点击「高级筛选」
3. 断言展开后面板可见姓名/科室/角色/状态及「重 置」「筛 选」",高级筛选面板展开;关键字段与筛选按钮可见。, 3. 断言展开后面板可见姓名/科室/角色/状态及「重 置」「筛 选」",高级筛选面板展开;关键字段与筛选按钮可见。,
USER-USR-004,基础设置 · 用户管理,用户管理 · 筛选与查询,高级筛选按状态过滤,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-004,基础设置 · 用户管理,用户管理 · 筛选与查询,高级筛选按状态过滤,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 展开高级筛选,状态下拉选择「启用」或「禁用」 2. 展开高级筛选,状态下拉选择「启用」或「禁用」
3. 点击「筛 选」 3. 点击「筛 选」
4. 断言列表仅保留对应状态用户(无匹配则空态且总数为 0)",按状态过滤成功;无报错。, 4. 断言列表仅保留对应状态用户(无匹配则空态且总数为 0)",按状态过滤成功;无报错。,
USER-USR-005,基础设置 · 用户管理,用户管理 · 筛选与查询,无匹配结果空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-005,基础设置 · 用户管理,用户管理 · 筛选与查询,无匹配结果空态(mock反例),中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 姓名框填入 `__NOT_EXIST_USER__99999` 2. 姓名框填入 `__NOT_EXIST_USER__99999`
3. 点击「查 询」 3. 点击「查 询」
4. 断言空态或「暂无数据」,分页总数为 0",无匹配时不报错;空态明确;总数为 0。, 4. 断言空态或「暂无数据」,分页总数为 0",无匹配时不报错;空态明确;总数为 0。,
USER-USR-006,基础设置 · 用户管理,用户管理 · 列表与分页,分页翻至第 2 页,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询总条数大于一页,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-006,基础设置 · 用户管理,用户管理 · 列表与分页,分页翻至第 2 页,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询总条数大于一页,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 记录第 1 页首行登录账号 2. 记录第 1 页首行登录账号
3. 点击页码「2」或「下一页」 3. 点击页码「2」或「下一页」
4. 断言页码高亮为 2,首行账号与第 1 页不同",翻页成功;数据更换。, 4. 断言页码高亮为 2,首行账号与第 1 页不同",翻页成功;数据更换。,
USER-USR-007,基础设置 · 用户管理,用户管理 · 新增用户,打开新增用户弹窗,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-007,基础设置 · 用户管理,用户管理 · 新增用户,打开新增用户弹窗,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「新 增」 2. 点击「新 增」
3. 断言弹出用户表单,可见账号、姓名、联系方式、医院员工编号、启用/禁用、「取 消」「确 定」",新增弹窗可见;关键字段与按钮齐全。, 3. 断言弹出用户表单,可见账号、姓名、联系方式、医院员工编号、启用/禁用、「取 消」「确 定」",新增弹窗可见;关键字段与按钮齐全。,
USER-USR-008,基础设置 · 用户管理,用户管理 · 新增用户,新增取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-008,基础设置 · 用户管理,用户管理 · 新增用户,新增取消不落库,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开新增,账号填 `${uniqLogin}`,姓名填任意合法值 2. 打开新增,账号填 `${uniqLogin}`,姓名填任意合法值
3. 点击「取 消」 3. 点击「取 消」
4. 按账号查询,断言无该记录",弹窗关闭;列表不出现 `${uniqLogin}`。, 4. 按账号查询,断言无该记录",弹窗关闭;列表不出现 `${uniqLogin}`。,
USER-USR-009,基础设置 · 用户管理,用户管理 · 新增用户,必填为空拦截,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-009,基础设置 · 用户管理,用户管理 · 新增用户,必填为空拦截,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe),"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开新增,账号或姓名留空后点击「确 定」 2. 打开新增,账号或姓名留空后点击「确 定」
3. 断言出现必填提示,弹窗仍打开",确定被拦截;不落库;弹窗不关闭。, 3. 断言出现必填提示,弹窗仍打开",确定被拦截;不落库;弹窗不关闭。,
USER-USR-010,基础设置 · 用户管理,用户管理 · 新增用户,登录账号重复拦截,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询至少 1 条且登录账号非空,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-010,基础设置 · 用户管理,用户管理 · 新增用户,登录账号重复拦截,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询至少 1 条且登录账号非空,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开新增,账号填入已存在的 `${sampleLoginName}`,补齐其它必填后点「确 定」 2. 打开新增,账号填入已存在的 `${sampleLoginName}`,补齐其它必填后点「确 定」
3. 断言保存失败并提示,弹窗仍打开 3. 断言保存失败并提示,弹窗仍打开
4. 查询该账号,断言原用户未被覆盖",重复账号被拒绝;不覆盖原用户。, 4. 查询该账号,断言原用户未被覆盖",重复账号被拒绝;不覆盖原用户。,
USER-USR-011,基础设置 · 用户管理,用户管理 · 编辑用户,打开编辑用户弹窗并回填,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-011,基础设置 · 用户管理,用户管理 · 编辑用户,打开编辑用户弹窗并回填,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击首行「编辑」 2. 点击首行「编辑」
3. 断言弹窗标题含「编辑用户」 3. 断言弹窗标题含「编辑用户」
4. 断言账号、姓名与该行 `${sampleLoginName}`、`${sampleUserName}` 一致(允许规范化空白)",编辑弹窗可见;回填与列表行一致。, 4. 断言账号、姓名与该行 `${sampleLoginName}`、`${sampleUserName}` 一致(允许规范化空白)",编辑弹窗可见;回填与列表行一致。,
USER-USR-012,基础设置 · 用户管理,用户管理 · 编辑用户,编辑取消不保存,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-012,基础设置 · 用户管理,用户管理 · 编辑用户,编辑取消不保存,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开首行编辑,记录原姓名 2. 打开首行编辑,记录原姓名
3. 将姓名改为临时值后点「取 消」 3. 将姓名改为临时值后点「取 消」
4. 再次打开编辑,断言姓名仍为原值",取消后列表与回看均为修改前姓名。, 4. 再次打开编辑,断言姓名仍为原值",取消后列表与回看均为修改前姓名。,
USER-USR-013,基础设置 · 用户管理,用户管理 · 编辑用户,编辑时账号只读,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-013,基础设置 · 用户管理,用户管理 · 编辑用户,编辑时账号只读,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开首行编辑 2. 打开首行编辑
3. 断言登录账号不可改(disabled/readonly 或无法输入)",编辑态登录账号只读。, 3. 断言登录账号不可改(disabled/readonly 或无法输入)",编辑态登录账号只读。,
USER-USR-014,基础设置 · 用户管理,用户管理 · 编辑用户,编辑弹窗可见签名图片,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-014,基础设置 · 用户管理,用户管理 · 编辑用户,编辑弹窗可见签名图片,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 打开首行编辑 2. 打开首行编辑
3. 断言可见「签名图片」区域(有图则展示,无图则上传入口可见) 3. 断言可见「签名图片」区域(有图则展示,无图则上传入口可见)
4. 点「取 消」,不保存",签名图片控件可见;本条不上传、不保存。, 4. 点「取 消」,不保存",签名图片控件可见;本条不上传、不保存。,
USER-USR-015,基础设置 · 用户管理,用户管理 · 授权,打开授权后取消不生效,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-015,基础设置 · 用户管理,用户管理 · 授权,打开授权后取消不生效,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击首行「授权」 2. 点击首行「授权」
3. 断言打开授权层(弹窗或权限树),可见可勾选菜单/功能 3. 断言打开授权层(弹窗或权限树),可见可勾选菜单/功能
4. 如可勾选则改一处勾选后点取消/关闭 4. 如可勾选则改一处勾选后点取消/关闭
5. 再次打开同一用户授权,断言勾选与操作前一致",授权层可打开;取消后勾选不落库。, 5. 再次打开同一用户授权,断言勾选与操作前一致",授权层可打开;取消后勾选不落库。,
USER-USR-016,基础设置 · 用户管理,用户管理 · 列表与分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip","1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-016,基础设置 · 用户管理,用户管理 · 列表与分页,切换20条每页页面正常且列表同步,中,"测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询总条数大于 10 且切换前可见行数不等于 min(N,20),否则 skip","1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」,记录分页「共 N 条」与当前页可见行数 2. 点击「查 询」,记录分页「共 N 条」与当前页可见行数
3. 分页区切换每页条数为「20 条/页」 3. 分页区切换每页条数为「20 条/页」
4. 断言分页显示 20 条/页;总条数 N 不变;当前页行数为 min(N,20) 且与切换前不同;筛选区、表头可见无报错",切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。, 4. 断言分页显示 20 条/页;总条数 N 不变;当前页行数为 min(N,20) 且与切换前不同;筛选区、表头可见无报错",切换 20 条/页成功;页面正常;列表可见行数随每页条数同步变化且总数不变。,
USER-USR-017,基础设置 · 用户管理,用户管理 · 列表与分页,打开列设置面板,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-017,基础设置 · 用户管理,用户管理 · 列表与分页,打开列设置面板,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询至少 1 条,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」等待列表稳定 2. 点击「查 询」等待列表稳定
3. 点击表格工具栏列设置(齿轮)图标 3. 点击表格工具栏列设置(齿轮)图标
4. 断言弹出面板含「列展示」「字段设置」「重置」,关闭面板",列设置面板可打开;关键项可见;关闭后列表正常。, 4. 断言弹出面板含「列展示」「字段设置」「重置」,关闭面板",列设置面板可打开;关键项可见;关闭后列表正常。,
USER-USR-018,基础设置 · 用户管理,用户管理 · 列表与分页,列设置取消勾选后列表列恢复,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询至少 1 条;存在可 toggle 的非核心列,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-018,基础设置 · 用户管理,用户管理 · 列表与分页,列设置取消勾选后列表列恢复,低,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);默认查询至少 1 条;存在可 toggle 的非核心列,否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭 2. 点击「查 询」后打开列设置,取消勾选一列非核心字段并关闭
3. 断言该列表头隐藏;再次打开列设置勾选恢复并关闭 3. 断言该列表头隐藏;再次打开列设置勾选恢复并关闭
4. 断言该列表头重新可见",取消勾选后表头隐藏;再勾选后表头恢复;无报错遮罩。, 4. 断言该列表头重新可见",取消勾选后表头隐藏;再勾选后表头恢复;无报错遮罩。,
USER-USR-019,基础设置 · 用户管理,用户管理 · 授权,授权弹框账号与所选行一致,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条且登录账号非空,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-019,基础设置 · 用户管理,用户管理 · 授权,授权弹框账号与所选行一致,高,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条且登录账号非空,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」,任选列表一条登录账号非空的行,记录 `${sampleLoginName}`、`${sampleUserName}` 2. 点击「查 询」,任选列表一条登录账号非空的行,记录 `${sampleLoginName}`、`${sampleUserName}`
3. 点击该行「授权」 3. 点击该行「授权」
4. 断言弹出用户角色授权弹框,弹框展示的账号信息与所选行登录账号、姓名一致 4. 断言弹出用户角色授权弹框,弹框展示的账号信息与所选行登录账号、姓名一致
5. 关闭弹框",用户角色授权弹框打开;弹框账号与所选列表行一致。, 5. 关闭弹框",用户角色授权弹框打开;弹框账号与所选列表行一致。,
USER-USR-020,基础设置 · 用户管理,用户管理 · 授权,授权弹框添加移除角色行,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条;授权弹框含「添加/移除」(非仅权限树模式),否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录 USER-USR-020,基础设置 · 用户管理,用户管理 · 授权,授权弹框添加移除角色行,中,测试环境可访问;正例账号 test_1 已通过 auth.setup 写入 storageState 登录(密码仅存 .env TEST_PASS,勿写入本表);具备「基础设置 → 用户管理 → 用户管理」权限;已进入用户管理页(/spd/basic/user/platform/person,无 iframe);列表至少 1 条;授权弹框含「添加/移除」(非仅权限树模式),否则 skip,"1. 确认 chromium 已加载 storageState(auth.setup,正例账号 test_1),无需在用例内重复登录
2. 点击「查 询」,任选列表一条数据行,点击该行「授权」 2. 点击「查 询」,任选列表一条数据行,点击该行「授权」
3. 断言弹出用户角色授权弹框 3. 断言弹出用户角色授权弹框
4. 记录弹框内可「移除」的角色行数,点击「添加」 4. 记录弹框内可「移除」的角色行数,点击「添加」
5. 断言可移除行数 +1;点击新增行的「移除」 5. 断言可移除行数 +1;点击新增行的「移除」
6. 断言可移除行数恢复为添加前;关闭弹框且不保存",添加后列表下方新增一行;移除后新增行被删除;未保存不落库。, 6. 断言可移除行数恢复为添加前;关闭弹框且不保存",添加后列表下方新增一行;移除后新增行被删除;未保存不落库。,
...@@ -167,7 +167,7 @@ ...@@ -167,7 +167,7 @@
--- ---
##### 【P1】【TC-UI-ADMIT-PAP-010】【TP_ADMIT_PAP_023】跳至指定页 ##### 【P1】【TC-UI-ADMIT-PAP-010】【TP_ADMIT_PAP_024】跳至指定页
- 前置条件:总页数不少于 2,分页存在「跳至」 - 前置条件:总页数不少于 2,分页存在「跳至」
- 操作步骤: - 操作步骤:
......
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