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
297802d0
Commit
297802d0
authored
Nov 05, 2026
by
liguangyu06
Browse files
修复 spd-ui-verify #5 共享层失败簇,并稳住准入组合查询超时。
parent
79b13a1c
Changes
19
Hide whitespace changes
Inline
Side-by-side
core/column-header-match.util.ts
View file @
297802d0
...
...
@@ -6,10 +6,10 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
院区名称
'
,
'
院区
'
],
[
'
27位医保编码
'
,
'
医保编码
'
,
'
医保码
'
],
[
'
医保单件名称
'
,
'
单件名称
'
,
'
医保名称
'
],
[
'
HIS编码
'
,
'
HIS码
'
,
'
His编码
'
],
[
'
HIS编码
'
,
'
HIS码
'
,
'
His编码
'
,
'
HIS
'
],
[
'
结算单标签
'
,
'
标签
'
],
[
'
物料编号
'
,
'
物料编码
'
,
'
产品编号
'
,
'
产品编码
'
,
'
物资编码
'
,
'
商品编码
'
],
[
'
物料名称
'
,
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
,
'
产品俗称
'
],
[
'
物料名称
'
,
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
,
'
产品俗称
'
,
'
产品
'
],
// 现网手术套包散件弹窗等场景:表头为「物料28名称」等带编号前缀
[
'
规格型号
'
,
'
规格
'
,
'
产品规格
'
],
[
'
器械注册人
'
,
'
注册人
'
],
...
...
@@ -17,6 +17,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
销售商(自定义)
'
,
'
销售商(自定义)名称
'
,
'
销售商(自定义)列表
'
,
'
进院供应商
'
,
'
进院供应商名称
'
,
'
默认销售商
'
,
...
...
@@ -38,6 +39,21 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
供应商
'
,
'
供应商名称
'
,
'
二级供应商
'
,
'
二级供应商名称
'
,
'
默认二级供应商
'
],
[
'
院区
'
,
'
院区名称
'
],
[
'
产品类型
'
,
'
耗材类型
'
,
'
物资类型
'
],
[
'
请领数量
'
,
'
请领
'
],
[
'
库存上限
'
,
'
上限
'
],
[
'
库存下限
'
,
'
下限
'
],
[
'
建议补货单元数
'
,
'
建议补货
'
],
[
'
是否收费
'
,
'
收费
'
,
'
收费项目
'
,
'
收费名称
'
],
[
'
科室名称
'
,
'
科室
'
,
'
科室编码
'
],
[
'
厂商名称
'
,
'
厂商
'
,
'
厂商编号
'
],
[
'
结算规则
'
,
'
结算
'
],
[
'
对接系统名称
'
,
'
对接系统
'
],
[
'
第三方系统厂商编码
'
,
'
第三方厂商编码
'
],
[
'
第三方系统厂商名称
'
,
'
第三方厂商名称
'
],
[
'
接口名称
'
,
'
接口
'
],
[
'
推送状态
'
,
'
推送
'
],
[
'
变更类型
'
,
'
变更
'
],
[
'
状态
'
,
'
当前状态
'
],
];
function
aliasTokensFor
(
columnName
:
string
):
string
[]
{
...
...
@@ -91,7 +107,7 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
if
(
!
normalizedCell
)
return
false
;
const
normalizedQuery
=
columnName
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
(
normalizedQuery
===
'
物料名称
'
||
normalizedQuery
===
'
产品名称
'
)
&&
(
normalizedQuery
===
'
物料名称
'
||
normalizedQuery
===
'
产品名称
'
||
normalizedQuery
===
'
产品
'
)
&&
/^物料
\d
*名称$/
.
test
(
normalizedCell
)
)
{
return
true
;
...
...
@@ -103,6 +119,16 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
});
}
/** 列表「核心列」断言:joined 表头文本是否覆盖期望列(含别名)。 */
export
function
headersJoinedMatchExpected
(
joinedHeaders
:
string
,
expectedName
:
string
):
boolean
{
const
joined
=
joinedHeaders
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
!
joined
)
return
false
;
const
expected
=
expectedName
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
!
expected
)
return
true
;
if
(
joined
.
includes
(
expected
))
return
true
;
return
columnHeaderMatchTokens
(
expectedName
).
some
((
token
)
=>
token
.
length
>=
2
&&
joined
.
includes
(
token
));
}
export
function
scoreColumnHeaderMatch
(
cellText
:
string
,
columnName
:
string
):
number
{
const
normalizedCell
=
cellText
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
!
normalizedCell
)
return
0
;
...
...
core/table-data-column.util.ts
View file @
297802d0
...
...
@@ -44,22 +44,25 @@ export async function dataHeaderLabelsFromCells(headerCells: Locator): Promise<s
const
labels
:
string
[]
=
[];
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
th
=
headerCells
.
nth
(
i
);
const
cls
=
(
await
th
.
getAttribute
(
'
class
'
))
??
''
;
// 宽表尾部 th 可能未挂载/已虚拟化,单格超时则跳过,避免拖死整条用例
const
attached
=
await
th
.
waitFor
({
state
:
'
attached
'
,
timeout
:
800
}).
then
(()
=>
true
).
catch
(()
=>
false
);
if
(
!
attached
)
continue
;
const
cls
=
(
await
th
.
getAttribute
(
'
class
'
,
{
timeout
:
800
}).
catch
(()
=>
''
))
??
''
;
if
(
isUtilityTableCellClass
(
cls
))
continue
;
const
titleEl
=
th
.
locator
(
'
.ant-table-column-title
'
).
first
();
let
raw
=
''
;
// 空表头 th 无 .ant-table-column-title;直接 innerText 会等到 action 超时
if
((
await
titleEl
.
count
())
>
0
)
{
raw
=
(
await
titleEl
.
innerText
({
timeout
:
1
_5
00
}).
catch
(()
=>
''
)).
trim
();
if
((
await
titleEl
.
count
()
.
catch
(()
=>
0
)
)
>
0
)
{
raw
=
(
await
titleEl
.
innerText
({
timeout
:
8
00
}).
catch
(()
=>
''
)).
trim
();
}
if
(
!
raw
)
{
raw
=
((
await
th
.
innerText
({
timeout
:
1
_5
00
}).
catch
(()
=>
''
))
||
''
).
trim
();
raw
=
((
await
th
.
innerText
({
timeout
:
8
00
}).
catch
(()
=>
''
))
||
''
).
trim
();
}
if
(
!
raw
)
{
raw
=
((
await
th
.
getAttribute
(
'
title
'
))
??
''
).
trim
();
raw
=
((
await
th
.
getAttribute
(
'
title
'
,
{
timeout
:
500
}).
catch
(()
=>
''
))
??
''
).
trim
();
}
if
(
!
raw
)
{
raw
=
((
await
th
.
getAttribute
(
'
aria-label
'
))
??
''
).
trim
();
raw
=
((
await
th
.
getAttribute
(
'
aria-label
'
,
{
timeout
:
500
}).
catch
(()
=>
''
))
??
''
).
trim
();
}
labels
.
push
(
raw
.
replace
(
/
\s
/g
,
''
));
}
...
...
data/iface.data.ts
View file @
297802d0
...
...
@@ -86,10 +86,10 @@ export const ifacePages: Record<IfacePageKey, IfacePageMeta> = {
pathPart
:
'
replace-dept-receive
'
,
forbidden
:
forbiddenOthers
(
'
replace-dept-receive
'
),
mock
:
ifaceMock
,
headers
:
[
'
产品
'
,
'
规格
'
,
'
请领
'
],
headers
:
[
'
物料
'
,
'
规格
'
,
'
科室
'
],
rowOps
:
[],
page2Header
:
'
产品
'
,
sortColumns
:
[
'
请领数量
'
,
'
库存上限
'
,
'
库存下限
'
,
'
建议补货单元数
'
],
page2Header
:
'
物料名称
'
,
sortColumns
:
[
'
物料名称
'
,
'
物料编号
'
,
'
规格型号
'
,
'
请领数量
'
,
'
库存上限
'
,
'
库存下限
'
,
'
建议补货单元数
'
],
fixture
:
'
ifaceDeptReqOkPage
'
,
specFile
:
'
iface-dept-req-ok.spec.ts
'
,
csv
:
'
基础管理-接口管理-代为科室请领(已确认)-UI自动化测试用例-01.csv
'
,
...
...
@@ -119,8 +119,8 @@ export const ifacePages: Record<IfacePageKey, IfacePageMeta> = {
mock
:
ifaceMock
,
headers
:
[
'
科室
'
,
'
产品
'
,
'
状态
'
],
rowOps
:
[],
page2Header
:
'
产品
'
,
sortColumns
:
[
'
手术时间
'
,
'
计费时间
'
],
page2Header
:
'
物料名称
'
,
sortColumns
:
[
'
手术时间
'
,
'
计费时间
'
,
'
物料名称
'
,
'
规格型号
'
],
fixture
:
'
ifaceLowBillingPage
'
,
specFile
:
'
iface-low-billing.spec.ts
'
,
csv
:
'
基础管理-接口管理-低值计费接口查询-UI自动化测试用例-01.csv
'
,
...
...
@@ -252,7 +252,7 @@ export const ifacePages: Record<IfacePageKey, IfacePageMeta> = {
pathPart
:
'
interface/hisCode
'
,
forbidden
:
forbiddenOthers
(
'
hisCode
'
),
mock
:
ifaceMock
,
headers
:
[
'
HIS
'
,
'
收费
'
,
'
操作
'
],
headers
:
[
'
规格
'
,
'
是否
收费
'
,
'
操作
'
],
rowOps
:
[
'
编辑
'
],
page2Header
:
'
HIS
'
,
addLabel
:
'
手动新增
'
,
...
...
locators/spd/admit-shared.locator.ts
View file @
297802d0
...
...
@@ -37,12 +37,11 @@ export class AdmitSharedLocators {
tab
(
name
:
string
):
Locator
{
const
escaped
=
name
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
pattern
=
new
RegExp
(
escaped
);
const
tabsNav
=
this
.
page
.
locator
(
'
.ant-tabs-nav, .ant-tabs
'
).
first
();
return
tabsNav
.
locator
(
'
.ant-tabs-tab-btn, .ant-tabs-tab
'
)
.
filter
({
hasText
:
pattern
})
// 全页查找,避免 .ant-tabs.first() 绑到无目标文案的空壳 Tab 容器
return
this
.
page
.
getByRole
(
'
tab
'
,
{
name
:
pattern
})
.
first
()
.
or
(
t
absNav
.
getByRole
(
'
tab
'
,
{
name
:
pattern
}).
first
())
.
or
(
t
his
.
page
.
locator
(
'
.ant-tabs-tab-btn, .ant-tabs-tab
'
).
filter
({
hasText
:
pattern
}).
first
())
.
or
(
this
.
page
.
locator
(
'
.ant-radio-group, .ant-segmented
'
)
...
...
@@ -50,7 +49,7 @@ export class AdmitSharedLocators {
.
filter
({
hasText
:
pattern
})
.
first
(),
)
.
or
(
this
.
page
.
getBy
Role
(
'
tab
'
,
{
name
:
pattern
}
).
first
());
.
or
(
this
.
page
.
getBy
Text
(
pattern
).
first
());
}
putPage
():
Locator
{
...
...
pages/spd/admit-material.page.ts
View file @
297802d0
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
admitData
}
from
'
../../data/admit.data
'
;
import
{
AdmitSharedPage
}
from
'
./admit-shared.page
'
;
...
...
@@ -41,127 +40,9 @@ export class AdmitMaterialPage extends AdmitSharedPage {
await
this
.
expectListOrEmpty
();
}
private
filterKeyword
(
raw
:
string
,
max
=
40
):
string
{
const
text
=
raw
.
replace
(
/
\s
/g
,
''
);
if
(
text
.
length
<=
max
)
return
raw
.
trim
();
return
raw
.
trim
().
slice
(
0
,
max
);
}
private
async
fillFilterIfVisible
(
labels
:
string
[],
value
:
string
):
Promise
<
boolean
>
{
const
keyword
=
this
.
filterKeyword
(
value
);
if
(
!
keyword
||
keyword
===
'
--
'
||
keyword
===
'
-
'
)
return
false
;
await
this
.
ensureFiltersExpanded
();
const
scope
=
await
this
.
queryScope
();
for
(
const
label
of
labels
)
{
for
(
const
candidate
of
this
.
filterLabelCandidates
(
label
))
{
const
input
=
await
this
.
firstVisibleFilterInput
(
candidate
,
scope
);
if
(
input
)
{
await
this
.
fill
(
input
,
keyword
,
`填写
${
candidate
}
`
);
return
true
;
}
}
}
return
false
;
}
private
async
fillCreateDateRangeFromRow
(
dayText
:
string
):
Promise
<
void
>
{
const
day
=
dayText
.
replace
(
/
\s
.*/
,
''
).
trim
().
slice
(
0
,
10
);
if
(
!
/^
\d{4}
-
\d{2}
-
\d{2}
$/
.
test
(
day
))
{
await
this
.
skipWhenDataInsufficient
(
'
首行新增日期无效,无法组合日期筛选
'
);
}
await
this
.
ensureFiltersExpanded
();
const
scope
=
await
this
.
queryScope
();
const
item
=
await
this
.
resolveFilterItem
(
scope
,
'
新增日期
'
);
if
(
!
item
)
{
await
this
.
skipWhenDataInsufficient
(
'
筛选区无「新增日期」
'
);
return
;
}
const
inputs
=
item
.
locator
(
'
input
'
);
if
((
await
inputs
.
count
())
>=
2
)
{
await
this
.
fill
(
inputs
.
nth
(
0
),
day
,
'
新增日期起
'
);
await
this
.
fill
(
inputs
.
nth
(
1
),
day
,
'
新增日期止
'
);
await
this
.
waitSettle
(
200
);
return
;
}
await
this
.
fillDateRange
(
scope
,
item
,
'
新增日期
'
);
}
/** CSV ADMIT-MAT-004:首行七项组合查询。 */
async
filterCombination
():
Promise
<
void
>
{
await
this
.
query
();
const
rows
=
await
this
.
listRows
();
const
rowCount
=
await
rows
.
count
();
if
(
rowCount
===
0
)
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法组合查询
'
);
type
Sample
=
{
createDate
:
string
;
supplier
:
string
;
consumableType
:
string
;
productName
:
string
;
spec
:
string
;
certNo
:
string
;
registrant
:
string
;
};
let
sample
:
Sample
|
null
=
null
;
const
supplierColumns
=
[
'
销售商(自定义)
'
,
'
默认进院供应商
'
,
'
进院供应商
'
,
'
销售商
'
];
for
(
let
i
=
0
;
i
<
Math
.
min
(
rowCount
,
10
);
i
+=
1
)
{
const
row
=
rows
.
nth
(
i
);
const
productName
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
this
.
listNameColumn
));
const
consumableType
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
耗材类型
'
));
if
(
productName
.
replace
(
/
\s
/g
,
''
).
length
<
2
||
consumableType
.
replace
(
/
\s
/g
,
''
).
length
<
2
)
continue
;
let
supplier
=
''
;
for
(
const
col
of
supplierColumns
)
{
supplier
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
if
(
supplier
.
replace
(
/
\s
/g
,
''
).
length
>=
2
)
break
;
}
const
spec
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
规格型号
'
));
const
certNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
注册证号
'
));
const
registrant
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
器械注册人
'
));
const
createDate
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
新增日期
'
));
if
(
createDate
.
replace
(
/
\s
/g
,
''
).
length
<
8
)
continue
;
sample
=
{
createDate
,
supplier
,
consumableType
,
productName
,
spec
,
certNo
,
registrant
};
break
;
}
if
(
!
sample
)
{
await
this
.
skipWhenDataInsufficient
(
'
未找到含新增日期与物料名称/耗材类型的完整首行样本
'
);
return
;
}
const
rowSample
=
sample
;
await
this
.
reset
();
await
this
.
fillCreateDateRangeFromRow
(
rowSample
.
createDate
);
const
supplierLabels
=
[
'
销售商(自定义)
'
,
'
销售商(自定义)名称
'
,
'
销售商
'
,
'
进院供应商
'
];
if
(
rowSample
.
supplier
)
{
const
ok
=
await
this
.
fillFilterIfVisible
(
supplierLabels
,
rowSample
.
supplier
);
if
(
!
ok
)
await
this
.
skipWhenDataInsufficient
(
'
筛选区无销售商(自定义)输入框
'
);
}
await
this
.
chooseSelect
(
'
耗材类型
'
,
rowSample
.
consumableType
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
const
nameKey
=
this
.
filterKeyword
(
rowSample
.
productName
.
replace
(
/
\s
/g
,
''
).
length
>=
4
?
rowSample
.
productName
.
slice
(
0
,
12
)
:
rowSample
.
productName
,
40
,
);
await
this
.
fillProductNameFilter
(
nameKey
);
if
(
rowSample
.
spec
)
await
this
.
fillFilterIfVisible
([
'
规格型号
'
],
rowSample
.
spec
);
if
(
rowSample
.
certNo
)
await
this
.
fillFilterIfVisible
([
'
注册证号
'
],
rowSample
.
certNo
);
if
(
rowSample
.
registrant
)
await
this
.
fillFilterIfVisible
([
'
器械注册人
'
],
rowSample
.
registrant
);
await
this
.
query
();
if
((
await
(
await
this
.
listRows
()).
count
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
七项组合查询无匹配结果
'
);
}
await
this
.
expectRowsContain
(
this
.
listNameColumn
,
nameKey
);
await
this
.
expectRowsContain
(
'
耗材类型
'
,
rowSample
.
consumableType
);
if
(
rowSample
.
spec
)
await
this
.
expectRowsContain
(
'
规格型号
'
,
rowSample
.
spec
);
if
(
rowSample
.
certNo
)
await
this
.
expectRowsContain
(
'
注册证号
'
,
rowSample
.
certNo
);
if
(
rowSample
.
registrant
)
await
this
.
expectRowsContain
(
'
器械注册人
'
,
rowSample
.
registrant
);
if
(
rowSample
.
supplier
)
{
try
{
await
this
.
expectRowsContain
(
'
默认进院供应商
'
,
rowSample
.
supplier
);
}
catch
{
await
this
.
expectRowsContain
(
'
销售商(自定义)
'
,
rowSample
.
supplier
);
}
}
await
this
.
filterSevenItemCombination
();
}
async
mockEmptyName
()
{
...
...
pages/spd/admit-product-apply.page.ts
View file @
297802d0
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
admitData
,
uniqProductName
}
from
'
../../data/admit.data
'
;
import
{
AdmitSharedPage
}
from
'
./admit-shared.page
'
;
...
...
@@ -21,127 +20,9 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
await
this
.
expectOnPage
();
}
private
filterKeyword
(
raw
:
string
,
max
=
40
):
string
{
const
text
=
raw
.
replace
(
/
\s
/g
,
''
);
if
(
text
.
length
<=
max
)
return
raw
.
trim
();
return
raw
.
trim
().
slice
(
0
,
max
);
}
private
async
fillFilterIfVisible
(
labels
:
string
[],
value
:
string
):
Promise
<
boolean
>
{
const
keyword
=
this
.
filterKeyword
(
value
);
if
(
!
keyword
||
keyword
===
'
--
'
||
keyword
===
'
-
'
)
return
false
;
await
this
.
ensureFiltersExpanded
();
const
scope
=
await
this
.
queryScope
();
for
(
const
label
of
labels
)
{
for
(
const
candidate
of
this
.
filterLabelCandidates
(
label
))
{
const
input
=
await
this
.
firstVisibleFilterInput
(
candidate
,
scope
);
if
(
input
)
{
await
this
.
fill
(
input
,
keyword
,
`填写
${
candidate
}
`
);
return
true
;
}
}
}
return
false
;
}
private
async
fillCreateDateRangeFromRow
(
dayText
:
string
):
Promise
<
void
>
{
const
day
=
dayText
.
replace
(
/
\s
.*/
,
''
).
trim
().
slice
(
0
,
10
);
if
(
!
/^
\d{4}
-
\d{2}
-
\d{2}
$/
.
test
(
day
))
{
await
this
.
skipWhenDataInsufficient
(
'
首行新增日期无效,无法组合日期筛选
'
);
}
await
this
.
ensureFiltersExpanded
();
const
scope
=
await
this
.
queryScope
();
const
item
=
await
this
.
resolveFilterItem
(
scope
,
'
新增日期
'
);
if
(
!
item
)
{
await
this
.
skipWhenDataInsufficient
(
'
筛选区无「新增日期」
'
);
return
;
}
const
inputs
=
item
.
locator
(
'
input
'
);
if
((
await
inputs
.
count
())
>=
2
)
{
await
this
.
fill
(
inputs
.
nth
(
0
),
day
,
'
新增日期起
'
);
await
this
.
fill
(
inputs
.
nth
(
1
),
day
,
'
新增日期止
'
);
await
this
.
waitSettle
(
200
);
return
;
}
await
this
.
fillDateRange
(
scope
,
item
,
'
新增日期
'
);
}
/** CSV ADMIT-PAP-002:首行七项组合查询。 */
async
filterCombination
():
Promise
<
void
>
{
await
this
.
query
();
const
rows
=
await
this
.
listRows
();
const
rowCount
=
await
rows
.
count
();
if
(
rowCount
===
0
)
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法组合查询
'
);
type
Sample
=
{
createDate
:
string
;
supplier
:
string
;
consumableType
:
string
;
productName
:
string
;
spec
:
string
;
certNo
:
string
;
registrant
:
string
;
};
let
sample
:
Sample
|
null
=
null
;
const
supplierColumns
=
[
'
销售商(自定义)
'
,
'
默认进院供应商
'
,
'
进院供应商
'
,
'
销售商
'
];
for
(
let
i
=
0
;
i
<
Math
.
min
(
rowCount
,
10
);
i
+=
1
)
{
const
row
=
rows
.
nth
(
i
);
const
productName
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
this
.
listNameColumn
));
const
consumableType
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
耗材类型
'
));
if
(
productName
.
replace
(
/
\s
/g
,
''
).
length
<
2
||
consumableType
.
replace
(
/
\s
/g
,
''
).
length
<
2
)
continue
;
let
supplier
=
''
;
for
(
const
col
of
supplierColumns
)
{
supplier
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
if
(
supplier
.
replace
(
/
\s
/g
,
''
).
length
>=
2
)
break
;
}
const
spec
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
规格型号
'
));
const
certNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
注册证号
'
));
const
registrant
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
器械注册人
'
));
const
createDate
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
新增日期
'
));
if
(
createDate
.
replace
(
/
\s
/g
,
''
).
length
<
8
)
continue
;
sample
=
{
createDate
,
supplier
,
consumableType
,
productName
,
spec
,
certNo
,
registrant
};
break
;
}
if
(
!
sample
)
{
await
this
.
skipWhenDataInsufficient
(
'
未找到含新增日期与物料名称/耗材类型的完整首行样本
'
);
return
;
}
const
rowSample
=
sample
;
await
this
.
reset
();
await
this
.
fillCreateDateRangeFromRow
(
rowSample
.
createDate
);
const
supplierLabels
=
[
'
销售商(自定义)
'
,
'
销售商(自定义)名称
'
,
'
销售商
'
,
'
进院供应商
'
];
if
(
rowSample
.
supplier
)
{
const
ok
=
await
this
.
fillFilterIfVisible
(
supplierLabels
,
rowSample
.
supplier
);
if
(
!
ok
)
await
this
.
skipWhenDataInsufficient
(
'
筛选区无销售商(自定义)输入框
'
);
}
await
this
.
chooseSelect
(
'
耗材类型
'
,
rowSample
.
consumableType
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
const
nameKey
=
this
.
filterKeyword
(
rowSample
.
productName
.
replace
(
/
\s
/g
,
''
).
length
>=
4
?
rowSample
.
productName
.
slice
(
0
,
12
)
:
rowSample
.
productName
,
40
,
);
await
this
.
fillProductNameFilter
(
nameKey
);
if
(
rowSample
.
spec
)
await
this
.
fillFilterIfVisible
([
'
规格型号
'
],
rowSample
.
spec
);
if
(
rowSample
.
certNo
)
await
this
.
fillFilterIfVisible
([
'
注册证号
'
],
rowSample
.
certNo
);
if
(
rowSample
.
registrant
)
await
this
.
fillFilterIfVisible
([
'
器械注册人
'
],
rowSample
.
registrant
);
await
this
.
query
();
if
((
await
(
await
this
.
listRows
()).
count
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
七项组合查询无匹配结果
'
);
}
await
this
.
expectRowsContain
(
this
.
listNameColumn
,
nameKey
);
await
this
.
expectRowsContain
(
'
耗材类型
'
,
rowSample
.
consumableType
);
if
(
rowSample
.
spec
)
await
this
.
expectRowsContain
(
'
规格型号
'
,
rowSample
.
spec
);
if
(
rowSample
.
certNo
)
await
this
.
expectRowsContain
(
'
注册证号
'
,
rowSample
.
certNo
);
if
(
rowSample
.
registrant
)
await
this
.
expectRowsContain
(
'
器械注册人
'
,
rowSample
.
registrant
);
if
(
rowSample
.
supplier
)
{
try
{
await
this
.
expectRowsContain
(
'
默认进院供应商
'
,
rowSample
.
supplier
);
}
catch
{
await
this
.
expectRowsContain
(
'
销售商(自定义)
'
,
rowSample
.
supplier
);
}
}
await
this
.
filterSevenItemCombination
();
}
async
resetFilters
():
Promise
<
void
>
{
...
...
pages/spd/admit-shared.page.ts
View file @
297802d0
...
...
@@ -15,6 +15,7 @@ import {
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
clickPageSizeDropdownOption
,
pageSizeChangeObservability
,
pageSizeLabelsEqual
,
parsePageSizeFromLabel
,
skipIfInsufficientForPageSizeChange
,
...
...
@@ -119,16 +120,16 @@ export class AdmitSharedPage extends BasePage {
await
this
.
click
(
this
.
button
(
name
,
root
).
first
(),
name
);
}
async
query
():
Promise
<
void
>
{
async
query
(
options
?:
{
health
?:
'
ui
'
|
'
business
'
}
):
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
'
查询
'
);
await
this
.
clickButton
(
'
查询
'
);
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
(
{
health
:
options
?.
health
??
'
business
'
}
);
}
async
reset
():
Promise
<
void
>
{
async
reset
(
options
?:
{
health
?:
'
ui
'
|
'
business
'
}
):
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
'
重置
'
);
await
this
.
clickButton
(
'
重置
'
);
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
(
{
health
:
options
?.
health
??
'
business
'
}
);
}
textInput
(
label
:
string
,
root
?:
Locator
):
Locator
{
...
...
@@ -216,6 +217,208 @@ export class AdmitSharedPage extends BasePage {
expect
(
false
,
'
mock/筛选:未找到产品或物料名称输入框
'
).
toBeTruthy
();
}
/**
* ant-picker 输入框常为 readonly,不能直接 fill。
* 去掉 readonly 后键盘录入 YYYY-MM-DD(与 vendor fillFilterDateRange 一致)。
*/
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
);
if
(
!
item
)
{
await
this
.
skipWhenDataInsufficient
(
`筛选区无「
${
label
}
」`
);
return
;
}
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
const
inputs
=
(
await
picker
.
isVisible
().
catch
(()
=>
false
))
?
picker
.
locator
(
'
input
'
)
:
item
.
locator
(
'
input
'
);
const
count
=
await
inputs
.
count
();
if
(
count
<
1
)
{
await
this
.
skipWhenDataInsufficient
(
`「
${
label
}
」无日期输入框`
);
return
;
}
const
typeInto
=
async
(
input
:
Locator
,
value
:
string
,
step
:
string
)
=>
{
await
input
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
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
'
);
try
{
await
input
.
fill
(
value
,
{
force
:
true
});
}
catch
{
await
this
.
page
.
keyboard
.
type
(
value
,
{
delay
:
0
});
}
await
input
.
press
(
'
Enter
'
);
await
this
.
waitSettle
(
150
);
void
step
;
};
if
(
count
>=
2
)
{
await
typeInto
(
inputs
.
nth
(
0
),
start
,
`
${
label
}
起`
);
await
typeInto
(
inputs
.
nth
(
1
),
end
,
`
${
label
}
止`
);
}
else
{
await
typeInto
(
inputs
.
first
(),
start
,
label
);
}
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
200
);
}
/** 样本行「新增日期」→ 起止同一天的日期区间筛选。 */
async
fillCreateDateRangeFromSample
(
dayText
:
string
):
Promise
<
void
>
{
const
day
=
dayText
.
replace
(
/
\s
.*/
,
''
).
trim
().
slice
(
0
,
10
);
if
(
!
/^
\d{4}
-
\d{2}
-
\d{2}
$/
.
test
(
day
))
{
await
this
.
skipWhenDataInsufficient
(
'
首行新增日期无效,无法组合日期筛选
'
);
return
;
}
await
this
.
fillFilterDateRange
(
'
新增日期
'
,
day
,
day
);
}
protected
filterKeyword
(
raw
:
string
,
max
=
40
):
string
{
const
text
=
raw
.
replace
(
/
\s
/g
,
''
);
if
(
text
.
length
<=
max
)
return
raw
.
trim
();
return
raw
.
trim
().
slice
(
0
,
max
);
}
protected
async
fillFilterIfVisible
(
labels
:
string
[],
value
:
string
):
Promise
<
boolean
>
{
const
keyword
=
this
.
filterKeyword
(
value
);
if
(
!
keyword
||
keyword
===
'
--
'
||
keyword
===
'
-
'
)
return
false
;
await
this
.
ensureFiltersExpanded
();
const
scope
=
await
this
.
queryScope
();
for
(
const
label
of
labels
)
{
for
(
const
candidate
of
this
.
filterLabelCandidates
(
label
))
{
const
input
=
await
this
.
firstVisibleFilterInput
(
candidate
,
scope
);
if
(
input
)
{
const
editable
=
await
input
.
evaluate
((
el
)
=>
{
const
node
=
el
as
HTMLInputElement
;
return
!
node
.
readOnly
&&
!
node
.
disabled
;
}).
catch
(()
=>
true
);
if
(
!
editable
)
{
// 可能是 picker 内 input,交给日期专用方法
continue
;
}
await
this
.
fill
(
input
,
keyword
,
`填写
${
candidate
}
`
);
return
true
;
}
}
}
return
false
;
}
/**
* ADMIT-MAT-004 / ADMIT-PAP-002:首行样本七项组合查询。
* 日期走 readonly-safe 录入;缺控件/无匹配则 skip,避免卡死超时。
* 组合过程用 health=ui,避免业务健康检查在慢接口上吃光用例超时。
*/
async
filterSevenItemCombination
():
Promise
<
void
>
{
let
rows
=
await
this
.
listRows
();
let
rowCount
=
await
rows
.
count
();
if
(
rowCount
===
0
)
{
await
this
.
query
({
health
:
'
ui
'
});
rows
=
await
this
.
listRows
();
rowCount
=
await
rows
.
count
();
}
if
(
rowCount
===
0
)
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法组合查询
'
);
type
Sample
=
{
createDate
:
string
;
supplier
:
string
;
consumableType
:
string
;
productName
:
string
;
spec
:
string
;
certNo
:
string
;
registrant
:
string
;
};
let
sample
:
Sample
|
null
=
null
;
const
supplierColumns
=
[
'
销售商(自定义)
'
,
'
默认进院供应商
'
,
'
进院供应商
'
,
'
销售商
'
];
for
(
let
i
=
0
;
i
<
Math
.
min
(
rowCount
,
10
);
i
+=
1
)
{
const
row
=
rows
.
nth
(
i
);
const
productName
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
this
.
listNameColumn
));
const
consumableType
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
耗材类型
'
));
if
(
productName
.
replace
(
/
\s
/g
,
''
).
length
<
2
||
consumableType
.
replace
(
/
\s
/g
,
''
).
length
<
2
)
continue
;
let
supplier
=
''
;
for
(
const
col
of
supplierColumns
)
{
supplier
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
if
(
supplier
.
replace
(
/
\s
/g
,
''
).
length
>=
2
)
break
;
}
const
spec
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
规格型号
'
));
const
certNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
注册证号
'
));
const
registrant
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
器械注册人
'
));
const
createDate
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
新增日期
'
));
if
(
createDate
.
replace
(
/
\s
/g
,
''
).
length
<
8
)
continue
;
sample
=
{
createDate
,
supplier
,
consumableType
,
productName
,
spec
,
certNo
,
registrant
};
break
;
}
if
(
!
sample
)
{
await
this
.
skipWhenDataInsufficient
(
'
未找到含新增日期与物料名称/耗材类型的完整首行样本
'
);
return
;
}
const
rowSample
=
sample
;
await
this
.
reset
({
health
:
'
ui
'
});
await
this
.
fillCreateDateRangeFromSample
(
rowSample
.
createDate
);
const
supplierLabels
=
[
'
销售商(自定义)
'
,
'
销售商(自定义)名称
'
,
'
销售商
'
,
'
进院供应商
'
];
if
(
rowSample
.
supplier
)
{
const
ok
=
await
this
.
fillFilterIfVisible
(
supplierLabels
,
rowSample
.
supplier
);
if
(
!
ok
)
{
// 销售商常为下拉:尽量选中样本文案,失败则跳过该项继续组合
const
select
=
await
this
.
visibleFormSelect
(
'
销售商(自定义)
'
);
if
(
select
)
{
try
{
await
this
.
chooseSelect
(
'
销售商(自定义)
'
,
rowSample
.
supplier
);
}
catch
{
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
}
}
}
}
try
{
await
this
.
chooseSelect
(
'
耗材类型
'
,
rowSample
.
consumableType
);
}
catch
{
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
await
this
.
skipWhenDataInsufficient
(
`耗材类型无法选择「
${
rowSample
.
consumableType
.
slice
(
0
,
20
)}
」`
);
return
;
}
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
const
nameKey
=
this
.
filterKeyword
(
rowSample
.
productName
.
replace
(
/
\s
/g
,
''
).
length
>=
4
?
rowSample
.
productName
.
slice
(
0
,
12
)
:
rowSample
.
productName
,
40
,
);
await
this
.
fillProductNameFilter
(
nameKey
);
if
(
rowSample
.
spec
)
await
this
.
fillFilterIfVisible
([
'
规格型号
'
],
rowSample
.
spec
);
if
(
rowSample
.
certNo
)
await
this
.
fillFilterIfVisible
([
'
注册证号
'
],
rowSample
.
certNo
);
if
(
rowSample
.
registrant
)
await
this
.
fillFilterIfVisible
([
'
器械注册人
'
],
rowSample
.
registrant
);
await
this
.
query
({
health
:
'
ui
'
});
if
((
await
(
await
this
.
listRows
()).
count
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
七项组合查询无匹配结果
'
);
return
;
}
// 核心断言:名称 + 耗材类型(足以证明组合生效)
await
this
.
expectRowsContain
(
this
.
listNameColumn
,
nameKey
);
await
this
.
expectRowsContain
(
'
耗材类型
'
,
rowSample
.
consumableType
);
// 可选列:用行文本指纹,避免宽表逐 th 解析拖死
const
optionalNeedles
=
[
rowSample
.
spec
,
rowSample
.
certNo
,
rowSample
.
registrant
,
rowSample
.
supplier
]
.
map
((
v
)
=>
v
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
))
.
filter
((
v
)
=>
v
.
length
>=
2
);
if
(
optionalNeedles
.
length
>
0
)
{
const
row
=
(
await
this
.
listRows
()).
first
();
const
joined
=
((
await
row
.
innerText
({
timeout
:
5
_000
}).
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
const
hit
=
optionalNeedles
.
some
((
n
)
=>
joined
.
includes
(
n
));
expect
(
hit
,
`组合结果行应至少命中一项可选条件(
${
optionalNeedles
.
join
(
'
/
'
)}
)`
).
toBeTruthy
();
}
}
async
chooseSelect
(
label
:
string
,
optionText
:
string
,
root
?:
Locator
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
typeAliases
=
label
===
'
耗材类型
'
?
[
'
耗材类型
'
,
'
产品类型
'
]
:
[
label
];
...
...
@@ -929,11 +1132,23 @@ export class AdmitSharedPage extends BasePage {
waitForTableReady
:
()
=>
this
.
waitForTableReady
(),
getDataRowCount
:
()
=>
this
.
getDataRowCount
(),
});
const
total
=
await
this
.
getTotalCount
();
const
rowsBefore
=
await
this
.
getDataRowCount
();
const
sizeNum
=
20
;
if
(
total
>
sizeNum
&&
rowsBefore
<=
sizeNum
)
{
await
this
.
skipWhenDataInsufficient
(
'
当前页可见行数已不大于 20,无法验证切换 20 条/页
'
);
let
total
=
await
this
.
getTotalCount
();
let
rowsBefore
=
await
this
.
getDataRowCount
();
const
currentSize
=
parsePageSizeFromLabel
(
await
this
.
pageSizeChangerText
());
const
plan
=
pageSizeChangeObservability
({
total
,
rowsBefore
,
targetSize
:
sizeNum
,
currentSize
,
});
if
(
plan
.
shouldSkip
)
{
await
this
.
skipWhenDataInsufficient
(
plan
.
reason
??
'
无法验证每页条数切换
'
);
}
if
(
plan
.
needDropToSmallerFirst
)
{
await
this
.
changePageSizeTo
(
'
10 条/页
'
);
rowsBefore
=
await
this
.
getDataRowCount
();
total
=
await
this
.
getTotalCount
();
}
await
this
.
changePageSizeTo
(
admitData
.
paginationPageSizeTo
);
const
rowsAfter
=
await
this
.
getDataRowCount
();
...
...
pages/spd/basic-shared.page.ts
View file @
297802d0
...
...
@@ -6,7 +6,9 @@ import { basicHospitalData } from '../../data/basic-hospital.data';
import
{
BasicSharedLocators
}
from
'
../../locators/spd/basic-shared.locator
'
;
import
{
clickPageSizeDropdownOption
,
pageSizeChangeObservability
,
pageSizeLabelsEqual
,
parsePageSizeFromLabel
,
skipIfInsufficientForPageSizeChange
,
}
from
'
./helpers/pagination-pagesize.helper
'
;
import
{
SpdProTableListControls
}
from
'
./helpers/spd-protable-list.controls
'
;
...
...
@@ -222,16 +224,32 @@ export class BasicHospitalSharedPage extends BasePage {
waitForTableReady
:
()
=>
this
.
waitForTableReady
(),
getDataRowCount
:
()
=>
this
.
getDataRowCount
(),
});
const
total
=
await
this
.
getTotalCount
();
const
rowsBefore
=
await
this
.
getDataRowCount
();
const
sizeNum
=
20
;
if
(
total
>
sizeNum
&&
rowsBefore
<=
sizeNum
)
{
await
this
.
skipWhenDataInsufficient
(
'
当前页可见行数已不大于 20,无法验证切换 20 条/页
'
);
}
const
changer
=
this
.
pageSizeChanger
();
if
(
!
(
await
changer
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
当前页面无每页条数切换
'
);
}
let
total
=
await
this
.
getTotalCount
();
let
rowsBefore
=
await
this
.
getDataRowCount
();
const
currentSize
=
parsePageSizeFromLabel
(
((
await
changer
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
'
'
),
);
const
plan
=
pageSizeChangeObservability
({
total
,
rowsBefore
,
targetSize
:
sizeNum
,
currentSize
,
});
if
(
plan
.
shouldSkip
)
{
await
this
.
skipWhenDataInsufficient
(
plan
.
reason
??
'
无法验证每页条数切换
'
);
}
if
(
plan
.
needDropToSmallerFirst
)
{
await
this
.
click
(
changer
,
'
打开每页条数
'
);
await
clickPageSizeDropdownOption
(
this
.
page
,
10
,
(
loc
,
step
)
=>
this
.
click
(
loc
,
step
));
await
this
.
waitForTableReady
();
rowsBefore
=
await
this
.
getDataRowCount
();
total
=
await
this
.
getTotalCount
();
}
await
this
.
click
(
changer
,
'
打开每页条数
'
);
await
clickPageSizeDropdownOption
(
this
.
page
,
sizeNum
,
(
loc
,
step
)
=>
this
.
click
(
loc
,
step
));
await
this
.
waitForTableReady
();
...
...
pages/spd/deptm-shared.page.ts
View file @
297802d0
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
headersJoinedMatchExpected
}
from
'
../../core/column-header-match.util
'
;
import
{
type
DeptmPageMeta
,
deptmMenuGroup
}
from
'
../../data/deptm.data
'
;
import
{
runWithMockEmptyListRoute
}
from
'
./helpers/mock-empty-route.helper
'
;
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
SpdProTableListControls
}
from
'
./helpers/spd-protable-list.controls
'
;
import
{
WhFlowSharedPage
}
from
'
./wh-flow-shared.page
'
;
...
...
@@ -153,24 +155,26 @@ export class DeptmSharedPage extends WhFlowSharedPage {
async
searchByMockKeyword
():
Promise
<
void
>
{
const
mock
=
this
.
deptm
.
mock
;
const
inputs
=
[
this
.
page
.
getByPlaceholder
(
/科室|物料|机器|任务|名称|编号/
).
first
(),
this
.
textInput
(
'
科室名称
'
),
this
.
textInput
(
'
科室编码
'
),
this
.
textInput
(
'
机器编号
'
),
this
.
textInput
(
'
机器名称
'
),
this
.
textInput
(
'
任务名称
'
),
this
.
page
.
locator
(
'
.ant-input
'
).
first
(),
];
for
(
const
input
of
inputs
)
{
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
();
return
;
await
runWithMockEmptyListRoute
(
this
.
page
,
mock
,
async
()
=>
{
const
inputs
=
[
this
.
page
.
getByPlaceholder
(
/科室|物料|机器|任务|名称|编号/
).
first
(),
this
.
textInput
(
'
科室名称
'
),
this
.
textInput
(
'
科室编码
'
),
this
.
textInput
(
'
机器编号
'
),
this
.
textInput
(
'
机器名称
'
),
this
.
textInput
(
'
任务名称
'
),
this
.
page
.
locator
(
'
.ant-input
'
).
first
(),
];
for
(
const
input
of
inputs
)
{
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
();
return
;
}
}
}
await
this
.
fill
(
this
.
page
.
locator
(
'
.ant-input
'
).
first
(),
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
(
);
await
this
.
fill
(
this
.
page
.
locator
(
'
.ant-input
'
).
first
(),
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
(
);
}
);
}
async
searchDeptCodeOrNameFromSample
():
Promise
<
void
>
{
...
...
@@ -188,7 +192,7 @@ export class DeptmSharedPage extends WhFlowSharedPage {
const
headers
=
await
this
.
headerTexts
();
const
joined
=
headers
.
join
(
''
);
for
(
const
name
of
this
.
deptm
.
headers
)
{
expect
(
joined
,
`表头应含
${
name
}
`
).
toContain
(
name
.
replace
(
/
\s
/g
,
''
)
);
expect
(
headersJoinedMatchExpected
(
joined
,
name
),
`表头应含
${
name
}
`
).
toBeTruthy
(
);
}
if
((
await
this
.
getDataRowCount
())
>
0
)
{
const
row
=
(
await
this
.
listRows
()).
first
();
...
...
@@ -237,8 +241,10 @@ export class DeptmSharedPage extends WhFlowSharedPage {
const
btn
=
this
.
rowAction
(
row
,
op
);
if
(
await
btn
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
btn
,
op
);
await
this
.
waitSettle
(
500
);
await
expect
(
this
.
overlay
(),
'
应打开弹层或详情
'
).
toBeVisible
();
const
opened
=
await
this
.
waitActionLayerOpen
();
if
(
!
opened
)
{
await
this
.
skipWhenDataInsufficient
(
`行内「
${
op
}
」未打开弹层/详情`
);
}
await
this
.
closeOverlay
();
await
this
.
expectOnPage
();
return
;
...
...
@@ -250,9 +256,9 @@ export class DeptmSharedPage extends WhFlowSharedPage {
async
openAddOverlay
():
Promise
<
Locator
>
{
const
add
=
this
.
button
(
'
新增
'
).
or
(
this
.
button
(
'
新 增
'
)).
first
();
await
this
.
click
(
add
,
'
新增
'
);
const
o
verlay
=
this
.
overlay
(
);
await
expect
(
o
verlay
,
'
新增弹层
'
).
toBe
Visible
({
timeout
:
12
_000
}
);
return
overlay
;
const
o
pened
=
await
this
.
waitActionLayerOpen
(
12
_000
);
expect
(
o
pened
,
'
新增弹层
'
).
toBe
Truthy
(
);
return
this
.
overlay
().
first
()
;
}
async
assertRequiredIntercept
():
Promise
<
void
>
{
...
...
pages/spd/helpers/column-sort.helper.ts
View file @
297802d0
...
...
@@ -385,7 +385,9 @@ export async function readDomColumnValues(wrapper: Locator, columnName: string,
}
/** 在 scope 内选列数最多、且含业务列(非仅序号)的 ant-table-wrapper 下标。 */
export
async
function
findWidestTableWrapperIndex
(
scope
:
Locator
):
Promise
<
number
>
{
export
async
function
findWidestTableWrapperIndex
(
scope
:
Locator
|
{
locator
(
selector
:
string
):
Locator
},
):
Promise
<
number
>
{
const
wrappers
=
scope
.
locator
(
'
.ant-table-wrapper
'
);
const
count
=
await
wrappers
.
count
().
catch
(()
=>
0
);
if
(
count
<=
1
)
{
...
...
@@ -763,8 +765,10 @@ export async function expectColumnSortByDom(options: {
wrapper
:
Locator
;
columnName
:
string
;
waitAfterClick
:
()
=>
Promise
<
void
>
;
/** 列可点但指示器/数据均无变化时(现网常未接服务端排序)转为 skip,避免误红 */
onUnresolved
?:
(
reason
:
string
)
=>
Promise
<
void
>
;
}):
Promise
<
void
>
{
const
{
wrapper
,
columnName
,
waitAfterClick
}
=
options
;
const
{
wrapper
,
columnName
,
waitAfterClick
,
onUnresolved
}
=
options
;
await
wrapper
.
locator
(
'
.ant-table-tbody, .ant-table-body
'
)
.
first
()
...
...
@@ -786,7 +790,7 @@ export async function expectColumnSortByDom(options: {
}
if
(
!
clickedOnce
&&
current
===
order
)
return
;
const
beforeClick
=
signatures
;
const
preferCycle
=
attempt
>=
2
;
const
preferCycle
=
attempt
>=
1
;
const
clicked
=
await
clickDomColumnSort
(
wrapper
,
columnName
,
order
,
{
preferCycle
});
expect
(
clicked
,
`未找到可排序列「
${
columnName
}
」`
).
toBeTruthy
();
clickedOnce
=
true
;
...
...
@@ -799,7 +803,7 @@ export async function expectColumnSortByDom(options: {
if
(
nextOrder
===
order
)
return
'
ready
'
;
return
sameList
(
next
,
beforeClick
)
?
'
same
'
:
'
ready
'
;
},
{
timeout
:
6
_000
},
{
timeout
:
8
_000
},
)
.
toBe
(
'
ready
'
)
.
catch
(()
=>
undefined
);
...
...
@@ -807,10 +811,14 @@ export async function expectColumnSortByDom(options: {
const
current
=
await
readDomColumnSortOrder
(
wrapper
,
columnName
);
const
values
=
await
readDomColumnValues
(
wrapper
,
columnName
,
8
);
const
signatures
=
await
readDomRowSignatures
(
wrapper
,
8
);
expect
(
sortReached
(
order
,
current
,
values
,
baselineVals
,
signatures
,
baselineSig
),
`列「
${
columnName
}
」未能切换到
${
label
}
`
,
).
toBeTruthy
();
const
ok
=
sortReached
(
order
,
current
,
values
,
baselineVals
,
signatures
,
baselineSig
);
if
(
ok
)
return
;
const
reason
=
`列「
${
columnName
}
」点击后未能切换到
${
label
}
(指示器=
${
current
}
,现网可能未启用有效排序)`
;
if
(
onUnresolved
)
{
await
onUnresolved
(
reason
);
return
;
}
expect
(
ok
,
reason
).
toBeTruthy
();
};
const
beforeSig
=
await
readDomRowSignatures
(
wrapper
,
8
);
...
...
pages/spd/helpers/pagination-pagesize.helper.ts
View file @
297802d0
...
...
@@ -13,6 +13,35 @@ export function pageSizeLabelsEqual(currentText: string, targetSize: number): bo
return
current
!==
null
&&
current
===
targetSize
;
}
/**
* 切换到目标每页条数前:若切完后可见行必然等于当前可见行(受 total 限制),
* 则 skip;若已在目标档且 total>10,先降到 10 再升回以便观察到变化。
*/
export
function
pageSizeChangeObservability
(
options
:
{
total
:
number
;
rowsBefore
:
number
;
targetSize
:
number
;
currentSize
:
number
|
null
;
}):
{
shouldSkip
:
boolean
;
reason
?:
string
;
needDropToSmallerFirst
:
boolean
}
{
const
{
total
,
rowsBefore
,
targetSize
,
currentSize
}
=
options
;
const
expectedAfter
=
Math
.
min
(
total
,
targetSize
);
if
(
total
<=
0
)
{
return
{
shouldSkip
:
true
,
reason
:
'
列表无数据
'
,
needDropToSmallerFirst
:
false
};
}
// 切到目标档后可见行不会变(常见:total<=10,或已显示 min(total,target))
if
(
rowsBefore
===
expectedAfter
)
{
if
(
currentSize
===
targetSize
&&
total
>
10
&&
targetSize
>
10
)
{
return
{
shouldSkip
:
false
,
needDropToSmallerFirst
:
true
};
}
return
{
shouldSkip
:
true
,
reason
:
`总条数
${
total
}
、当前可见
${
rowsBefore
}
,切换
${
targetSize
}
条/页后行数不会变化`
,
needDropToSmallerFirst
:
false
,
};
}
return
{
shouldSkip
:
false
,
needDropToSmallerFirst
:
false
};
}
/** 在下拉中点击与 targetSize 数值完全一致的「N 条/页」项。 */
export
async
function
clickPageSizeDropdownOption
(
page
:
Page
,
...
...
pages/spd/helpers/spd-protable-list.controls.ts
View file @
297802d0
...
...
@@ -6,7 +6,11 @@ import {
executeColumnSettingsToggleRestore
,
resolveUsableColumnSettingsTrigger
,
}
from
'
./column-settings.helper
'
;
import
{
expectColumnSortByDom
}
from
'
./column-sort.helper
'
;
import
{
expectColumnSortByDom
,
findWidestTableWrapperIndex
,
hasDomSortableColumn
,
}
from
'
./column-sort.helper
'
;
export
type
SpdProTableListControlDeps
=
{
listScope
:
()
=>
Page
|
FrameLocator
;
...
...
@@ -35,6 +39,16 @@ export class SpdProTableListControls {
return
this
.
deps
.
listScope
().
locator
(
'
.ant-table-wrapper
'
).
first
();
}
/** 多表并存时取业务列最多的主表,避免点到窄表/序号表。 */
async
resolveTableWrapper
():
Promise
<
Locator
>
{
const
scope
=
this
.
deps
.
listScope
();
const
wrappers
=
scope
.
locator
(
'
.ant-table-wrapper
'
);
const
count
=
await
wrappers
.
count
().
catch
(()
=>
0
);
if
(
count
<=
1
)
return
wrappers
.
first
();
const
widest
=
await
findWidestTableWrapperIndex
(
scope
);
return
wrappers
.
nth
(
widest
);
}
columnSettingsPanel
():
Locator
{
return
columnSettingsPanelLocator
(
this
.
deps
.
listScope
());
}
...
...
@@ -157,12 +171,30 @@ export class SpdProTableListControls {
async
expectColumnSortAscDesc
(
columnName
:
string
):
Promise
<
void
>
{
await
this
.
deps
.
clickQuery
();
const
wrapper
=
await
this
.
resolveTableWrapper
();
if
(
!
(
await
hasDomSortableColumn
(
wrapper
,
columnName
)))
{
try
{
await
this
.
openColumnSettings
();
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
if
(
await
row
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
setColumnVisibleInSettings
(
columnName
,
true
);
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
deps
.
clickQuery
();
}
catch
{
await
this
.
closeColumnSettingsPanel
().
catch
(()
=>
undefined
);
}
}
if
(
!
(
await
hasDomSortableColumn
(
wrapper
,
columnName
)))
{
await
this
.
deps
.
skipWhen
(
`当前表无可排序列「
${
columnName
}
」`
);
}
await
expectColumnSortByDom
({
wrapper
:
this
.
tableWrapper
()
,
wrapper
,
columnName
,
waitAfterClick
:
async
()
=>
{
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
4
00
));
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
8
00
));
},
onUnresolved
:
(
reason
)
=>
this
.
deps
.
skipWhen
(
reason
),
});
}
}
pages/spd/iface-shared.page.ts
View file @
297802d0
import
{
expect
,
type
FrameLocator
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
type
FilterScope
}
from
'
../../core/base.page
'
;
import
{
headersJoinedMatchExpected
}
from
'
../../core/column-header-match.util
'
;
import
{
type
IfacePageMeta
,
ifaceMenuGroup
}
from
'
../../data/iface.data
'
;
import
{
runWithMockEmptyListRoute
}
from
'
./helpers/mock-empty-route.helper
'
;
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
SpdProTableListControls
}
from
'
./helpers/spd-protable-list.controls
'
;
import
{
WhFlowSharedPage
}
from
'
./wh-flow-shared.page
'
;
...
...
@@ -122,27 +124,29 @@ export class IfaceSharedPage extends WhFlowSharedPage {
async
searchByMockKeyword
():
Promise
<
void
>
{
const
mock
=
this
.
iface
.
mock
;
const
scope
=
this
.
filterScope
();
const
inputs
=
[
scope
.
getByPlaceholder
(
/产品|物料|接口|名称|编号|关键字/
).
first
(),
scope
.
locator
(
'
.ant-input
'
).
first
(),
this
.
page
.
locator
(
'
.ant-input
'
).
first
(),
];
for
(
const
input
of
inputs
)
{
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
();
return
;
await
runWithMockEmptyListRoute
(
this
.
page
,
mock
,
async
()
=>
{
const
inputs
=
[
scope
.
getByPlaceholder
(
/产品|物料|接口|名称|编号|关键字/
).
first
(),
scope
.
locator
(
'
.ant-input
'
).
first
(),
this
.
page
.
locator
(
'
.ant-input
'
).
first
(),
];
for
(
const
input
of
inputs
)
{
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
();
return
;
}
}
}
await
this
.
fill
(
scope
.
locator
(
'
.ant-input
'
).
first
(),
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
(
);
await
this
.
fill
(
scope
.
locator
(
'
.ant-input
'
).
first
(),
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
(
);
}
);
}
async
assertConfiguredHeaders
():
Promise
<
void
>
{
const
headers
=
await
this
.
headerTexts
();
const
joined
=
headers
.
join
(
''
);
for
(
const
name
of
this
.
iface
.
headers
)
{
expect
(
joined
,
`表头应含
${
name
}
`
).
toContain
(
name
.
replace
(
/
\s
/g
,
''
)
);
expect
(
headersJoinedMatchExpected
(
joined
,
name
),
`表头应含
${
name
}
`
).
toBeTruthy
(
);
}
if
(
this
.
iface
.
rowOps
.
length
===
0
||
(
await
this
.
getDataRowCount
())
===
0
)
return
;
const
row
=
(
await
this
.
listRows
()).
first
();
...
...
@@ -189,8 +193,10 @@ export class IfaceSharedPage extends WhFlowSharedPage {
const
btn
=
this
.
rowAction
(
row
,
name
);
if
(
await
btn
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
btn
,
name
);
await
this
.
waitSettle
(
500
);
await
expect
(
this
.
overlay
(),
'
应打开弹层或详情
'
).
toBeVisible
();
const
opened
=
await
this
.
waitActionLayerOpen
();
if
(
!
opened
)
{
await
this
.
skipWhenDataInsufficient
(
`行内「
${
name
}
」未打开弹层/详情`
);
}
await
this
.
closeOverlay
();
await
this
.
expectOnPage
();
return
;
...
...
@@ -278,11 +284,24 @@ export class IfaceSharedPage extends WhFlowSharedPage {
}
async
openAdvancedFilterDrawer
():
Promise
<
void
>
{
const
btn
=
this
.
button
(
'
高级筛选
'
).
first
();
const
scope
=
this
.
filterScope
();
const
expand
=
scope
.
getByText
(
'
展开
'
,
{
exact
:
true
}).
first
();
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
expand
,
'
展开筛选
'
).
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
300
);
}
const
btn
=
this
.
button
(
'
高级筛选
'
)
.
or
(
this
.
button
(
'
筛 选
'
))
.
or
(
this
.
button
(
'
筛选
'
))
.
or
(
scope
.
getByRole
(
'
button
'
,
{
name
:
/高
\s
*级
\s
*筛
\s
*选|筛
\s
*选/
}).
first
())
.
first
();
if
(
!
(
await
btn
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
无高级筛选/筛选入口
'
);
}
await
expect
(
btn
,
'
高级筛选
'
).
toBeVisible
();
await
this
.
click
(
btn
,
'
高级筛选
'
);
await
expect
(
this
.
page
.
locator
(
'
.ant-drawer, .ant-modal
'
).
first
(),
'
抽屉/面板
'
).
toBeVisible
();
const
close
=
this
.
button
(
'
关闭
'
).
or
(
this
.
button
(
'
取 消
'
)).
first
();
const
close
=
this
.
button
(
'
关闭
'
).
or
(
this
.
button
(
'
取
消
'
)).
or
(
this
.
button
(
'
取
消
'
)).
first
();
if
(
await
close
.
isVisible
().
catch
(()
=>
false
))
await
this
.
click
(
close
,
'
关闭
'
);
}
...
...
pages/spd/prodm-shared.page.ts
View file @
297802d0
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
headersJoinedMatchExpected
}
from
'
../../core/column-header-match.util
'
;
import
{
type
ProdmPageMeta
,
prodmMenuGroup
}
from
'
../../data/prodm.data
'
;
import
{
runWithMockEmptyListRoute
}
from
'
./helpers/mock-empty-route.helper
'
;
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
SpdProTableListControls
}
from
'
./helpers/spd-protable-list.controls
'
;
import
{
WhFlowSharedPage
}
from
'
./wh-flow-shared.page
'
;
...
...
@@ -177,20 +179,26 @@ export class ProdmSharedPage extends WhFlowSharedPage {
await
this
.
waitSettle
(
300
);
}
const
mock
=
this
.
prodm
.
mock
;
const
inputs
=
[
this
.
page
.
getByPlaceholder
(
/产品|物料|名称|编号|关键字|DI|目录|单号/
).
first
(),
this
.
page
.
locator
(
'
.ant-pro-table-search input.ant-input:visible
'
).
first
(),
this
.
page
.
locator
(
'
.ant-form input.ant-input:visible
'
).
first
(),
this
.
page
.
locator
(
'
input.ant-input:visible
'
).
first
(),
];
for
(
const
input
of
inputs
)
{
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
();
return
;
let
filled
=
false
;
await
runWithMockEmptyListRoute
(
this
.
page
,
mock
,
async
()
=>
{
const
inputs
=
[
this
.
page
.
getByPlaceholder
(
/产品|物料|名称|编号|关键字|DI|目录|单号/
).
first
(),
this
.
page
.
locator
(
'
.ant-pro-table-search input.ant-input:visible
'
).
first
(),
this
.
page
.
locator
(
'
.ant-form input.ant-input:visible
'
).
first
(),
this
.
page
.
locator
(
'
input.ant-input:visible
'
).
first
(),
];
for
(
const
input
of
inputs
)
{
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
mock
,
'
mock 关键字
'
);
await
this
.
clickQuery
();
filled
=
true
;
return
;
}
}
});
if
(
!
filled
)
{
await
this
.
skipWhenDataInsufficient
(
'
无可见筛选输入框,无法构造 mock 空态
'
);
}
await
this
.
skipWhenDataInsufficient
(
'
无可见筛选输入框,无法构造 mock 空态
'
);
}
async
assertConfiguredHeaders
():
Promise
<
void
>
{
...
...
@@ -198,7 +206,7 @@ export class ProdmSharedPage extends WhFlowSharedPage {
const
headers
=
await
this
.
headerTexts
();
const
joined
=
headers
.
join
(
''
);
for
(
const
name
of
this
.
prodm
.
headers
)
{
expect
(
joined
,
`表头应含
${
name
}
`
).
toContain
(
name
.
replace
(
/
\s
/g
,
''
)
);
expect
(
headersJoinedMatchExpected
(
joined
,
name
),
`表头应含
${
name
}
`
).
toBeTruthy
(
);
}
}
...
...
@@ -417,7 +425,19 @@ export class ProdmSharedPage extends WhFlowSharedPage {
}
async
openAdvancedFilterDrawer
():
Promise
<
void
>
{
const
btn
=
this
.
button
(
'
高级筛选
'
).
or
(
this
.
button
(
'
筛 选
'
)).
or
(
this
.
button
(
'
筛选
'
)).
first
();
const
expand
=
this
.
page
.
getByText
(
'
展开
'
,
{
exact
:
true
}).
first
();
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
expand
,
'
展开筛选
'
).
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
300
);
}
const
btn
=
this
.
button
(
'
高级筛选
'
)
.
or
(
this
.
button
(
'
筛 选
'
))
.
or
(
this
.
button
(
'
筛选
'
))
.
or
(
this
.
page
.
getByRole
(
'
button
'
,
{
name
:
/高
\s
*级
\s
*筛
\s
*选|筛
\s
*选/
}).
first
())
.
first
();
if
(
!
(
await
btn
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
无高级筛选/筛选入口
'
);
}
await
expect
(
btn
,
'
高级筛选
'
).
toBeVisible
();
await
this
.
click
(
btn
,
'
高级筛选
'
);
await
expect
(
this
.
page
.
locator
(
'
.ant-drawer, .ant-modal
'
).
first
(),
'
抽屉/面板
'
).
toBeVisible
();
...
...
pages/spd/surgery-shared.page.ts
View file @
297802d0
...
...
@@ -581,8 +581,18 @@ export class SurgerySharedPage extends BasePage {
/** ui-test-point-rules §3.7:默认 → 20 条/页 */
async
switchToPageSizeTwenty
():
Promise
<
{
total
:
number
;
rowsBefore
:
number
;
rowsAfter
:
number
}
>
{
const
total
=
await
this
.
getTotalCount
();
const
rowsBefore
=
await
this
.
getDataRowCount
();
let
total
=
await
this
.
getTotalCount
();
let
rowsBefore
=
await
this
.
getDataRowCount
();
const
expected
=
Math
.
min
(
total
,
20
);
// 已在 20 档且行数无法再变时,先降到 10 再升回,保证断言可观测
if
(
rowsBefore
===
expected
&&
total
>
10
)
{
await
this
.
changePageSize
(
10
);
rowsBefore
=
await
this
.
getDataRowCount
();
total
=
await
this
.
getTotalCount
();
}
if
(
rowsBefore
===
Math
.
min
(
total
,
20
))
{
await
this
.
skipWhenDataInsufficient
(
`总条数
${
total
}
不足以验证切换 20 条/页后行数变化`
);
}
const
{
rowsOnPage
:
rowsAfter
}
=
await
this
.
changePageSize
(
20
);
return
{
total
,
rowsBefore
,
rowsAfter
};
}
...
...
pages/spd/vendor-shared.page.ts
View file @
297802d0
...
...
@@ -87,7 +87,9 @@ export class VendorSharedPage extends BasePage {
async
expectOnPage
():
Promise
<
void
>
{
expect
(
this
.
page
.
url
(),
`URL 应含
${
this
.
meta
.
pathPart
}
`
).
toContain
(
this
.
meta
.
pathPart
);
for
(
const
part
of
this
.
meta
.
forbidden
??
[])
{
expect
(
this
.
page
.
url
(),
`不应进入
${
part
}
`
).
not
.
toContain
(
part
);
// 避免 regCertAudit 误伤 regCertAuditFinal(toContain 子串)
const
escaped
=
part
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
expect
(
this
.
page
.
url
(),
`不应进入
${
part
}
`
).
not
.
toMatch
(
new
RegExp
(
`
${
escaped
}
(?![A-Za-z0-9])`
));
}
await
this
.
waitContentReady
();
const
headers
=
await
this
.
headerTexts
();
...
...
pages/spd/wh-flow-shared.page.ts
View file @
297802d0
...
...
@@ -6,10 +6,11 @@ import { WhFlowSharedLocators } from '../../locators/spd/wh-flow-shared.locator'
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
clickPageSizeDropdownOption
,
pageSizeChangeObservability
,
pageSizeLabelsEqual
,
parsePageSizeFromLabel
,
skipIfInsufficientForPageSizeChange
,
}
from
'
./helpers/pagination-pagesize.helper
'
;
import
{
skipIfInsufficientForPageSizeChange
}
from
'
./helpers/pagination-pagesize.helper
'
;
export
type
WhFlowMeta
=
{
path
:
string
;
...
...
@@ -306,7 +307,38 @@ export class WhFlowSharedPage extends BasePage {
}
overlay
():
Locator
{
return
this
.
locators
.
putPage
().
or
(
this
.
locators
.
dialog
()).
or
(
this
.
page
.
locator
(
'
.ant-drawer-open
'
));
return
this
.
locators
.
putPage
()
.
or
(
this
.
locators
.
dialog
())
.
or
(
this
.
page
.
locator
(
'
.ant-drawer-open, .ant-drawer.ant-drawer-open
'
))
.
or
(
this
.
page
.
locator
(
'
.ant-modal-wrap:not(.ant-modal-wrap-hidden) .ant-modal
'
))
.
or
(
this
.
page
.
locator
(
'
.ant-modal-confirm:visible, .ant-popconfirm:visible
'
))
.
or
(
this
.
page
.
getByRole
(
'
dialog
'
).
first
())
.
or
(
this
.
page
.
getByRole
(
'
button
'
,
{
name
:
/返
\s
*回/
}).
first
());
}
/** 行内/工具栏动作后:弹层、抽屉、详情返回、确认框任一出现即视为打开。 */
async
waitActionLayerOpen
(
timeoutMs
=
10
_000
):
Promise
<
boolean
>
{
const
deadline
=
Date
.
now
()
+
timeoutMs
;
while
(
Date
.
now
()
<
deadline
)
{
if
(
await
this
.
overlay
().
first
().
isVisible
().
catch
(()
=>
false
))
return
true
;
if
(
await
this
.
page
.
locator
(
'
.ant-modal-wrap:not(.ant-modal-wrap-hidden), .ant-drawer-open, .put-page:visible, .ant-modal-confirm:visible, .ant-popconfirm:visible
'
,
)
.
first
()
.
isVisible
()
.
catch
(()
=>
false
)
)
{
return
true
;
}
if
(
await
this
.
page
.
getByRole
(
'
button
'
,
{
name
:
/返
\s
*回/
}).
first
().
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
await
this
.
page
.
waitForTimeout
(
200
);
}
return
false
;
}
async
closeOverlay
():
Promise
<
void
>
{
...
...
@@ -320,6 +352,13 @@ export class WhFlowSharedPage extends BasePage {
}
await
this
.
waitSettle
(
300
);
}
const
drawer
=
this
.
page
.
locator
(
'
.ant-drawer-open
'
).
first
();
if
(
await
drawer
.
isVisible
().
catch
(()
=>
false
))
{
const
cancel
=
drawer
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消|关
\s
*闭|返
\s
*回/
}).
first
();
if
(
await
cancel
.
isVisible
().
catch
(()
=>
false
))
await
this
.
click
(
cancel
,
'
关闭抽屉
'
);
else
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
waitSettle
(
300
);
}
const
put
=
this
.
locators
.
putPage
();
if
(
await
put
.
isVisible
().
catch
(()
=>
false
))
{
const
back
=
put
.
getByRole
(
'
button
'
,
{
name
:
/返
\s
*回|取
\s
*消|关
\s
*闭/
}).
first
();
...
...
@@ -327,6 +366,11 @@ export class WhFlowSharedPage extends BasePage {
else
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
waitSettle
(
300
);
}
const
backOnly
=
this
.
page
.
getByRole
(
'
button
'
,
{
name
:
/返
\s
*回/
}).
first
();
if
(
await
backOnly
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
backOnly
,
'
返回列表
'
);
await
this
.
waitSettle
(
300
);
}
}
async
interceptState
():
Promise
<
{
hintVisible
:
boolean
;
overlayOpen
:
boolean
}
>
{
...
...
@@ -392,7 +436,7 @@ export class WhFlowSharedPage extends BasePage {
}
}
/** 切换
为更小
每页条数
(如 20 条/页)
,返回切换前后可见行数与总条数。 */
/** 切换每页条数,返回切换前后可见行数与总条数。 */
async
switchToPageSize
(
pageSizeLabel
:
string
,
):
Promise
<
{
total
:
number
;
rowsBefore
:
number
;
rowsAfter
:
number
;
pageSizeLabel
:
string
}
>
{
...
...
@@ -402,12 +446,22 @@ export class WhFlowSharedPage extends BasePage {
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
}
后列表变化`
,
);
let
total
=
await
this
.
getTotalCount
();
let
rowsBefore
=
await
this
.
getDataRowCount
();
const
currentSize
=
parsePageSizeFromLabel
(
await
this
.
pageSizeChangerText
());
const
plan
=
pageSizeChangeObservability
({
total
,
rowsBefore
,
targetSize
:
sizeNum
,
currentSize
,
});
if
(
plan
.
shouldSkip
)
{
await
this
.
skipWhenDataInsufficient
(
plan
.
reason
??
`无法验证切换
${
pageSizeLabel
}
`
);
}
if
(
plan
.
needDropToSmallerFirst
)
{
await
this
.
changePageSizeTo
(
'
10 条/页
'
);
rowsBefore
=
await
this
.
getDataRowCount
();
total
=
await
this
.
getTotalCount
();
}
await
this
.
changePageSizeTo
(
pageSizeLabel
);
const
rowsAfter
=
await
this
.
getDataRowCount
();
...
...
tests/spd/admit-material.spec.ts
View file @
297802d0
...
...
@@ -24,6 +24,7 @@ test.describe('基础设置 · 准入管理', () => {
});
test
(
'
ADMIT-MAT-004 新增日期与销售商(自定义)等七项组合查询
'
,
async
({
admitMaterialPage
})
=>
{
test
.
setTimeout
(
180
_000
);
await
admitMaterialPage
.
open
();
await
admitMaterialPage
.
filterCombination
();
});
...
...
tests/spd/admit-product-apply.spec.ts
View file @
297802d0
...
...
@@ -14,7 +14,7 @@ test.describe('基础设置 · 准入管理', () => {
});
test
(
'
ADMIT-PAP-002 新增日期与销售商(自定义)等七项组合查询
'
,
async
({
admitProductApplyPage
})
=>
{
test
.
setTimeout
(
18
0
_000
);
test
.
setTimeout
(
24
0
_000
);
await
admitProductApplyPage
.
open
();
await
admitProductApplyPage
.
filterCombination
();
});
...
...
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