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