Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
test
autotest-playwright-web-spd
Commits
b8b4c69c
Commit
b8b4c69c
authored
Nov 04, 2026
by
liguangyu06
Browse files
新增产品管理全链路自动化资产,并修复详情层/树页/路径串页等稳定问题。
parent
297e887d
Changes
234
Hide whitespace changes
Inline
Side-by-side
scripts/capture-prodm-overlays.mjs
0 → 100644
View file @
b8b4c69c
#!/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
);
});
}
scripts/generate-prodm-md.py
0 → 100644
View file @
b8b4c69c
#!/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'
]
:
03
d
}
"
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'
]
:
03
d
}
"
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
)
:
2
d
}
cases
\t
{
md_path
.
name
}
"
)
print
(
f
"done pages=
{
len
(
pages
)
}
cases=
{
total
}
"
)
if
__name__
==
"__main__"
:
main
()
scripts/generate-prodm-playwright.py
0 → 100644
View file @
b8b4c69c
#!/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
()
scripts/generate-prodm-testpoints.py
0 → 100644
View file @
b8b4c69c
#!/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
()
scripts/json-to-locator-yaml.mjs
View file @
b8b4c69c
...
...
@@ -9,6 +9,7 @@
* 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-iface
* node scripts/json-to-locator-yaml.mjs --all-prodm
*/
import
path
from
'
node:path
'
;
import
{
fileURLToPath
}
from
'
node:url
'
;
...
...
@@ -100,6 +101,32 @@ const NEW_PAGES = {
'
iface-mat-push
'
:
{
pageTitle
:
'
基础设置 > 接口管理 > 物资字典推送记录
'
,
module
:
'
spd-basic
'
},
'
iface-req-query
'
:
{
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
=
{
...
...
@@ -161,6 +188,34 @@ const GROUPS = {
'
iface-req-query
'
,
'
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
),
};
...
...
@@ -186,7 +241,7 @@ async function main() {
const
args
=
process
.
argv
.
slice
(
2
);
if
(
!
args
.
length
)
{
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
);
}
...
...
scripts/scan-interactive-elements.mjs
View file @
b8b4c69c
...
...
@@ -253,11 +253,22 @@ const SCAN_SCRIPT = `(() => {
}
function sectionOf(el) {
const detail = el.closest('.put-page');
if (detail) {
const title = detail.querySelector('.ant-descriptions-title, .ant-page-header-heading-title');
const t = title ? norm(title.textContent) : '';
return t ? 'detail:' + t : 'detail';
// SPD 自定义详情页(产品详情 / 调价详情 / 审批页等)
const customDetail = el.closest('.detail-spin-page, .approve-page, .xc-layout-tb.xc-page, .put-page');
if (customDetail) {
const titleEl = customDetail.querySelector(
'.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');
if (modal) {
...
...
@@ -819,7 +830,18 @@ function buildYaml(scan) {
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
}
`
;
let
finalKey
;
...
...
@@ -849,7 +871,8 @@ function buildYaml(scan) {
const
rec
=
el
.
recommended
;
const
pw
=
toPlaywrightExpr
(
rec
,
frameLabel
);
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
(
` section:
${
JSON
.
stringify
(
section
.
replace
(
/token=
[^
&
]
+/g
,
'
token=${token}
'
))}
`
);
if
(
frameLabel
!==
'
main
'
)
{
...
...
@@ -929,7 +952,11 @@ export function writeYamlFromScanJson(jsonPath, config = {}) {
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
=
{
url
:
page
.
url
(),
title
:
await
page
.
title
(),
...
...
@@ -1036,13 +1063,15 @@ async function clickQueryIfPresent(reportFrame) {
}
function
fingerprint
(
el
)
{
return
[
el
.
section
,
el
.
type
,
el
.
id
||
''
,
el
.
role
||
''
,
el
.
label
||
''
,
el
.
text
||
''
,
el
.
placeholder
||
''
].
join
(
'
|
'
);
return
el
ementFingerprint
(
el
);
}
async
function
closeScanOverlays
(
page
)
{
for
(
let
i
=
0
;
i
<
6
;
i
+=
1
)
{
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
*闭/
})
.
first
();
if
(
await
back
.
isVisible
().
catch
(()
=>
false
))
{
...
...
@@ -1050,7 +1079,12 @@ async function closeScanOverlays(page) {
await
page
.
waitForTimeout
(
400
);
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
))
{
await
backText
.
click
({
timeout
:
4
_000
,
force
:
true
}).
catch
(()
=>
undefined
);
await
page
.
waitForTimeout
(
400
);
...
...
@@ -1072,9 +1106,11 @@ async function closeScanOverlays(page) {
continue
;
}
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
;
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
;
btn
.
click
();
return
true
;
...
...
scripts/scan-prodm-batch.mjs
0 → 100644
View file @
b8b4c69c
#!/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
);
});
scripts/scan-prodm-overlays-locators.mjs
0 → 100644
View file @
b8b4c69c
#!/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
);
});
scripts/sync-docs-ui-specs.py
View file @
b8b4c69c
#!/usr/bin/env python3
"""将 docs/ 下页面截图同步至 knowledge-base/08-UI专项规范库(统计报表、准入、厂商、用户、库房、流程、科室管理、接口管理等)。"""
"""将 docs/ 下页面截图同步至 knowledge-base/08-UI专项规范库(统计报表、准入、厂商、
产品管理、
用户、库房、流程、科室管理、接口管理等)。"""
from
__future__
import
annotations
...
...
@@ -383,8 +383,10 @@ USER_PAGES: list[dict] = [
(
"产品管理"
,
"产品核对重新发起、产品维护、物料信息管理、产品初审核对、产品供应关系维护、产品物价核对、"
"院区产品设置、院区单元包设置、中心库上下限建议(AI)、请领出库渠道设置、结算点设置、"
"调价管理、产品信息变更、变更单明细查询、外围系统产品新增、外围系统产品变更管理、产品查询 等"
,
"院区产品设置、院区单元包设置、中心库上下限建议(AI)、请领出库渠道设置、中心库上下限建议、"
"结算点设置、调价管理、产品信息变更、变更单明细查询、外围系统产品新增、外围系统产品变更管理、"
"产品DI查询、缺货目录维护、中心库库区产品设置、缺货目录明细、物料信息变更、产品DI变更、"
"产品资质齐全规则、产品资质一览、物料变更单明细查询"
,
),
],
},
...
...
@@ -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/基础管理-科室管理-*)
DEPTM_PAGES
:
list
[
dict
]
=
[
{
...
...
@@ -891,6 +1196,7 @@ PAGES: list[dict] = (
STAT_PAGES
+
ADMIT_PAGES
+
VENDOR_PAGES
+
PRODM_PAGES
+
USER_PAGES
+
WH_PAGES
+
FLOW_PAGES
...
...
@@ -903,6 +1209,99 @@ def png_files(folder: Path) -> list[Path]:
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
:
title
=
f
"科室管理-
{
page
[
'name'
]
}
页面说明"
menu
=
f
"**基础设置** → **科室管理** → **
{
page
[
'name'
]
}
**"
...
...
@@ -1550,6 +1949,8 @@ author: ""
def
render_spec
(
page
:
dict
,
spec_id
:
str
)
->
str
:
if
spec_id
.
startswith
(
"IFACE-"
):
return
render_iface_spec
(
page
,
spec_id
)
if
spec_id
.
startswith
(
"PRODM-"
):
return
render_prodm_spec
(
page
,
spec_id
)
if
spec_id
.
startswith
(
"DEPTM-"
):
return
render_deptm_spec
(
page
,
spec_id
)
if
spec_id
.
startswith
(
"ADMIT-"
):
...
...
testcase/基础管理-产品管理-中心库上下限建议-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-中心库上下限建议(AI)-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-中心库库区产品设置-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-产品DI变更-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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) 且与切换前不同",每页条数切换生效;总数不变;列表同步。,
testcase/基础管理-产品管理-产品DI查询-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
testcase/基础管理-产品管理-产品供应关系维护-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言无未捕获异常(不校验文件内容)",导出流程可启动。,
testcase/基础管理-产品管理-产品信息变更-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-产品初审核对-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-产品核对重新发起-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-产品物价核对-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
testcase/基础管理-产品管理-产品维护-UI自动化测试用例-01.csv
0 → 100644
View file @
b8b4c69c
用例编号,功能模块,功能测试点,用例标题,优先级,前置条件,测试步骤,预期结果,实际结果
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. 断言出现必填提示,弹层仍打开,列表不刷新为成功态",确定被拦截;不落库。,
Prev
1
…
3
4
5
6
7
8
9
10
11
12
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment