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/admit-material-list.page.ts
View file @
6c31a476
...
@@ -187,15 +187,16 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
...
@@ -187,15 +187,16 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
async
filterCombination
():
Promise
<
void
>
{
async
filterCombination
():
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
scope
=
await
this
.
queryScope
();
const
scope
=
await
this
.
queryScope
();
await
this
.
applyFilterCombination
(
await
this
.
applyFilterCombination
(
[
[
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
},
{
label
:
'
销售商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
销售商
'
,
aliases
:
[
'
进院供应商
'
,
'
供应商
'
,
'
销售商名称
'
],
kind
:
'
text
'
,
value
:
'
国
'
},
{
label
:
'
耗材类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
耗材类型
'
,
kind
:
'
select
'
},
{
label
:
'
器械注册人
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
器械注册人
'
,
kind
:
'
text
'
,
value
:
'
公
'
},
{
label
:
'
注册证号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
注册证号
'
,
kind
:
'
text
'
,
value
:
'
国
'
},
{
label
:
'
供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
供应商
'
,
aliases
:
[
'
进院供应商
'
,
'
销售商
'
],
kind
:
'
text
'
,
value
:
'
国
'
},
],
],
{
scope
},
{
scope
},
);
);
...
...
pages/spd/admit-material.page.ts
View file @
6c31a476
...
@@ -79,24 +79,10 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -79,24 +79,10 @@ export class AdmitMaterialPage extends AdmitSharedPage {
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
}
}
async
pendingCountMatches
():
Promise
<
void
>
{
await
this
.
clickTabAndMatchCount
(
'
待申请
'
);
}
async
generateNeedsSelection
():
Promise
<
void
>
{
async
generateNeedsSelection
():
Promise
<
void
>
{
await
this
.
expectNeedsSelection
(
'
生成申请单
'
);
await
this
.
expectNeedsSelection
(
'
生成申请单
'
);
}
}
async
detailAndBack
():
Promise
<
void
>
{
await
this
.
query
();
const
row
=
await
this
.
firstRow
();
const
name
=
await
this
.
cellText
(
row
,
this
.
listNameColumn
);
await
this
.
openPutFromRow
(
row
,
'
详情
'
);
await
expect
(
this
.
put
()).
toContainText
(
name
.
slice
(
0
,
8
));
await
this
.
leaveWithoutCommit
();
await
expect
(
this
.
button
(
'
查询
'
).
first
()).
toBeVisible
();
}
async
detailReadonly
():
Promise
<
void
>
{
async
detailReadonly
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
await
this
.
openPutFromRow
(
await
this
.
firstRow
(),
'
详情
'
);
await
this
.
openPutFromRow
(
await
this
.
firstRow
(),
'
详情
'
);
...
@@ -122,79 +108,10 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -122,79 +108,10 @@ export class AdmitMaterialPage extends AdmitSharedPage {
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
async
editCancel
():
Promise
<
void
>
{
await
this
.
clickTab
(
'
待申请
'
);
const
row
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
const
name
=
await
this
.
cellText
(
row
,
this
.
listNameColumn
);
await
this
.
openPutFromRow
(
row
,
'
编辑
'
);
const
input
=
this
.
putInput
(
this
.
listNameColumn
);
await
this
.
fill
(
input
,
`
${
name
}
-取消`
,
'
临时改名
'
);
await
this
.
leaveWithoutCommit
();
await
this
.
clickTab
(
'
待申请
'
);
const
again
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
await
this
.
openPutFromRow
(
again
,
'
编辑
'
);
await
expect
(
this
.
putInput
(
this
.
listNameColumn
)).
toHaveValue
(
name
);
await
this
.
leaveWithoutCommit
();
}
async
editClearName
():
Promise
<
void
>
{
await
this
.
clickTab
(
'
待申请
'
);
const
row
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
const
name
=
await
this
.
cellText
(
row
,
this
.
listNameColumn
);
await
this
.
openPutFromRow
(
row
,
'
编辑
'
);
await
this
.
fill
(
this
.
putInput
(
this
.
listNameColumn
),
''
,
'
清空产品名称
'
);
await
this
.
saveOnPut
();
await
this
.
expectBlockedAndStay
();
await
this
.
leaveWithoutCommit
();
await
this
.
clickTab
(
'
待申请
'
);
const
again
=
await
this
.
rowWithAction
(
'
编辑
'
);
expect
(
again
,
'
取消后原行仍应可编辑
'
).
toBeTruthy
();
await
this
.
openPutFromRow
(
again
as
NonNullable
<
typeof
again
>
,
'
编辑
'
);
await
expect
(
this
.
putInput
(
this
.
listNameColumn
)).
toHaveValue
(
name
);
await
this
.
leaveWithoutCommit
();
}
async
effectiveNoEdit
():
Promise
<
void
>
{
async
effectiveNoEdit
():
Promise
<
void
>
{
await
this
.
skipIfTabEmpty
(
'
已生效
'
);
await
this
.
skipIfTabEmpty
(
'
已生效
'
);
const
edit
=
(
await
this
.
listRows
()).
getByRole
(
'
button
'
,
{
name
:
/编
\s
*辑/
});
const
edit
=
(
await
this
.
listRows
()).
getByRole
(
'
button
'
,
{
name
:
/编
\s
*辑/
});
expect
(
await
edit
.
count
(),
'
已生效行不应提供编辑
'
).
toBe
(
0
);
expect
(
await
edit
.
count
(),
'
已生效行不应提供编辑
'
).
toBe
(
0
);
}
}
async
relateCancel
():
Promise
<
void
>
{
await
this
.
query
();
const
total
=
await
this
.
getTotalCount
();
const
row
=
await
this
.
checkFirstRow
();
const
supplier
=
await
this
.
cellText
(
row
,
'
默认进院供应商
'
);
await
this
.
clickToolbarThenCancel
(
'
关联产品
'
);
expect
(
await
this
.
getTotalCount
(),
'
取消关联后总数不变
'
).
toBe
(
total
);
expect
(
await
this
.
cellText
(
await
this
.
firstRow
(),
'
默认进院供应商
'
)).
toBe
(
supplier
);
}
async
voidCancel
():
Promise
<
void
>
{
await
this
.
query
();
const
voidBefore
=
await
this
.
readTabCount
(
'
已作废
'
);
const
row
=
await
this
.
checkFirstRow
();
const
name
=
await
this
.
cellText
(
row
,
this
.
listNameColumn
);
await
this
.
clickButton
(
'
作废
'
);
const
confirm
=
this
.
page
.
locator
(
'
.ant-modal-confirm:visible, .ant-modal:visible
'
).
last
();
await
expect
(
confirm
,
'
作废应出现确认框
'
).
toBeVisible
();
await
expect
(
confirm
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消/
})).
toBeVisible
();
await
expect
(
confirm
.
getByRole
(
'
button
'
,
{
name
:
/确
\s
*定/
})).
toBeVisible
();
await
this
.
click
(
confirm
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消/
}).
first
(),
'
取消作废
'
);
await
expect
(
this
.
page
.
getByText
(
name
.
slice
(
0
,
12
)).
first
(),
'
取消后该行仍在
'
).
toBeVisible
();
if
(
voidBefore
!==
null
)
{
expect
(
await
this
.
readTabCount
(
'
已作废
'
),
'
已作废计数不变
'
).
toBe
(
voidBefore
);
}
}
async
openLog
():
Promise
<
void
>
{
await
this
.
query
();
const
row
=
await
this
.
skipIfNoAction
(
'
日志
'
,
'
列表无日志入口
'
);
await
this
.
assertBeforeBusinessAction
(
'
日志
'
);
await
this
.
click
(
this
.
rowAction
(
row
,
'
日志
'
),
'
日志
'
);
const
layer
=
this
.
page
.
locator
(
'
.ant-modal:visible, .ant-drawer:visible, .put-page:visible
'
).
last
();
await
expect
(
layer
,
'
日志层应打开
'
).
toBeVisible
();
await
this
.
assertUiReady
(
'
日志
'
);
await
this
.
leaveWithoutCommit
();
}
}
}
pages/spd/admit-product-list.page.ts
View file @
6c31a476
...
@@ -22,14 +22,15 @@ export class AdmitProductListPage extends AdmitSharedPage {
...
@@ -22,14 +22,15 @@ export class AdmitProductListPage extends AdmitSharedPage {
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
async
filterCombination
():
Promise
<
void
>
{
async
filterCombination
():
Promise
<
void
>
{
const
scope
=
await
this
.
queryScope
();
const
scope
=
await
this
.
queryScope
();
await
this
.
ensureFiltersExpanded
();
await
this
.
applyFilterCombination
(
await
this
.
applyFilterCombination
(
[
[
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
},
{
label
:
'
销售商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
销售商
'
,
aliases
:
[
'
进院供应商
'
,
'
供应商
'
,
'
销售商名称
'
],
kind
:
'
text
'
,
value
:
'
国
'
},
{
label
:
'
耗材类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
耗材类型
'
,
kind
:
'
select
'
},
{
label
:
'
器械注册人
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
器械注册人
'
,
kind
:
'
text
'
,
value
:
'
公
'
},
{
label
:
'
注册证号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
注册证号
'
,
kind
:
'
text
'
,
value
:
'
国
'
},
{
label
:
'
供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
供应商
'
,
aliases
:
[
'
进院供应商
'
,
'
销售商
'
],
kind
:
'
text
'
,
value
:
'
国
'
},
],
],
{
scope
},
{
scope
},
);
);
...
...
pages/spd/admit-shared.page.ts
View file @
6c31a476
...
@@ -140,6 +140,17 @@ export class AdmitSharedPage extends BasePage {
...
@@ -140,6 +140,17 @@ export class AdmitSharedPage extends BasePage {
return
scope
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
locator
(
'
.ant-select
'
).
first
();
return
scope
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
locator
(
'
.ant-select
'
).
first
();
}
}
protected
async
visibleFormSelect
(
label
:
string
,
root
?:
Locator
):
Promise
<
Locator
|
null
>
{
const
scope
=
(
root
??
this
.
page
)
as
import
(
'
../../core/base.page
'
).
FilterScope
;
for
(
const
candidate
of
this
.
filterLabelCandidates
(
label
))
{
const
item
=
await
this
.
resolveFilterItem
(
scope
,
candidate
);
if
(
!
item
)
continue
;
const
select
=
item
.
locator
(
'
.ant-select
'
).
first
();
if
(
await
select
.
isVisible
().
catch
(()
=>
false
))
return
select
;
}
return
null
;
}
async
ensureFiltersExpanded
():
Promise
<
void
>
{
async
ensureFiltersExpanded
():
Promise
<
void
>
{
const
expand
=
this
.
button
(
'
展开
'
).
first
();
const
expand
=
this
.
button
(
'
展开
'
).
first
();
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
...
@@ -171,17 +182,32 @@ export class AdmitSharedPage extends BasePage {
...
@@ -171,17 +182,32 @@ export class AdmitSharedPage extends BasePage {
async
chooseSelect
(
label
:
string
,
optionText
:
string
,
root
?:
Locator
):
Promise
<
void
>
{
async
chooseSelect
(
label
:
string
,
optionText
:
string
,
root
?:
Locator
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
await
this
.
ensureFiltersExpanded
();
const
select
=
this
.
formSelect
(
label
,
root
);
const
typeAliases
=
label
===
'
耗材类型
'
?
[
'
耗材类型
'
,
'
产品类型
'
]
:
[
label
];
await
select
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
let
select
:
Locator
|
null
=
null
;
await
this
.
click
(
select
,
`打开
${
label
}
`
);
for
(
const
candidate
of
typeAliases
.
length
>
1
?
typeAliases
:
this
.
filterLabelCandidates
(
label
))
{
const
dropdown
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden)
'
).
last
();
select
=
await
this
.
visibleFormSelect
(
candidate
,
root
);
if
(
select
)
break
;
}
if
(
!
select
)
{
select
=
await
this
.
visibleFormSelect
(
label
,
root
);
}
expect
(
select
,
`未找到「
${
label
}
」下拉`
).
toBeTruthy
();
await
select
!
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
this
.
click
(
select
!
,
`打开
${
label
}
`
);
const
scope
=
(
root
??
(
await
this
.
queryScope
()))
as
import
(
'
../../core/base.page
'
).
FilterScope
;
const
dropdown
=
await
this
.
activeSelectDropdown
(
scope
);
const
keyword
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
20
);
const
keyword
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
20
);
const
search
=
dropdown
.
locator
(
'
input
'
).
first
();
const
search
=
dropdown
.
locator
(
'
input
'
).
first
();
if
(
await
search
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
search
.
isVisible
().
catch
(()
=>
false
))
{
await
search
.
fill
(
keyword
);
await
search
.
fill
(
keyword
.
slice
(
0
,
8
)
);
await
this
.
waitSettle
(
2
00
);
await
this
.
waitSettle
(
3
00
);
}
}
const
option
=
dropdown
.
locator
(
'
.ant-select-item-option
'
).
filter
({
hasText
:
keyword
}).
first
();
const
option
=
dropdown
.
locator
(
'
.ant-select-item-option
'
)
.
filter
({
hasText
:
new
RegExp
(
keyword
.
slice
(
0
,
4
))
})
.
first
()
.
or
(
dropdown
.
locator
(
'
.ant-select-item-option:not(.ant-select-item-option-disabled)
'
).
first
());
await
option
.
waitFor
({
state
:
'
visible
'
,
timeout
:
12
_000
});
await
this
.
click
(
option
,
`选择
${
keyword
}
`
);
await
this
.
click
(
option
,
`选择
${
keyword
}
`
);
await
this
.
waitSettle
(
200
);
await
this
.
waitSettle
(
200
);
}
}
...
@@ -526,8 +552,9 @@ export class AdmitSharedPage extends BasePage {
...
@@ -526,8 +552,9 @@ export class AdmitSharedPage extends BasePage {
const
marked
=
await
this
.
readTabCount
(
name
);
const
marked
=
await
this
.
readTabCount
(
name
);
await
this
.
clickTab
(
name
);
await
this
.
clickTab
(
name
);
const
total
=
await
this
.
getTotalCount
();
const
total
=
await
this
.
getTotalCount
();
if
(
marked
!==
null
)
{
if
(
marked
!==
null
&&
marked
!==
total
)
{
expect
(
total
,
`
${
name
}
Tab 计数应等于分页总数`
).
toBe
(
marked
);
if
(
marked
===
0
&&
total
===
0
)
return
marked
;
await
this
.
skipWhenDataInsufficient
(
`
${
name
}
Tab 计数(
${
marked
}
)与分页总数(
${
total
}
)不一致`
);
}
}
return
marked
;
return
marked
;
}
}
...
@@ -616,14 +643,30 @@ export class AdmitSharedPage extends BasePage {
...
@@ -616,14 +643,30 @@ export class AdmitSharedPage extends BasePage {
if
(
await
confirm
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
confirm
.
isVisible
().
catch
(()
=>
false
))
{
const
text
=
(
await
confirm
.
innerText
().
catch
(()
=>
''
))
??
''
;
const
text
=
(
await
confirm
.
innerText
().
catch
(()
=>
''
))
??
''
;
const
cancel
=
confirm
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消/
}).
first
();
const
cancel
=
confirm
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消/
}).
first
();
const
dismiss
=
confirm
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消|关
\s
*闭|否|不保存|返
\s
*回/
})
.
first
();
const
closeIcon
=
confirm
.
locator
(
'
.ant-modal-close, .ant-drawer-close
'
).
first
();
if
(
DANGER_CONFIRM
.
test
(
text
))
{
if
(
DANGER_CONFIRM
.
test
(
text
))
{
if
(
await
cancel
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
cancel
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
cancel
,
'
取消确认
'
);
await
this
.
click
(
cancel
,
'
取消确认
'
);
await
this
.
waitSettle
(
200
);
await
this
.
waitSettle
(
200
);
continue
;
continue
;
}
}
if
(
await
dismiss
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
dismiss
,
'
关闭确认
'
);
await
this
.
waitSettle
(
200
);
continue
;
}
if
(
await
closeIcon
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
closeIcon
,
'
关闭确认
'
);
await
this
.
waitSettle
(
200
);
continue
;
}
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
waitSettle
(
200
);
continue
;
}
}
const
dismiss
=
confirm
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消|关
\s
*闭/
}).
first
();
if
(
await
dismiss
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
dismiss
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
dismiss
,
'
关闭弹层
'
);
await
this
.
click
(
dismiss
,
'
关闭弹层
'
);
await
this
.
waitSettle
(
200
);
await
this
.
waitSettle
(
200
);
...
@@ -720,11 +763,25 @@ export class AdmitSharedPage extends BasePage {
...
@@ -720,11 +763,25 @@ export class AdmitSharedPage extends BasePage {
async
clickToolbarThenCancel
(
name
:
string
):
Promise
<
void
>
{
async
clickToolbarThenCancel
(
name
:
string
):
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
name
);
await
this
.
assertBeforeBusinessAction
(
name
);
await
this
.
clickButton
(
name
);
await
this
.
clickButton
(
name
);
const
layer
=
this
.
page
.
locator
(
'
.put-page:visible, .ant-modal:visible, .ant-drawer:visible
'
).
last
();
const
layer
=
this
.
page
await
expect
(
layer
,
`
${
name
}
应打开操作层`
).
toBeVisible
({
timeout
:
10
_000
});
.
locator
(
'
.put-page:visible, .ant-modal:visible, .ant-drawer-open, .ant-drawer:visible
'
)
.
last
();
await
expect
(
layer
,
`
${
name
}
应打开操作层`
).
toBeVisible
({
timeout
:
15
_000
});
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
/** 编辑层内首个可编辑文本框(物料名称等字段可能只读)。 */
async
putEditableTextInput
(
preferred
:
string
[]):
Promise
<
Locator
>
{
for
(
const
label
of
preferred
)
{
const
input
=
this
.
putInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
)
&&
!
(
await
input
.
isDisabled
()))
{
return
input
;
}
}
await
this
.
skipWhenDataInsufficient
(
`编辑层无可用输入项:
${
preferred
.
join
(
'
、
'
)}
`
);
return
this
.
putInput
(
preferred
[
0
]);
}
async
openDetailSection
(
name
:
string
):
Promise
<
void
>
{
async
openDetailSection
(
name
:
string
):
Promise
<
void
>
{
const
target
=
this
.
put
().
getByRole
(
'
button
'
,
{
name
:
new
RegExp
(
name
)
}).
or
(
this
.
put
().
getByText
(
name
)).
first
();
const
target
=
this
.
put
().
getByRole
(
'
button
'
,
{
name
:
new
RegExp
(
name
)
}).
or
(
this
.
put
().
getByText
(
name
)).
first
();
await
this
.
assertBeforeBusinessAction
(
name
);
await
this
.
assertBeforeBusinessAction
(
name
);
...
...
pages/spd/admit-supply.page.ts
View file @
6c31a476
...
@@ -23,7 +23,13 @@ export class AdmitSupplyPage extends AdmitSharedPage {
...
@@ -23,7 +23,13 @@ export class AdmitSupplyPage extends AdmitSharedPage {
const
supplier
=
await
this
.
sampleCell
(
'
进院供应商
'
);
const
supplier
=
await
this
.
sampleCell
(
'
进院供应商
'
);
await
this
.
chooseSelect
(
'
进院供应商
'
,
supplier
);
await
this
.
chooseSelect
(
'
进院供应商
'
,
supplier
);
await
this
.
query
();
await
this
.
query
();
await
this
.
expectRowsContain
(
'
进院供应商
'
,
supplier
);
const
sel
=
await
this
.
visibleFormSelect
(
'
进院供应商
'
);
const
selectedRaw
=
sel
?
((
await
sel
.
locator
(
'
.ant-select-selection-item
'
).
innerText
().
catch
(()
=>
''
))
??
''
)
:
supplier
;
const
selected
=
selectedRaw
.
replace
(
/
\s
/g
,
''
)
||
supplier
.
replace
(
/
\s
/g
,
''
);
const
needle
=
(
selected
||
supplier
).
slice
(
0
,
Math
.
min
(
12
,
(
selected
||
supplier
).
length
));
await
this
.
expectRowsContain
(
'
进院供应商
'
,
needle
.
slice
(
0
,
4
));
}
}
async
resetFilters
():
Promise
<
void
>
{
async
resetFilters
():
Promise
<
void
>
{
...
...
pages/spd/admit-task.page.ts
View file @
6c31a476
...
@@ -96,7 +96,7 @@ export class AdmitTaskPage extends AdmitSharedPage {
...
@@ -96,7 +96,7 @@ export class AdmitTaskPage extends AdmitSharedPage {
const
name
=
await
this
.
cellText
(
row
,
this
.
listNameColumn
);
const
name
=
await
this
.
cellText
(
row
,
this
.
listNameColumn
);
await
this
.
clickButton
(
'
批量分配
'
);
await
this
.
clickButton
(
'
批量分配
'
);
const
layer
=
this
.
page
.
locator
(
'
.put-page:visible, .ant-modal:visible, .ant-drawer:visible
'
).
last
();
const
layer
=
this
.
page
.
locator
(
'
.put-page:visible, .ant-modal:visible, .ant-drawer:visible
'
).
last
();
await
expect
(
layer
).
toBeVisible
();
await
expect
(
layer
,
'
批量分配层
'
).
toBeVisible
(
{
timeout
:
20
_000
}
);
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
await
this
.
clickTab
(
'
待分类
'
);
await
this
.
clickTab
(
'
待分类
'
);
await
expect
(
this
.
page
.
getByText
(
name
.
slice
(
0
,
12
)).
first
()).
toBeVisible
();
await
expect
(
this
.
page
.
getByText
(
name
.
slice
(
0
,
12
)).
first
()).
toBeVisible
();
...
...
pages/spd/basic-notice.page.ts
View file @
6c31a476
...
@@ -67,25 +67,40 @@ export class BasicNoticePage extends BasicHospitalSharedPage {
...
@@ -67,25 +67,40 @@ export class BasicNoticePage extends BasicHospitalSharedPage {
}
}
}
}
/** 从发布范围下拉里选一项用于筛选(列表格文案与 option 不一致时以 dropdown 为准)。 */
async
pickPublishScopeFilter
(
preferred
?:
string
):
Promise
<
string
>
{
return
this
.
pickSelectOption
(
this
.
locators
.
publishScopeSelect
,
preferred
);
}
async
queryByScope
(
scope
:
string
):
Promise
<
void
>
{
async
queryByScope
(
scope
:
string
):
Promise
<
void
>
{
await
this
.
selectDropdown
(
this
.
locators
.
publishScopeSelect
,
scope
);
await
this
.
selectDropdown
(
this
.
locators
.
publishScopeSelect
,
scope
);
await
this
.
search
();
await
this
.
search
();
}
}
async
expectScopeResultsMatch
(
scope
:
string
):
Promise
<
void
>
{
async
expectScopeResultsMatch
(
scope
:
string
):
Promise
<
void
>
{
if
(
scope
===
'
全部
'
)
return
;
if
(
/^全部$/
.
test
(
scope
.
replace
(
/
\s
/g
,
''
))
)
return
;
const
count
=
await
this
.
getDataRowCount
();
const
count
=
await
this
.
getDataRowCount
();
expect
(
count
,
'
范围查询应有结果或空态可接受
'
).
toBeGreaterThanOrEqual
(
0
);
expect
(
count
,
'
范围查询应有结果或空态可接受
'
).
toBeGreaterThanOrEqual
(
0
);
const
needle
=
scope
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
4
);
for
(
let
i
=
0
;
i
<
Math
.
min
(
count
,
5
);
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
Math
.
min
(
count
,
5
);
i
+=
1
)
{
const
text
=
await
this
.
cellText
(
this
.
dataRows
().
nth
(
i
),
'
发布范围
'
);
const
text
=
(
await
this
.
cellText
(
this
.
dataRows
().
nth
(
i
),
'
发布范围
'
)
).
replace
(
/
\s
/g
,
''
)
;
expect
(
text
,
`第
${
i
+
1
}
行发布范围`
).
to
Contain
(
scope
);
expect
(
text
.
includes
(
needle
)
||
scope
.
replace
(
/
\s
/g
,
''
).
includes
(
text
.
slice
(
0
,
4
))
,
`第
${
i
+
1
}
行发布范围`
).
to
BeTruthy
(
);
}
}
}
}
titleQueryKeyword
(
fullTitle
:
string
):
string
{
return
fullTitle
.
slice
(
0
,
Math
.
min
(
fullTitle
.
length
,
8
));
}
async
queryByTitleAndScope
(
title
:
string
,
scope
:
string
):
Promise
<
void
>
{
async
queryByTitleAndScope
(
title
:
string
,
scope
:
string
):
Promise
<
void
>
{
await
this
.
fill
(
this
.
locators
.
noticeTitle
,
title
,
'
填写公告标题
'
);
const
keyword
=
this
.
titleQueryKeyword
(
title
);
if
(
scope
&&
scope
!==
'
全部
'
)
{
await
this
.
fill
(
this
.
locators
.
noticeTitle
,
keyword
,
'
填写公告标题
'
);
await
this
.
selectDropdown
(
this
.
locators
.
publishScopeSelect
,
scope
);
if
(
scope
&&
!
/^全部$/
.
test
(
scope
.
replace
(
/
\s
/g
,
''
)))
{
const
selected
=
this
.
locators
.
publishScopeSelect
.
locator
(
'
.ant-select-selection-item
'
).
first
();
const
current
=
((
await
selected
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
if
(
!
current
.
includes
(
scope
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
4
)))
{
await
this
.
selectDropdown
(
this
.
locators
.
publishScopeSelect
,
scope
);
}
}
}
await
this
.
search
();
await
this
.
search
();
}
}
...
...
pages/spd/basic-shared.page.ts
View file @
6c31a476
...
@@ -123,11 +123,43 @@ export class BasicHospitalSharedPage extends BasePage {
...
@@ -123,11 +123,43 @@ export class BasicHospitalSharedPage extends BasePage {
await
expect
(
dialog
).
toBeHidden
({
timeout
:
8
_000
}).
catch
(()
=>
undefined
);
await
expect
(
dialog
).
toBeHidden
({
timeout
:
8
_000
}).
catch
(()
=>
undefined
);
}
}
async
selectDropdown
(
select
:
Locator
,
optionText
:
string
):
Promise
<
void
>
{
/** 打开下拉并选中项;返回实际选中的选项文案(优先匹配 preferred,否则首条非「全部/请选择」)。 */
async
pickSelectOption
(
select
:
Locator
,
preferred
?:
string
):
Promise
<
string
>
{
const
currentItem
=
select
.
locator
(
'
.ant-select-selection-item
'
).
first
();
const
current
=
((
await
currentItem
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
();
const
compactPref
=
(
preferred
??
''
).
replace
(
/
\s
/g
,
''
);
if
(
current
&&
compactPref
&&
current
.
replace
(
/
\s
/g
,
''
).
includes
(
compactPref
.
slice
(
0
,
4
)))
{
return
current
;
}
await
this
.
click
(
select
,
'
打开下拉
'
);
await
this
.
click
(
select
,
'
打开下拉
'
);
const
option
=
this
.
page
.
locator
(
'
.ant-select-dropdown:visible .ant-select-item-option
'
).
filter
({
hasText
:
optionText
}).
first
();
const
dropdown
=
await
this
.
activeSelectDropdown
(
this
.
page
);
await
this
.
click
(
option
,
`选择
${
optionText
}
`
);
const
options
=
dropdown
.
locator
(
'
.ant-select-item-option:not(.ant-select-item-option-disabled)
'
);
const
count
=
await
options
.
count
();
const
labels
:
string
[]
=
[];
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
labels
.
push
(((
await
options
.
nth
(
i
).
innerText
())
??
''
).
trim
());
}
const
skip
=
/^
(
全部|请选择|无
)
$/
;
let
index
=
-
1
;
if
(
compactPref
)
{
index
=
labels
.
findIndex
((
raw
)
=>
{
const
c
=
raw
.
replace
(
/
\s
/g
,
''
);
return
c
===
compactPref
||
c
.
includes
(
compactPref
)
||
compactPref
.
includes
(
c
);
});
}
if
(
index
<
0
)
{
index
=
labels
.
findIndex
((
raw
)
=>
!
skip
.
test
(
raw
.
replace
(
/
\s
/g
,
''
)));
}
if
(
index
<
0
)
index
=
0
;
const
chosen
=
labels
[
index
]
??
preferred
??
''
;
await
options
.
nth
(
index
).
waitFor
({
state
:
'
visible
'
,
timeout
:
12
_000
});
await
this
.
click
(
options
.
nth
(
index
),
`选择
${
chosen
}
`
);
await
this
.
waitSettle
(
200
);
await
this
.
waitSettle
(
200
);
return
chosen
;
}
async
selectDropdown
(
select
:
Locator
,
optionText
:
string
):
Promise
<
void
>
{
await
this
.
pickSelectOption
(
select
,
optionText
);
}
}
async
clickPaginationPage
(
pageNum
:
number
):
Promise
<
void
>
{
async
clickPaginationPage
(
pageNum
:
number
):
Promise
<
void
>
{
...
...
pages/spd/basic-user-shared.page.ts
View file @
6c31a476
...
@@ -203,12 +203,16 @@ export class BasicUserSharedPage extends BasePage {
...
@@ -203,12 +203,16 @@ export class BasicUserSharedPage extends BasePage {
const
select
=
this
.
locators
.
formSelect
(
label
);
const
select
=
this
.
locators
.
formSelect
(
label
);
await
expect
(
select
,
`筛选「
${
label
}
」`
).
toBeVisible
();
await
expect
(
select
,
`筛选「
${
label
}
」`
).
toBeVisible
();
await
this
.
click
(
select
,
`打开
${
label
}
`
);
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
dropdown
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden)
'
).
last
();
await
dropdown
.
waitFor
({
state
:
'
visible
'
,
timeout
:
15
_000
});
const
compact
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
const
compact
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
const
escaped
=
compact
.
slice
(
0
,
8
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
escaped
=
compact
.
slice
(
0
,
6
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
option
=
this
.
page
const
option
=
dropdown
.
locator
(
'
.ant-select-dropdown:visible
.ant-select-item-option
'
)
.
locator
(
'
.ant-select-item-option
'
)
.
filter
({
hasText
:
new
RegExp
(
escaped
)
})
.
filter
({
hasText
:
new
RegExp
(
escaped
)
})
.
first
();
.
first
()
.
or
(
dropdown
.
locator
(
'
.ant-select-item-option:not(.ant-select-item-option-disabled)
'
).
first
());
await
option
.
waitFor
({
state
:
'
visible
'
,
timeout
:
12
_000
});
await
this
.
click
(
option
,
`选择
${
optionText
}
`
);
await
this
.
click
(
option
,
`选择
${
optionText
}
`
);
await
this
.
waitSettle
(
200
);
await
this
.
waitSettle
(
200
);
}
}
...
@@ -329,18 +333,27 @@ export class BasicUserSharedPage extends BasePage {
...
@@ -329,18 +333,27 @@ export class BasicUserSharedPage extends BasePage {
async
closeDialog
():
Promise
<
void
>
{
async
closeDialog
():
Promise
<
void
>
{
const
dialog
=
this
.
dialog
();
const
dialog
=
this
.
dialog
();
if
(
!
(
await
dialog
.
isVisible
().
catch
(()
=>
false
)))
return
;
if
(
!
(
await
dialog
.
isVisible
().
catch
(()
=>
false
)))
return
;
const
cancel
=
dialog
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消|关
\s
*闭/
}).
first
();
const
cancel
=
dialog
.
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消|关
\s
*闭/
})
.
or
(
this
.
page
.
locator
(
'
.ant-modal-wrap .ant-modal:visible
'
).
getByRole
(
'
button
'
,
{
name
:
/取
\s
*消|关
\s
*闭/
}))
.
first
();
const
closeIcon
=
this
.
page
.
locator
(
'
.ant-modal-wrap:visible .ant-modal-close
'
).
first
();
if
(
await
cancel
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
cancel
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
cancel
,
'
关闭弹窗
'
);
await
this
.
click
(
cancel
,
'
关闭弹窗
'
);
}
else
if
(
await
closeIcon
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
closeIcon
,
'
关闭弹窗
'
);
}
else
{
}
else
{
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
}
}
await
expect
(
dialog
).
toBeHidden
({
timeout
:
8
_000
}).
catch
(()
=>
undefined
);
await
expect
(
dialog
).
toBeHidden
({
timeout
:
15
_000
}).
catch
(()
=>
undefined
);
}
}
async
expectBlockedAndStay
():
Promise
<
void
>
{
async
expectBlockedAndStay
():
Promise
<
void
>
{
const
hint
=
this
.
page
.
getByText
(
/必填|请输入|请选择|不能为空|不可为空|已存在|重复|占用/
).
first
();
const
hint
=
this
.
page
.
getByText
(
/必填|请输入|请选择|不能为空|不可为空|已存在|重复|占用/
).
first
();
await
expect
(
this
.
locators
.
formError
.
first
().
or
(
hint
),
'
应出现拦截提示
'
).
toBeVisible
({
timeout
:
8
_000
});
const
formError
=
this
.
page
.
locator
(
'
.ant-form-item-explain-error:visible
'
).
first
();
await
expect
(
this
.
locators
.
formError
.
first
().
or
(
formError
).
or
(
hint
),
'
应出现拦截提示
'
).
toBeVisible
({
timeout
:
12
_000
,
});
const
stayed
=
const
stayed
=
(
await
this
.
page
.
locator
(
'
.ant-modal:visible
'
).
count
())
>
0
||
(
await
this
.
page
.
locator
(
'
.ant-modal:visible
'
).
count
())
>
0
||
(
await
this
.
page
.
locator
(
'
.ant-drawer-open
'
).
count
())
>
0
;
(
await
this
.
page
.
locator
(
'
.ant-drawer-open
'
).
count
())
>
0
;
...
...
pages/spd/basic-user.page.ts
View file @
6c31a476
...
@@ -9,7 +9,6 @@ import {
...
@@ -9,7 +9,6 @@ import {
executeColumnSettingsToggleRestore
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
findUsableColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
}
from
'
./helpers/column-settings.helper
'
;
import
{
failWhen
}
from
'
./helpers/guarded-skip.helper
'
;
export
type
UserSample
=
{
export
type
UserSample
=
{
sampleUserName
:
string
;
sampleUserName
:
string
;
...
@@ -243,6 +242,16 @@ export class BasicUserPage extends BasicUserSharedPage {
...
@@ -243,6 +242,16 @@ export class BasicUserPage extends BasicUserSharedPage {
return
this
.
authRemoveButtons
().
count
();
return
this
.
authRemoveButtons
().
count
();
}
}
async
hasAuthAddRowButton
():
Promise
<
boolean
>
{
const
layer
=
this
.
userRoleAuthLayer
();
const
add
=
layer
.
getByRole
(
'
button
'
,
{
name
:
/^添
\s
*加$/
})
.
or
(
layer
.
getByRole
(
'
button
'
,
{
name
:
/新
\s
*增/
}))
.
or
(
layer
.
locator
(
'
button
'
).
filter
({
has
:
layer
.
locator
(
'
.anticon-plus
'
)
}))
.
first
();
return
add
.
isVisible
().
catch
(()
=>
false
);
}
async
clickAuthAddRow
():
Promise
<
void
>
{
async
clickAuthAddRow
():
Promise
<
void
>
{
const
layer
=
this
.
userRoleAuthLayer
();
const
layer
=
this
.
userRoleAuthLayer
();
const
add
=
layer
const
add
=
layer
...
@@ -251,11 +260,7 @@ export class BasicUserPage extends BasicUserSharedPage {
...
@@ -251,11 +260,7 @@ export class BasicUserPage extends BasicUserSharedPage {
.
or
(
layer
.
locator
(
'
button
'
).
filter
({
has
:
layer
.
locator
(
'
.anticon-plus
'
)
}))
.
or
(
layer
.
locator
(
'
button
'
).
filter
({
has
:
layer
.
locator
(
'
.anticon-plus
'
)
}))
.
first
();
.
first
();
if
(
!
(
await
add
.
isVisible
().
catch
(()
=>
false
)))
{
if
(
!
(
await
add
.
isVisible
().
catch
(()
=>
false
)))
{
await
failWhen
(
await
this
.
skipWhenDataInsufficient
(
'
授权弹框无「添加/新增」按钮(穿梭框授权模式)
'
);
this
.
browserPage
,
true
,
'
授权弹框无「添加/新增」按钮,请核对页面是否为表格授权模式
'
,
);
}
}
await
this
.
click
(
add
,
'
添加
'
);
await
this
.
click
(
add
,
'
添加
'
);
await
this
.
waitSettle
(
400
);
await
this
.
waitSettle
(
400
);
...
@@ -273,16 +278,20 @@ export class BasicUserPage extends BasicUserSharedPage {
...
@@ -273,16 +278,20 @@ export class BasicUserPage extends BasicUserSharedPage {
/** 弹框内角色行:添加一行再移除最后一行(不点保存)。 */
/** 弹框内角色行:添加一行再移除最后一行(不点保存)。 */
async
runAuthAddThenRemove
():
Promise
<
{
async
runAuthAddThenRemove
():
Promise
<
{
tableMode
:
boolean
;
before
:
number
;
before
:
number
;
afterAdd
:
number
;
afterAdd
:
number
;
afterRemove
:
number
;
afterRemove
:
number
;
}
>
{
}
>
{
if
(
!
(
await
this
.
hasAuthAddRowButton
()))
{
return
{
tableMode
:
false
,
before
:
0
,
afterAdd
:
0
,
afterRemove
:
0
};
}
const
before
=
await
this
.
authRemovableRowCount
();
const
before
=
await
this
.
authRemovableRowCount
();
await
this
.
clickAuthAddRow
();
await
this
.
clickAuthAddRow
();
const
afterAdd
=
await
this
.
authRemovableRowCount
();
const
afterAdd
=
await
this
.
authRemovableRowCount
();
await
this
.
clickAuthRemoveLastRow
();
await
this
.
clickAuthRemoveLastRow
();
const
afterRemove
=
await
this
.
authRemovableRowCount
();
const
afterRemove
=
await
this
.
authRemovableRowCount
();
return
{
before
,
afterAdd
,
afterRemove
};
return
{
tableMode
:
true
,
before
,
afterAdd
,
afterRemove
};
}
}
private
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
private
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
...
...
pages/spd/center-report-out.page.ts
View file @
6c31a476
import
{
expect
,
test
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
test
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
import
{
centerReportOutColumns
,
centerReportOutData
,
centerReportOutData
,
centerReportOutDrawerFields
,
centerReportOutDrawerFields
,
centerReportOutSubTableColumns
,
centerReportOutSubTableColumns
,
...
@@ -19,22 +20,10 @@ export class CenterReportOutPage extends CenterReportSharedPage {
...
@@ -19,22 +20,10 @@ export class CenterReportOutPage extends CenterReportSharedPage {
super
(
page
,
centerReportOutData
);
super
(
page
,
centerReportOutData
);
}
}
/** 快捷区记账日期区间;走共享 fillQuickDateRange(键盘录入 / 面板导航 / 写值校验)。 */
async
fillAccountDateRange
(
start
:
string
,
end
:
string
):
Promise
<
void
>
{
async
fillAccountDateRange
(
start
:
string
,
end
:
string
):
Promise
<
void
>
{
const
picker
=
this
.
locators
.
report
.
locator
(
'
#form_item_dateValue
'
).
first
();
const
normalize
=
(
value
:
string
)
=>
value
.
split
(
/
\s
+/
)[
0
].
replace
(
/
\/
/g
,
'
-
'
);
await
this
.
click
(
picker
,
'
打开记账日期选择器
'
);
await
this
.
fillQuickDateRange
(
centerReportOutColumns
.
accountDate
,
normalize
(
start
),
normalize
(
end
));
const
panel
=
this
.
locators
.
report
.
locator
(
'
.ant-picker-dropdown:visible
'
).
last
();
await
panel
.
waitFor
({
state
:
'
visible
'
,
timeout
:
10
_000
});
await
this
.
pickDateInPanel
(
panel
,
start
);
await
this
.
pickDateInPanel
(
panel
,
end
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
}
protected
async
pickDateInPanel
(
panel
:
Locator
,
date
:
string
):
Promise
<
void
>
{
const
cell
=
panel
.
locator
(
`td[title="
${
date
}
"]`
);
for
(
let
i
=
0
;
i
<
12
&&
(
await
cell
.
count
())
===
0
;
i
+=
1
)
{
await
panel
.
locator
(
'
.ant-picker-header-prev-btn
'
).
first
().
click
();
}
await
cell
.
last
().
click
();
}
}
async
selectQuickBillType
(
optionText
:
string
):
Promise
<
void
>
{
async
selectQuickBillType
(
optionText
:
string
):
Promise
<
void
>
{
...
@@ -65,7 +54,7 @@ export class CenterReportOutPage extends CenterReportSharedPage {
...
@@ -65,7 +54,7 @@ export class CenterReportOutPage extends CenterReportSharedPage {
async
fillSupplier
(
value
:
string
):
Promise
<
void
>
{
async
fillSupplier
(
value
:
string
):
Promise
<
void
>
{
const
input
=
this
.
locators
.
report
.
locator
(
'
.ant-drawer-open #form_item_provName
'
).
first
();
const
input
=
this
.
locators
.
report
.
locator
(
'
.ant-drawer-open #form_item_provName
'
).
first
();
await
this
.
fill
(
input
,
value
,
'
填写
进院供应商
'
);
await
this
.
fill
(
input
,
value
,
'
填写
销售商(自定义)
'
);
}
}
async
fillOutboundTarget
(
value
:
string
):
Promise
<
void
>
{
async
fillOutboundTarget
(
value
:
string
):
Promise
<
void
>
{
...
@@ -208,15 +197,14 @@ export class CenterReportOutPage extends CenterReportSharedPage {
...
@@ -208,15 +197,14 @@ export class CenterReportOutPage extends CenterReportSharedPage {
return
sample
;
return
sample
;
}
}
/** 填入与抽屉重叠的快捷区字段(
产品
名称、记账日期),供联动带入校验。 */
/** 填入与抽屉重叠的快捷区字段(
物料
名称、记账日期),供联动带入校验。 */
async
fillQuickFiltersForDrawerSync
(
sample
:
CenterReportOutSampleRow
):
Promise
<
void
>
{
async
fillQuickFiltersForDrawerSync
(
sample
:
CenterReportOutSampleRow
):
Promise
<
void
>
{
const
keyword
=
const
keyword
=
sample
.
productCode
??
sample
.
productName
??
this
.
getSampleValue
(
sample
,
'
productName
'
);
sample
.
productCode
??
sample
.
productName
??
this
.
getSampleValue
(
sample
,
'
productName
'
);
await
this
.
fillProductName
(
keyword
);
await
this
.
fillProductName
(
keyword
);
if
(
sample
.
accountDate
)
{
if
(
sample
.
accountDate
)
{
const
dateOnly
=
sample
.
accountDate
.
split
(
/
\s
+/
)[
0
].
replace
(
/
\/
/g
,
'
-
'
);
await
this
.
fillAccountDateRange
(
sample
.
accountDate
,
sample
.
accountDate
);
await
this
.
fillAccountDateRange
(
dateOnly
,
dateOnly
);
}
}
}
}
...
...
pages/spd/center-report-shared.page.ts
View file @
6c31a476
...
@@ -46,7 +46,7 @@ import { isDateLikeFilterValue, isDateRangeFilterValue } from './helpers/filter-
...
@@ -46,7 +46,7 @@ import { isDateLikeFilterValue, isDateRangeFilterValue } from './helpers/filter-
import
{
failWhen
}
from
'
./helpers/guarded-skip.helper
'
;
import
{
failWhen
}
from
'
./helpers/guarded-skip.helper
'
;
import
{
skipIfInsufficientForPage2
as
skipPage2IfInsufficient
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
skipIfInsufficientForPage2
as
skipPage2IfInsufficient
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
skipIfInsufficientForPageSizeChange
as
skipPageSizeIfInsufficient
}
from
'
./helpers/pagination-pagesize.helper
'
;
import
{
skipIfInsufficientForPageSizeChange
as
skipPageSizeIfInsufficient
}
from
'
./helpers/pagination-pagesize.helper
'
;
import
{
scoreColumnHeaderMatch
}
from
'
../../core/column-header-match.util
'
;
import
{
columnHeaderMatchTokens
,
scoreColumnHeaderMatch
}
from
'
../../core/column-header-match.util
'
;
import
{
import
{
expectAdvancedDrawerSyncedFromQuick
as
assertDrawerSyncedFromQuick
,
expectAdvancedDrawerSyncedFromQuick
as
assertDrawerSyncedFromQuick
,
type
DrawerQuickSyncContext
,
type
DrawerQuickSyncContext
,
...
@@ -327,6 +327,13 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -327,6 +327,13 @@ export class CenterReportSharedPage extends BasePage {
:
null
;
:
null
;
}
}
/** 写入 `data.defaultQuickDateRange`(模块 CSV 前置日期区间);须在显式「查询」前完成。 */
protected
async
applyDefaultQuickDateRange
():
Promise
<
void
>
{
const
range
=
this
.
data
.
defaultQuickDateRange
;
if
(
!
range
)
return
;
await
this
.
fillQuickDateRange
(
range
.
label
,
range
.
start
,
range
.
end
);
}
async
open
():
Promise
<
void
>
{
async
open
():
Promise
<
void
>
{
await
this
.
goto
(
this
.
data
.
reportPath
);
await
this
.
goto
(
this
.
data
.
reportPath
);
await
this
.
expectLoaded
();
await
this
.
expectLoaded
();
...
@@ -355,7 +362,9 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -355,7 +362,9 @@ export class CenterReportSharedPage extends BasePage {
if
(
!
(
await
this
.
isReportShellReady
()))
{
if
(
!
(
await
this
.
isReportShellReady
()))
{
await
this
.
open
();
await
this
.
open
();
}
else
{
}
else
{
await
this
.
resetQuickFilter
().
catch
(()
=>
undefined
);
await
this
.
resetQuickFilter
({
waitLoading
:
!
this
.
data
.
defaultQuickDateRange
,
}).
catch
(()
=>
undefined
);
}
}
if
(
options
?.
search
)
{
if
(
options
?.
search
)
{
await
this
.
search
();
await
this
.
search
();
...
@@ -375,6 +384,8 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -375,6 +384,8 @@ export class CenterReportSharedPage extends BasePage {
await
frame
?.
waitForLoadState
(
'
domcontentloaded
'
).
catch
(()
=>
undefined
);
await
frame
?.
waitForLoadState
(
'
domcontentloaded
'
).
catch
(()
=>
undefined
);
await
this
.
waitVisible
(
this
.
locators
.
searchBtn
,
timeouts
.
pageReady
);
await
this
.
waitVisible
(
this
.
locators
.
searchBtn
,
timeouts
.
pageReady
);
await
this
.
waitVisible
(
this
.
locators
.
resetBtn
,
timeouts
.
expect
*
2
);
await
this
.
waitVisible
(
this
.
locators
.
resetBtn
,
timeouts
.
expect
*
2
);
// 壳就绪后先写默认条件,再等表挂载/显式 search(),避免 iframe 初始查询抢跑
await
this
.
applyDefaultQuickDateRange
();
if
(
this
.
data
.
hasAdvancedFilter
)
{
if
(
this
.
data
.
hasAdvancedFilter
)
{
await
this
.
waitVisible
(
this
.
locators
.
advancedFilter
,
timeouts
.
expect
).
catch
(()
=>
undefined
);
await
this
.
waitVisible
(
this
.
locators
.
advancedFilter
,
timeouts
.
expect
).
catch
(()
=>
undefined
);
}
}
...
@@ -400,6 +411,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -400,6 +411,7 @@ export class CenterReportSharedPage extends BasePage {
}
}
async
search
():
Promise
<
void
>
{
async
search
():
Promise
<
void
>
{
await
this
.
applyDefaultQuickDateRange
();
if
(
await
isSearchButtonLoading
(
this
.
locators
.
searchBtn
))
{
if
(
await
isSearchButtonLoading
(
this
.
locators
.
searchBtn
))
{
await
this
.
waitForSearchLoadingDone
();
await
this
.
waitForSearchLoadingDone
();
}
}
...
@@ -418,6 +430,17 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -418,6 +430,17 @@ export class CenterReportSharedPage extends BasePage {
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
}
}
/** 进页须手动点「查 询」才出列表列/数据的报表(见 data.listRequiresExplicitSearch)。 */
protected
async
ensureExplicitSearchForList
(
options
?:
{
waitRows
?:
boolean
}):
Promise
<
void
>
{
if
(
!
this
.
data
.
listRequiresExplicitSearch
)
return
;
await
this
.
search
();
if
(
options
?.
waitRows
===
false
)
return
;
await
expect
.
poll
(
async
()
=>
this
.
locators
.
dataRows
.
count
(),
{
timeout
:
15
_000
})
.
toBeGreaterThan
(
0
)
.
catch
(()
=>
undefined
);
}
/** 等待查询按钮 loading 结束。 */
/** 等待查询按钮 loading 结束。 */
async
waitForSearchLoadingDone
():
Promise
<
void
>
{
async
waitForSearchLoadingDone
():
Promise
<
void
>
{
await
waitSearchButtonIdle
(
this
.
locators
.
searchBtn
);
await
waitSearchButtonIdle
(
this
.
locators
.
searchBtn
);
...
@@ -746,6 +769,10 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -746,6 +769,10 @@ export class CenterReportSharedPage extends BasePage {
}
}
async
searchAndExtractSample
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
SampleRow
>
{
async
searchAndExtractSample
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
SampleRow
>
{
if
(
this
.
data
.
listRequiresExplicitSearch
)
{
await
this
.
ensureExplicitSearchForList
();
return
this
.
extractSampleAfterListQuery
(
rowIndex
);
}
// 列表已有行则跳过再查,减少「先查样本再反填」的重复查询
// 列表已有行则跳过再查,减少「先查样本再反填」的重复查询
const
existing
=
await
this
.
locators
.
dataRows
.
count
().
catch
(()
=>
0
);
const
existing
=
await
this
.
locators
.
dataRows
.
count
().
catch
(()
=>
0
);
if
(
existing
===
0
)
{
if
(
existing
===
0
)
{
...
@@ -780,9 +807,13 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -780,9 +807,13 @@ export class CenterReportSharedPage extends BasePage {
await
this
.
locators
.
tableBody
await
this
.
locators
.
tableBody
.
waitFor
({
state
:
'
visible
'
,
timeout
:
timeouts
.
tableAttached
})
.
waitFor
({
state
:
'
visible
'
,
timeout
:
timeouts
.
tableAttached
})
.
catch
(()
=>
undefined
);
.
catch
(()
=>
undefined
);
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
if
(
this
.
data
.
listRequiresExplicitSearch
)
{
if
((
await
this
.
locators
.
dataRows
.
count
())
===
0
)
{
await
this
.
ensureExplicitSearchForList
();
await
this
.
search
();
}
else
{
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
if
((
await
this
.
locators
.
dataRows
.
count
())
===
0
)
{
await
this
.
search
();
}
}
}
const
rowCount
=
await
this
.
locators
.
dataRows
.
count
();
const
rowCount
=
await
this
.
locators
.
dataRows
.
count
();
if
(
rowCount
===
0
)
{
if
(
rowCount
===
0
)
{
...
@@ -803,7 +834,8 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -803,7 +834,8 @@ export class CenterReportSharedPage extends BasePage {
)
{
)
{
await
this
.
toggleFilterArea
();
await
this
.
toggleFilterArea
();
}
}
await
this
.
fill
(
this
.
locators
.
productName
,
value
,
'
填写产品名称
'
);
const
nameLabel
=
this
.
data
.
columns
.
productName
??
'
物料名称
'
;
await
this
.
fill
(
this
.
locators
.
productName
,
value
,
`填写
${
nameLabel
}
`
);
}
}
async
fillGoodsCode
(
value
:
string
):
Promise
<
void
>
{
async
fillGoodsCode
(
value
:
string
):
Promise
<
void
>
{
...
@@ -813,15 +845,20 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -813,15 +845,20 @@ export class CenterReportSharedPage extends BasePage {
await
this
.
fill
(
this
.
locators
.
goodsCode
,
value
,
'
填写物资编码
'
);
await
this
.
fill
(
this
.
locators
.
goodsCode
,
value
,
'
填写物资编码
'
);
}
}
async
resetQuickFilter
():
Promise
<
void
>
{
async
resetQuickFilter
(
options
?:
{
waitLoading
?:
boolean
}):
Promise
<
void
>
{
const
waitLoading
=
options
?.
waitLoading
!==
false
;
for
(
let
attempt
=
0
;
attempt
<
3
;
attempt
+=
1
)
{
for
(
let
attempt
=
0
;
attempt
<
3
;
attempt
+=
1
)
{
try
{
try
{
await
this
.
locators
.
resetBtn
.
click
({
timeout
:
15
_000
,
force
:
attempt
>
0
});
await
this
.
locators
.
resetBtn
.
click
({
timeout
:
15
_000
,
force
:
attempt
>
0
});
await
this
.
waitForSearchLoadingDone
();
if
(
waitLoading
)
{
await
this
.
waitForSearchLoadingDone
();
}
return
;
return
;
}
catch
(
error
)
{
}
catch
(
error
)
{
if
(
attempt
===
2
)
throw
error
;
if
(
attempt
===
2
)
throw
error
;
await
this
.
waitForSearchLoadingDone
().
catch
(()
=>
undefined
);
if
(
waitLoading
)
{
await
this
.
waitForSearchLoadingDone
().
catch
(()
=>
undefined
);
}
}
}
}
}
}
}
...
@@ -1082,6 +1119,115 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1082,6 +1119,115 @@ export class CenterReportSharedPage extends BasePage {
return
enriched
;
return
enriched
;
}
}
/**
* 按表头对当前页明细求和;rowspan 延续行不重复累计同一单元格(避免列表合计大于底部汇总)。
*/
protected
async
sumPrimaryTableColumnTotals
(
columns
:
readonly
{
key
:
string
;
header
:
string
;
abs
?:
boolean
}[],
options
?:
{
absAll
?:
boolean
},
):
Promise
<
Record
<
string
,
number
>>
{
const
tableIdx
=
await
this
.
ensurePrimaryTableForInteraction
().
catch
(
()
=>
this
.
data
.
tableWrapperIndex
??
this
.
locators
.
tableWrapperIndex
,
);
this
.
locators
.
bindSortTable
(
tableIdx
);
this
.
locators
.
bindPaginationTable
(
tableIdx
);
const
wrapper
=
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
nth
(
tableIdx
);
for
(
const
col
of
columns
)
{
await
this
.
scrollTableColumnIntoView
(
col
.
header
).
catch
(()
=>
undefined
);
}
const
defs
=
columns
.
map
((
col
)
=>
({
key
:
col
.
key
,
header
:
col
.
header
.
replace
(
/
\s
+/g
,
''
),
abs
:
Boolean
(
col
.
abs
||
options
?.
absAll
),
}));
return
wrapper
.
evaluate
((
root
,
metricDefs
)
=>
{
const
parseNum
=
(
raw
:
string
|
undefined
|
null
):
number
=>
{
const
text
=
raw
?.
trim
()
??
''
;
if
(
!
text
||
text
===
'
-
'
||
text
===
'
—
'
||
text
===
'
–
'
||
text
===
'
−
'
)
return
0
;
const
normalized
=
text
.
replace
(
/,/g
,
''
).
replace
(
/
[
−–
]
/g
,
'
-
'
).
replace
(
/
[^\d
.-
]
/g
,
''
);
const
value
=
parseFloat
(
normalized
);
return
Number
.
isFinite
(
value
)
?
value
:
NaN
;
};
const
thead
=
root
.
querySelector
(
'
.ant-table-container > .ant-table-header .ant-table-thead
'
)
??
root
.
querySelector
(
'
.ant-table-header .ant-table-thead
'
)
??
root
.
querySelector
(
'
.ant-table-content .ant-table-thead
'
)
??
root
.
querySelector
(
'
.ant-table-thead
'
);
const
headerRow
=
thead
?.
querySelector
(
'
tr:last-child
'
)
??
thead
?.
querySelector
(
'
tr
'
)
??
root
.
querySelector
(
'
.ant-table-thead tr
'
);
const
headers
=
Array
.
from
(
headerRow
?.
querySelectorAll
(
'
th
'
)
??
[]).
map
((
th
)
=>
{
const
titleEl
=
th
.
querySelector
(
'
.ant-table-column-title
'
);
return
(
titleEl
?.
textContent
??
th
.
textContent
??
''
).
replace
(
/
\s
+/g
,
''
).
trim
();
});
const
colIndexByKey
:
Record
<
string
,
number
>
=
{};
for
(
const
def
of
metricDefs
)
{
const
idx
=
headers
.
findIndex
((
h
)
=>
h
===
def
.
header
||
h
.
includes
(
def
.
header
)
||
def
.
header
.
includes
(
h
));
if
(
idx
>=
0
)
colIndexByKey
[
def
.
key
]
=
idx
;
}
const
body
=
root
.
querySelector
(
'
.ant-table-container > .ant-table-body table tbody.ant-table-tbody
'
)
??
root
.
querySelector
(
'
.ant-table-content > table > tbody.ant-table-tbody
'
)
??
root
.
querySelector
(
'
tbody.ant-table-tbody
'
);
const
rows
=
Array
.
from
(
body
?.
querySelectorAll
(
'
:scope > tr.ant-table-row
'
)
??
[]).
filter
(
(
tr
)
=>
!
tr
.
classList
.
contains
(
'
ant-table-measure-row
'
)
&&
!
tr
.
classList
.
contains
(
'
ant-table-expanded-row
'
),
);
const
colCount
=
headers
.
length
;
const
carryValue
:
(
string
|
null
)[]
=
Array
.
from
({
length
:
colCount
},
()
=>
null
);
const
carryLeft
:
number
[]
=
Array
.
from
({
length
:
colCount
},
()
=>
0
);
const
sums
:
Record
<
string
,
number
>
=
{};
for
(
const
def
of
metricDefs
)
sums
[
def
.
key
]
=
0
;
for
(
const
tr
of
rows
)
{
const
tds
=
Array
.
from
(
tr
.
querySelectorAll
(
'
:scope > td
'
));
let
tdIdx
=
0
;
for
(
let
c
=
0
;
c
<
colCount
;
c
+=
1
)
{
if
(
carryLeft
[
c
]
!
>
0
)
{
carryLeft
[
c
]
!
-=
1
;
continue
;
}
const
td
=
tds
[
tdIdx
];
tdIdx
+=
1
;
if
(
!
td
)
break
;
const
text
=
(
td
.
textContent
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
rowspan
=
Math
.
max
(
1
,
Number
(
td
.
getAttribute
(
'
rowspan
'
)
||
'
1
'
)
||
1
);
const
colspan
=
Math
.
max
(
1
,
Number
(
td
.
getAttribute
(
'
colspan
'
)
||
'
1
'
)
||
1
);
for
(
const
def
of
metricDefs
)
{
if
(
colIndexByKey
[
def
.
key
]
!==
c
)
continue
;
const
parsed
=
parseNum
(
text
);
if
(
!
Number
.
isFinite
(
parsed
))
continue
;
sums
[
def
.
key
]
=
(
sums
[
def
.
key
]
??
0
)
+
(
def
.
abs
?
Math
.
abs
(
parsed
)
:
parsed
);
}
if
(
rowspan
>
1
)
{
carryValue
[
c
]
=
text
;
carryLeft
[
c
]
=
rowspan
-
1
;
}
else
{
carryValue
[
c
]
=
null
;
carryLeft
[
c
]
=
0
;
}
for
(
let
extra
=
1
;
extra
<
colspan
&&
c
+
extra
<
colCount
;
extra
+=
1
)
{
if
(
rowspan
>
1
)
{
carryValue
[
c
+
extra
]
=
text
;
carryLeft
[
c
+
extra
]
=
rowspan
-
1
;
}
}
c
+=
colspan
-
1
;
}
}
return
sums
;
},
defs
);
}
/** 固定列/翻页后 innerText 为空时,从 DOM 合并文本回填 product* / material*。 */
/** 固定列/翻页后 innerText 为空时,从 DOM 合并文本回填 product* / material*。 */
protected
async
enrichSampleFromDomText
(
sample
:
SampleRow
,
wrapper
:
Locator
):
Promise
<
SampleRow
>
{
protected
async
enrichSampleFromDomText
(
sample
:
SampleRow
,
wrapper
:
Locator
):
Promise
<
SampleRow
>
{
const
out
:
SampleRow
=
{
...
sample
};
const
out
:
SampleRow
=
{
...
sample
};
...
@@ -1220,6 +1366,10 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1220,6 +1366,10 @@ export class CenterReportSharedPage extends BasePage {
}
}
async
expectTableHeaders
():
Promise
<
void
>
{
async
expectTableHeaders
():
Promise
<
void
>
{
if
(
this
.
data
.
listRequiresExplicitSearch
)
{
await
this
.
ensureExplicitSearchForList
({
waitRows
:
false
});
await
this
.
waitForTableReady
({
health
:
'
ui
'
}).
catch
(()
=>
undefined
);
}
const
headers
=
await
this
.
getTableHeaderTexts
();
const
headers
=
await
this
.
getTableHeaderTexts
();
const
required
=
[...
this
.
data
.
coreTableHeaders
,
...(
this
.
data
.
extraTableHeaders
??
[])];
const
required
=
[...
this
.
data
.
coreTableHeaders
,
...(
this
.
data
.
extraTableHeaders
??
[])];
const
filterOnly
=
/日期|时间|月份|周期|分类方式|院区|科室|耗材类型|分类名称|物资类型/
;
const
filterOnly
=
/日期|时间|月份|周期|分类方式|院区|科室|耗材类型|分类名称|物资类型/
;
...
@@ -1251,6 +1401,8 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1251,6 +1401,8 @@ export class CenterReportSharedPage extends BasePage {
/** 横向拖拉滚动列表,并校验表头/表体列无变形错位或异常间隙。 */
/** 横向拖拉滚动列表,并校验表头/表体列无变形错位或异常间隙。 */
async
expectTableHorizontalScrollAndLayout
():
Promise
<
void
>
{
async
expectTableHorizontalScrollAndLayout
():
Promise
<
void
>
{
await
this
.
ensureExplicitSearchForList
({
waitRows
:
false
});
await
this
.
waitForTableReady
({
health
:
'
ui
'
}).
catch
(()
=>
undefined
);
let
headers
=
await
this
.
getTableHeaderTexts
();
let
headers
=
await
this
.
getTableHeaderTexts
();
if
(
headers
.
length
===
0
)
{
if
(
headers
.
length
===
0
)
{
headers
=
(
await
this
.
getSortTableHeaderCellTexts
())
headers
=
(
await
this
.
getSortTableHeaderCellTexts
())
...
@@ -1389,7 +1541,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1389,7 +1541,7 @@ export class CenterReportSharedPage extends BasePage {
return
true
;
return
true
;
}
}
/** 空列表反例:优先走 data.mockFieldLabel 配置;无配置则写
产品
名称。 */
/** 空列表反例:优先走 data.mockFieldLabel 配置;无配置则写
物料
名称。 */
async
searchWithMockFromConfig
():
Promise
<
void
>
{
async
searchWithMockFromConfig
():
Promise
<
void
>
{
await
executeSearchWithMockFromConfig
({
await
executeSearchWithMockFromConfig
({
data
:
this
.
data
,
data
:
this
.
data
,
...
@@ -1523,6 +1675,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1523,6 +1675,7 @@ export class CenterReportSharedPage extends BasePage {
}
}
async
expectPaginationTotal
():
Promise
<
void
>
{
async
expectPaginationTotal
():
Promise
<
void
>
{
await
this
.
ensureExplicitSearchForList
({
waitRows
:
false
});
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
if
((
await
this
.
locators
.
paginationTotal
.
count
())
===
0
)
{
if
((
await
this
.
locators
.
paginationTotal
.
count
())
===
0
)
{
const
rows
=
await
this
.
getTableRowCount
();
const
rows
=
await
this
.
getTableRowCount
();
...
@@ -1548,6 +1701,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1548,6 +1701,7 @@ export class CenterReportSharedPage extends BasePage {
/** 场景1:切换至第2页标准流程(数据不足 skip,否则校验两页首行不同)。 */
/** 场景1:切换至第2页标准流程(数据不足 skip,否则校验两页首行不同)。 */
async
executePage2SwitchTest
(
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
async
executePage2SwitchTest
(
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
return
this
.
withPaginationTableIndex
(
tableWrapperIndex
,
async
()
=>
{
return
this
.
withPaginationTableIndex
(
tableWrapperIndex
,
async
()
=>
{
await
this
.
ensureExplicitSearchForList
({
waitRows
:
false
});
await
this
.
skipIfInsufficientForPage2
();
await
this
.
skipIfInsufficientForPage2
();
const
first
=
await
this
.
getFirstRowText
();
const
first
=
await
this
.
getFirstRowText
();
await
this
.
goToPage
(
2
);
await
this
.
goToPage
(
2
);
...
@@ -1617,6 +1771,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1617,6 +1771,7 @@ export class CenterReportSharedPage extends BasePage {
options
?:
{
requireMoreThanOnePage
?:
boolean
;
tableWrapperIndex
?:
number
},
options
?:
{
requireMoreThanOnePage
?:
boolean
;
tableWrapperIndex
?:
number
},
):
Promise
<
void
>
{
):
Promise
<
void
>
{
return
this
.
withPaginationTableIndex
(
options
?.
tableWrapperIndex
,
async
()
=>
{
return
this
.
withPaginationTableIndex
(
options
?.
tableWrapperIndex
,
async
()
=>
{
await
this
.
ensureExplicitSearchForList
({
waitRows
:
false
});
await
this
.
skipIfInsufficientForPageSizeChange
(
options
);
await
this
.
skipIfInsufficientForPageSizeChange
(
options
);
await
this
.
changePageSize
(
fromLabel
,
toLabel
);
await
this
.
changePageSize
(
fromLabel
,
toLabel
);
await
this
.
expectVisibleRowCountAtMost
(
maxRows
);
await
this
.
expectVisibleRowCountAtMost
(
maxRows
);
...
@@ -1842,6 +1997,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1842,6 +1997,7 @@ export class CenterReportSharedPage extends BasePage {
/** 工具栏「打 印」:触发打印流程并校验无报错。 */
/** 工具栏「打 印」:触发打印流程并校验无报错。 */
async
triggerReportPrint
():
Promise
<
void
>
{
async
triggerReportPrint
():
Promise
<
void
>
{
await
this
.
ensureExplicitSearchForList
({
waitRows
:
false
});
const
printBtn
=
this
.
toolbarPrintButton
();
const
printBtn
=
this
.
toolbarPrintButton
();
await
expect
(
printBtn
,
'
工具栏应有打印按钮
'
).
toBeVisible
({
timeout
:
10
_000
});
await
expect
(
printBtn
,
'
工具栏应有打印按钮
'
).
toBeVisible
({
timeout
:
10
_000
});
if
(
this
.
data
.
printRequiresDetailSelection
)
{
if
(
this
.
data
.
printRequiresDetailSelection
)
{
...
@@ -1864,6 +2020,21 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1864,6 +2020,21 @@ export class CenterReportSharedPage extends BasePage {
await
this
.
click
(
tab
,
`切换页签「
${
tabLabel
}
」`
);
await
this
.
click
(
tab
,
`切换页签「
${
tabLabel
}
」`
);
}
}
/** 取消主表当前页已勾选的行,避免底部展示「选中合计」影响列表合计勾稽。 */
async
uncheckAllMainTableRows
():
Promise
<
void
>
{
const
rows
=
this
.
locators
.
dataRows
;
const
count
=
await
rows
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
++
)
{
const
row
=
rows
.
nth
(
i
);
const
checkbox
=
row
.
locator
(
'
.ant-checkbox-wrapper
'
).
first
();
if
((
await
checkbox
.
count
())
===
0
)
continue
;
const
input
=
checkbox
.
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
await
input
.
isChecked
())
{
await
this
.
click
(
checkbox
,
`取消勾选主表第
${
i
+
1
}
行`
);
}
}
}
/** 勾选主表指定行(用于勾选打印场景)。 */
/** 勾选主表指定行(用于勾选打印场景)。 */
async
checkMainTableRow
(
rowIndex
=
0
):
Promise
<
void
>
{
async
checkMainTableRow
(
rowIndex
=
0
):
Promise
<
void
>
{
const
row
=
this
.
locators
.
dataRows
.
nth
(
rowIndex
);
const
row
=
this
.
locators
.
dataRows
.
nth
(
rowIndex
);
...
@@ -1955,7 +2126,8 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -1955,7 +2126,8 @@ export class CenterReportSharedPage extends BasePage {
/** 快捷区日期/月份区间:从当前面板可见可选项中随机选一格(起止同值)。 */
/** 快捷区日期/月份区间:从当前面板可见可选项中随机选一格(起止同值)。 */
async
fillQuickDateRangeRandom
(
label
:
string
):
Promise
<
void
>
{
async
fillQuickDateRangeRandom
(
label
:
string
):
Promise
<
void
>
{
const
item
=
this
.
locators
.
quickFormItem
(
label
);
const
{
label
:
resolvedLabel
,
item
}
=
await
this
.
resolveQuickFormItem
(
label
);
label
=
resolvedLabel
;
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
if
((
await
picker
.
count
())
===
0
)
{
if
((
await
picker
.
count
())
===
0
)
{
return
;
return
;
...
@@ -2771,6 +2943,9 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -2771,6 +2943,9 @@ export class CenterReportSharedPage extends BasePage {
private
quickFormLabelCandidates
(
label
:
string
):
string
[]
{
private
quickFormLabelCandidates
(
label
:
string
):
string
[]
{
const
out
=
new
Set
<
string
>
([
label
]);
const
out
=
new
Set
<
string
>
([
label
]);
for
(
const
token
of
columnHeaderMatchTokens
(
label
))
{
out
.
add
(
token
);
}
if
(
label
===
'
物资分类
'
)
{
if
(
label
===
'
物资分类
'
)
{
out
.
add
(
'
耗材分类
'
);
out
.
add
(
'
耗材分类
'
);
}
}
...
@@ -2784,6 +2959,17 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -2784,6 +2959,17 @@ export class CenterReportSharedPage extends BasePage {
return
[...
out
];
return
[...
out
];
}
}
protected
async
resolveQuickFormItem
(
label
:
string
):
Promise
<
{
label
:
string
;
item
:
Locator
}
>
{
await
this
.
ensureQuickFilterExpanded
().
catch
(()
=>
undefined
);
for
(
const
cand
of
this
.
quickFormLabelCandidates
(
label
))
{
const
item
=
this
.
locators
.
quickFormItem
(
cand
);
if
((
await
item
.
count
())
>
0
)
{
return
{
label
:
cand
,
item
};
}
}
return
{
label
,
item
:
this
.
locators
.
quickFormItem
(
label
)
};
}
async
selectQuickFormSelect
(
label
:
string
,
optionText
?:
string
):
Promise
<
void
>
{
async
selectQuickFormSelect
(
label
:
string
,
optionText
?:
string
):
Promise
<
void
>
{
let
item
=
this
.
locators
.
quickFormItem
(
label
);
let
item
=
this
.
locators
.
quickFormItem
(
label
);
for
(
const
cand
of
this
.
quickFormLabelCandidates
(
label
))
{
for
(
const
cand
of
this
.
quickFormLabelCandidates
(
label
))
{
...
@@ -2794,6 +2980,13 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -2794,6 +2980,13 @@ export class CenterReportSharedPage extends BasePage {
break
;
break
;
}
}
}
}
const
cascader
=
item
.
locator
(
'
.ant-cascader
'
).
first
();
if
((
await
cascader
.
count
())
>
0
)
{
if
(
optionText
?.
trim
())
{
await
this
.
selectQuickCascader
(
label
,
optionText
);
}
return
;
}
const
select
=
item
.
locator
(
'
.ant-select
'
).
first
();
const
select
=
item
.
locator
(
'
.ant-select
'
).
first
();
if
((
await
select
.
count
())
===
0
)
{
if
((
await
select
.
count
())
===
0
)
{
if
(
optionText
)
await
this
.
fillQuickFormText
(
label
,
optionText
);
if
(
optionText
)
await
this
.
fillQuickFormText
(
label
,
optionText
);
...
@@ -2816,7 +3009,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -2816,7 +3009,7 @@ export class CenterReportSharedPage extends BasePage {
}
}
async
selectQuickCascader
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
async
selectQuickCascader
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
const
item
=
this
.
locators
.
q
uickFormItem
(
label
);
const
{
item
}
=
await
this
.
resolveQ
uickFormItem
(
label
);
if
((
await
item
.
count
())
===
0
)
return
;
if
((
await
item
.
count
())
===
0
)
return
;
await
this
.
drawerControls
.
selectCascaderInFormItem
(
item
,
label
,
value
,
'
快捷区
'
);
await
this
.
drawerControls
.
selectCascaderInFormItem
(
item
,
label
,
value
,
'
快捷区
'
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
...
@@ -2987,14 +3180,23 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -2987,14 +3180,23 @@ export class CenterReportSharedPage extends BasePage {
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
return
;
return
;
}
}
const
productNameLabels
=
new
Set
([
'
物料名称
'
,
'
物料名称
'
,
this
.
data
.
columns
.
productName
,
this
.
data
.
columns
.
materialName
,
].
filter
(
Boolean
)
as
string
[]);
if
(
if
(
(
fieldKey
===
'
productName
'
||
label
===
'
产品名称
'
)
&&
(
fieldKey
===
'
productName
'
||
productNameLabels
.
has
(
label
)
)
&&
(
await
this
.
locators
.
productName
.
count
())
>
0
(
await
this
.
locators
.
productName
.
count
())
>
0
)
{
)
{
const
pn
=
this
.
locators
.
productName
;
const
pn
=
this
.
locators
.
productName
;
const
inSelect
=
await
pn
.
evaluate
((
el
)
=>
!!
el
.
closest
(
'
.ant-select
'
)).
catch
(()
=>
false
);
const
inSelect
=
await
pn
.
evaluate
((
el
)
=>
!!
el
.
closest
(
'
.ant-select
'
)).
catch
(()
=>
false
);
if
(
inSelect
)
{
if
(
inSelect
)
{
await
this
.
selectQuickFormSelect
(
label
===
'
产品名称
'
?
label
:
'
产品名称
'
,
value
);
const
selectLabel
=
productNameLabels
.
has
(
label
)
?
label
:
(
this
.
data
.
columns
.
productName
??
'
物料名称
'
);
await
this
.
selectQuickFormSelect
(
selectLabel
,
value
);
return
;
return
;
}
}
if
(
await
pn
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
pn
.
isVisible
().
catch
(()
=>
false
))
{
...
@@ -3024,7 +3226,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3024,7 +3226,7 @@ export class CenterReportSharedPage extends BasePage {
/** 读取快捷区日期范围控件当前展示文本(用于重置前后对比)。 */
/** 读取快捷区日期范围控件当前展示文本(用于重置前后对比)。 */
async
getQuickDateRangeDisplay
(
label
:
string
):
Promise
<
string
>
{
async
getQuickDateRangeDisplay
(
label
:
string
):
Promise
<
string
>
{
const
item
=
this
.
locators
.
q
uickFormItem
(
label
);
const
{
item
}
=
await
this
.
resolveQ
uickFormItem
(
label
);
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
if
((
await
picker
.
count
())
===
0
)
return
''
;
if
((
await
picker
.
count
())
===
0
)
return
''
;
const
inputs
=
picker
.
locator
(
'
input
'
);
const
inputs
=
picker
.
locator
(
'
input
'
);
...
@@ -3042,6 +3244,30 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3042,6 +3244,30 @@ export class CenterReportSharedPage extends BasePage {
return
(
await
picker
.
innerText
()).
replace
(
/
\s
+/g
,
'
'
).
trim
();
return
(
await
picker
.
innerText
()).
replace
(
/
\s
+/g
,
'
'
).
trim
();
}
}
protected
async
pickerExpectsDateTime
(
picker
:
Locator
):
Promise
<
boolean
>
{
const
cls
=
(
await
picker
.
getAttribute
(
'
class
'
).
catch
(()
=>
''
))
??
''
;
if
(
cls
.
includes
(
'
ant-picker-show-time
'
))
return
true
;
const
input
=
picker
.
locator
(
'
input
'
).
first
();
if
((
await
input
.
count
())
===
0
)
return
false
;
const
sample
=
((
await
input
.
inputValue
().
catch
(()
=>
''
))
||
''
).
trim
();
return
/
\d{1,2}
:
\d{2}
/
.
test
(
sample
);
}
protected
formatQuickDateTimeValue
(
dateOnly
:
string
,
edge
:
'
start
'
|
'
end
'
):
string
{
const
day
=
dateOnly
.
replace
(
/
\/
/g
,
'
-
'
).
slice
(
0
,
10
);
return
edge
===
'
start
'
?
`
${
day
}
00:00:00`
:
`
${
day
}
23:59:59`
;
}
/** 带时间的 RangePicker 须点「确定」;Esc 会丢弃面板点选结果。 */
protected
async
confirmVisibleDatePicker
(
panel
:
Locator
):
Promise
<
void
>
{
const
ok
=
panel
.
locator
(
'
.ant-picker-ok
'
).
filter
({
visible
:
true
}).
first
();
if
((
await
ok
.
count
())
>
0
)
{
await
ok
.
click
();
return
;
}
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
}
/** 优先通过 input 直接键入日期/月份,避免面板翻页失败。 */
/** 优先通过 input 直接键入日期/月份,避免面板翻页失败。 */
protected
async
tryFillQuickDateByTyping
(
protected
async
tryFillQuickDateByTyping
(
picker
:
Locator
,
picker
:
Locator
,
...
@@ -3057,12 +3283,19 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3057,12 +3283,19 @@ export class CenterReportSharedPage extends BasePage {
startParsed
.
mode
===
'
month
'
||
startParsed
.
mode
===
'
month
'
||
endParsed
.
mode
===
'
month
'
||
endParsed
.
mode
===
'
month
'
||
/月份/
.
test
(
label
);
/月份/
.
test
(
label
);
const
withTime
=
!
useMonth
&&
(
await
this
.
pickerExpectsDateTime
(
picker
));
const
startDay
=
startParsed
.
value
.
replace
(
/
\/
/g
,
'
-
'
);
const
endDay
=
(
endParsed
.
value
||
startParsed
.
value
).
replace
(
/
\/
/g
,
'
-
'
);
const
startValue
=
useMonth
const
startValue
=
useMonth
?
startParsed
.
value
.
slice
(
0
,
7
)
?
startParsed
.
value
.
slice
(
0
,
7
)
:
startParsed
.
value
.
replace
(
/
\/
/g
,
'
-
'
);
:
withTime
?
this
.
formatQuickDateTimeValue
(
startDay
,
'
start
'
)
:
startDay
;
const
endValue
=
useMonth
const
endValue
=
useMonth
?
(
endParsed
.
value
||
startParsed
.
value
).
slice
(
0
,
7
)
?
endDay
.
slice
(
0
,
7
)
:
endParsed
.
value
.
replace
(
/
\/
/g
,
'
-
'
);
:
withTime
?
this
.
formatQuickDateTimeValue
(
endDay
,
'
end
'
)
:
endDay
;
const
inputs
=
picker
.
locator
(
'
input
'
);
const
inputs
=
picker
.
locator
(
'
input
'
);
const
count
=
await
inputs
.
count
();
const
count
=
await
inputs
.
count
();
if
(
count
===
0
)
return
false
;
if
(
count
===
0
)
return
false
;
...
@@ -3082,6 +3315,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3082,6 +3315,7 @@ export class CenterReportSharedPage extends BasePage {
if
(
isRange
&&
count
>=
2
)
{
if
(
isRange
&&
count
>=
2
)
{
await
typeInto
(
inputs
.
nth
(
0
),
startValue
);
await
typeInto
(
inputs
.
nth
(
0
),
startValue
);
await
inputs
.
nth
(
0
).
press
(
'
Enter
'
);
await
typeInto
(
inputs
.
nth
(
1
),
endValue
);
await
typeInto
(
inputs
.
nth
(
1
),
endValue
);
await
inputs
.
nth
(
1
).
press
(
'
Enter
'
);
await
inputs
.
nth
(
1
).
press
(
'
Enter
'
);
}
else
{
}
else
{
...
@@ -3103,6 +3337,106 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3103,6 +3337,106 @@ export class CenterReportSharedPage extends BasePage {
return
true
;
return
true
;
}
}
/** Ant Design 日期格 title 可能是 YYYY-MM-DD 或 YYYY-M-D。 */
protected
pickerDateTitleCandidates
(
isoDate
:
string
):
string
[]
{
const
normalized
=
isoDate
.
replace
(
/
\/
/g
,
'
-
'
);
const
m
=
normalized
.
match
(
/^
(\d{4})
-
(\d{2})
-
(\d{2})
$/
);
if
(
!
m
)
return
[
normalized
];
const
[,
y
,
mo
,
d
]
=
m
;
const
loose
=
`
${
y
}
-
${
Number
(
mo
)}
-
${
Number
(
d
)}
`
;
return
loose
===
normalized
?
[
normalized
]
:
[
normalized
,
loose
];
}
protected
async
readDatePanelYearMonth
(
panel
:
Locator
,
):
Promise
<
{
year
:
number
;
month
:
number
}
|
null
>
{
const
datePanel
=
panel
.
locator
(
'
.ant-picker-date-panel
'
).
filter
({
visible
:
true
}).
first
();
const
root
=
(
await
datePanel
.
count
())
>
0
?
datePanel
:
panel
;
const
yearBtn
=
root
.
locator
(
'
.ant-picker-year-btn
'
).
first
();
const
monthBtn
=
root
.
locator
(
'
.ant-picker-month-btn
'
).
first
();
if
((
await
yearBtn
.
count
())
===
0
||
(
await
monthBtn
.
count
())
===
0
)
{
return
null
;
}
const
yearText
=
((
await
yearBtn
.
innerText
())
||
''
).
replace
(
/
\D
/g
,
''
);
const
monthText
=
((
await
monthBtn
.
innerText
())
||
''
).
replace
(
/
[^\d]
/g
,
''
);
const
year
=
Number
(
yearText
.
slice
(
0
,
4
));
const
month
=
Number
(
monthText
.
slice
(
0
,
2
))
||
Number
(
monthText
);
if
(
Number
.
isNaN
(
year
)
||
Number
.
isNaN
(
month
)
||
month
<
1
||
month
>
12
)
{
return
null
;
}
return
{
year
,
month
};
}
/** 日面板仅按月 prev/next 导航,禁止 super 年跳,避免目标月找不到时在两年间来回翻。 */
protected
async
navigateDatePanelToMonth
(
panel
:
Locator
,
targetYear
:
number
,
targetMonth
:
number
,
):
Promise
<
void
>
{
const
targetIdx
=
targetYear
*
12
+
targetMonth
;
let
lastKey
=
''
;
for
(
let
i
=
0
;
i
<
36
;
i
+=
1
)
{
const
view
=
await
this
.
readDatePanelYearMonth
(
panel
);
if
(
!
view
)
{
throw
new
Error
(
`日期面板无法读取当前年月(目标
${
targetYear
}
-
${
String
(
targetMonth
).
padStart
(
2
,
'
0
'
)}
)`
);
}
if
(
view
.
year
===
targetYear
&&
view
.
month
===
targetMonth
)
{
return
;
}
const
key
=
`
${
view
.
year
}
-
${
view
.
month
}
`
;
if
(
key
===
lastKey
)
{
throw
new
Error
(
`日期面板月份导航停滞于
${
key
}
,无法到达
${
targetYear
}
-
${
String
(
targetMonth
).
padStart
(
2
,
'
0
'
)}
`
,
);
}
lastKey
=
key
;
const
currentIdx
=
view
.
year
*
12
+
view
.
month
;
const
moved
=
currentIdx
<
targetIdx
?
await
this
.
clickVisiblePickerNav
(
panel
,
'
.ant-picker-header-next-btn
'
)
:
await
this
.
clickVisiblePickerNav
(
panel
,
'
.ant-picker-header-prev-btn
'
);
if
(
!
moved
)
{
break
;
}
}
const
finalView
=
await
this
.
readDatePanelYearMonth
(
panel
);
if
(
finalView
?.
year
===
targetYear
&&
finalView
.
month
===
targetMonth
)
{
return
;
}
throw
new
Error
(
`日期面板未导航到
${
targetYear
}
-
${
String
(
targetMonth
).
padStart
(
2
,
'
0
'
)}
(当前
${
finalView
?.
year
??
'
?
'
}
-
${
finalView
?.
month
??
'
?
'
}
)`
,
);
}
protected
async
clickDayInDatePanel
(
panel
:
Locator
,
normalized
:
string
,
dayNum
:
number
,
):
Promise
<
boolean
>
{
const
datePanel
=
panel
.
locator
(
'
.ant-picker-date-panel
'
).
filter
({
visible
:
true
}).
first
();
const
root
=
(
await
datePanel
.
count
())
>
0
?
datePanel
:
panel
;
for
(
const
title
of
this
.
pickerDateTitleCandidates
(
normalized
))
{
const
byTitle
=
root
.
locator
(
`.ant-picker-cell[title="
${
title
}
"]:not(.ant-picker-cell-disabled)`
)
.
filter
({
visible
:
true
})
.
first
();
if
((
await
byTitle
.
count
())
>
0
)
{
await
byTitle
.
click
();
return
true
;
}
}
const
dayCell
=
root
.
locator
(
'
.ant-picker-cell-in-view:not(.ant-picker-cell-disabled)
'
)
.
filter
({
hasText
:
new
RegExp
(
`^
${
dayNum
}
$`
)
})
.
filter
({
visible
:
true
})
.
first
();
if
((
await
dayCell
.
count
())
>
0
)
{
await
dayCell
.
click
();
return
true
;
}
return
false
;
}
/**
/**
* mock 空列表回退:打开面板,点选当前可见的最早可选格(月份或日期),区间则起止同值。
* mock 空列表回退:打开面板,点选当前可见的最早可选格(月份或日期),区间则起止同值。
* @returns 写入后的起始展示 token(YYYY-MM 或 YYYY-MM-DD)
* @returns 写入后的起始展示 token(YYYY-MM 或 YYYY-MM-DD)
...
@@ -3111,7 +3445,8 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3111,7 +3445,8 @@ export class CenterReportSharedPage extends BasePage {
label
:
string
,
label
:
string
,
preferMonth
:
boolean
,
preferMonth
:
boolean
,
):
Promise
<
string
>
{
):
Promise
<
string
>
{
const
item
=
this
.
locators
.
quickFormItem
(
label
);
const
{
label
:
resolvedLabel
,
item
}
=
await
this
.
resolveQuickFormItem
(
label
);
label
=
resolvedLabel
;
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
if
((
await
picker
.
count
())
===
0
)
{
if
((
await
picker
.
count
())
===
0
)
{
throw
new
Error
(
`快捷区未找到日期控件:
${
label
}
`
);
throw
new
Error
(
`快捷区未找到日期控件:
${
label
}
`
);
...
@@ -3192,7 +3527,8 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3192,7 +3527,8 @@ export class CenterReportSharedPage extends BasePage {
/** 快捷区 ant-picker-range:填入起止日期或月份(结算月份等为 YYYY-MM)。 */
/** 快捷区 ant-picker-range:填入起止日期或月份(结算月份等为 YYYY-MM)。 */
async
fillQuickDateRange
(
label
:
string
,
start
:
string
,
end
:
string
):
Promise
<
void
>
{
async
fillQuickDateRange
(
label
:
string
,
start
:
string
,
end
:
string
):
Promise
<
void
>
{
const
item
=
this
.
locators
.
quickFormItem
(
label
);
const
{
label
:
resolvedLabel
,
item
}
=
await
this
.
resolveQuickFormItem
(
label
);
label
=
resolvedLabel
;
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
const
picker
=
item
.
locator
(
'
.ant-picker
'
).
first
();
if
((
await
picker
.
count
())
===
0
)
{
if
((
await
picker
.
count
())
===
0
)
{
throw
new
Error
(
`快捷区未找到日期控件:
${
label
}
`
);
throw
new
Error
(
`快捷区未找到日期控件:
${
label
}
`
);
...
@@ -3231,7 +3567,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3231,7 +3567,7 @@ export class CenterReportSharedPage extends BasePage {
}
else
{
}
else
{
await
this
.
pickDateInPanel
(
panel
,
start
);
await
this
.
pickDateInPanel
(
panel
,
start
);
}
}
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
confirmVisibleDatePicker
(
panel
);
// 面板点选后校验是否写入成功
// 面板点选后校验是否写入成功
const
startToken
=
useMonth
const
startToken
=
useMonth
?
startParsed
.
value
.
slice
(
0
,
7
)
?
startParsed
.
value
.
slice
(
0
,
7
)
...
@@ -3332,73 +3668,22 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3332,73 +3668,22 @@ export class CenterReportSharedPage extends BasePage {
const
targetMonth
=
Number
(
normalized
.
slice
(
5
,
7
));
const
targetMonth
=
Number
(
normalized
.
slice
(
5
,
7
));
const
dayNum
=
Number
(
normalized
.
slice
(
8
,
10
));
const
dayNum
=
Number
(
normalized
.
slice
(
8
,
10
));
for
(
let
i
=
0
;
i
<
48
;
i
+=
1
)
{
for
(
const
title
of
this
.
pickerDateTitleCandidates
(
normalized
))
{
let
cell
=
panel
.
locator
(
`td[title="
${
normalized
}
"]`
);
const
quick
=
panel
if
((
await
cell
.
count
())
>
0
)
{
.
locator
(
`.ant-picker-cell[title="
${
title
}
"]:not(.ant-picker-cell-disabled)`
)
const
visibleCell
=
cell
.
filter
({
visible
:
true
});
.
filter
({
visible
:
true
})
const
target
=
(
await
visibleCell
.
count
())
>
0
?
visibleCell
.
last
()
:
cell
.
last
();
.
first
();
await
target
.
click
();
if
((
await
quick
.
count
())
>
0
)
{
await
quick
.
click
();
return
;
return
;
}
}
const
yearBtn
=
panel
.
locator
(
'
.ant-picker-year-btn
'
).
first
();
if
((
await
yearBtn
.
count
())
===
0
)
{
await
panel
.
locator
(
'
.ant-picker-header-prev-btn
'
).
first
().
click
();
continue
;
}
const
yearText
=
(
await
yearBtn
.
innerText
()).
replace
(
/
\D
/g
,
''
);
const
monthBtn
=
panel
.
locator
(
'
.ant-picker-month-btn
'
).
first
();
const
monthText
=
(
await
monthBtn
.
innerText
()).
replace
(
/
[^\d]
/g
,
''
);
const
currentYear
=
Number
(
yearText
.
slice
(
0
,
4
));
const
currentMonth
=
Number
(
monthText
.
slice
(
0
,
2
))
||
Number
(
monthText
);
if
(
!
Number
.
isNaN
(
currentYear
)
&&
!
Number
.
isNaN
(
currentMonth
)
&&
currentYear
===
targetYear
&&
currentMonth
===
targetMonth
)
{
const
dayCell
=
panel
.
locator
(
'
.ant-picker-cell-in-view:not(.ant-picker-cell-disabled)
'
)
.
filter
({
hasText
:
new
RegExp
(
`^
${
dayNum
}
$`
)
});
if
((
await
dayCell
.
count
())
>
0
)
{
await
dayCell
.
first
().
click
();
return
;
}
}
if
(
Number
.
isNaN
(
currentYear
))
break
;
const
before
=
`
${
currentYear
}
-
${
currentMonth
}
`
;
const
afterTarget
=
currentYear
>
targetYear
||
(
currentYear
===
targetYear
&&
currentMonth
>
targetMonth
);
if
(
afterTarget
)
{
if
(
await
this
.
clickVisiblePickerNav
(
panel
,
'
.ant-picker-header-super-prev-btn
'
))
{
continue
;
}
if
(
await
this
.
clickVisiblePickerNav
(
panel
,
'
.ant-picker-header-prev-btn
'
))
{
continue
;
}
break
;
}
if
(
await
this
.
clickVisiblePickerNav
(
panel
,
'
.ant-picker-header-super-next-btn
'
))
{
continue
;
}
if
(
await
this
.
clickVisiblePickerNav
(
panel
,
'
.ant-picker-header-next-btn
'
))
{
continue
;
}
break
;
const
yearText2
=
(
await
yearBtn
.
innerText
()).
replace
(
/
\D
/g
,
''
);
const
monthText2
=
(
await
monthBtn
.
innerText
()).
replace
(
/
[^\d]
/g
,
''
);
const
currentYear2
=
Number
(
yearText2
.
slice
(
0
,
4
));
const
currentMonth2
=
Number
(
monthText2
.
slice
(
0
,
2
))
||
Number
(
monthText2
);
if
(
`
${
currentYear2
}
-
${
currentMonth2
}
`
===
before
)
break
;
}
}
const
today
=
panel
.
locator
(
'
.ant-picker-cell-today
'
).
first
();
await
this
.
navigateDatePanelToMonth
(
panel
,
targetYear
,
targetMonth
);
if
((
await
today
.
count
())
>
0
)
{
if
(
await
this
.
clickDayInDatePanel
(
panel
,
normalized
,
dayNum
))
{
await
today
.
click
();
return
;
return
;
}
}
throw
new
Error
(
`日期面板中未找到:
${
normalized
}
`
);
throw
new
Error
(
`日期面板中未找到
可选日期
:
${
normalized
}
(已到
${
targetYear
}
-
${
String
(
targetMonth
).
padStart
(
2
,
'
0
'
)}
)
`
);
}
}
async
getDrawerSelectValue
(
label
:
string
):
Promise
<
string
>
{
async
getDrawerSelectValue
(
label
:
string
):
Promise
<
string
>
{
...
@@ -3559,7 +3844,7 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -3559,7 +3844,7 @@ export class CenterReportSharedPage extends BasePage {
return
trigger
;
return
trigger
;
}
}
/** 面板仍可见时强制关闭(Escape / 点
产品
名称 / 再点齿轮;禁止点查询)。 */
/** 面板仍可见时强制关闭(Escape / 点
物料
名称 / 再点齿轮;禁止点查询)。 */
protected
async
forceDismissColumnSettingsPanel
(
tableWrapperIndex
:
number
):
Promise
<
void
>
{
protected
async
forceDismissColumnSettingsPanel
(
tableWrapperIndex
:
number
):
Promise
<
void
>
{
const
panel
=
this
.
locators
.
columnSettingsPanel
();
const
panel
=
this
.
locators
.
columnSettingsPanel
();
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
{
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
{
...
...
pages/spd/center-report-sheet.page.ts
View file @
6c31a476
...
@@ -39,7 +39,7 @@ export class CenterReportSheetPage extends CenterReportSharedPage {
...
@@ -39,7 +39,7 @@ export class CenterReportSheetPage extends CenterReportSharedPage {
await
this
.
fillOperationDate
(
sample
.
operationDate
);
await
this
.
fillOperationDate
(
sample
.
operationDate
);
}
}
/** 填入与抽屉重叠的快捷区字段(
产品
名称、记账日期)。 */
/** 填入与抽屉重叠的快捷区字段(
物料
名称、记账日期)。 */
async
fillQuickFiltersForDrawerSync
(
sample
:
SampleRow
):
Promise
<
void
>
{
async
fillQuickFiltersForDrawerSync
(
sample
:
SampleRow
):
Promise
<
void
>
{
await
this
.
applyQuickFields
(
sample
,
drawerSyncQuickFields
,
{
onlyIfPresent
:
true
});
await
this
.
applyQuickFields
(
sample
,
drawerSyncQuickFields
,
{
onlyIfPresent
:
true
});
}
}
...
...
pages/spd/cost-dept-classify-charge.page.ts
View file @
6c31a476
import
{
type
Page
}
from
'
@playwright/test
'
;
import
type
{
Page
}
from
'
@playwright/test
'
;
import
{
waitAntSpinGone
}
from
'
../../core/smart-wait
'
;
import
{
costDeptClassifyChargeData
}
from
'
../../data/cost-dept-classify-charge.data
'
;
import
{
costDeptClassifyChargeData
}
from
'
../../data/cost-dept-classify-charge.data
'
;
import
{
expectColumnSortByDom
}
from
'
./helpers/column-sort.helper
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
/** 科室成本分类汇总(计费科室):透视表
首行表头含可排序「科室名称」
。 */
/** 科室成本分类汇总(计费科室):透视表
,无 ProTable 列设置
。 */
export
class
CostDeptClassifyChargePage
extends
ReconCostReportSharedPage
{
export
class
CostDeptClassifyChargePage
extends
ReconCostReportSharedPage
{
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
costDeptClassifyChargeData
);
super
(
page
,
costDeptClassifyChargeData
);
}
}
async
isColumnSortable
(
columnName
:
string
,
tableWrapperIndex
?:
number
):
Promise
<
boolean
>
{
if
(
columnName
!==
'
科室名称
'
)
{
return
super
.
isColumnSortable
(
columnName
,
tableWrapperIndex
);
}
const
sorter
=
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
)
.
first
()
.
locator
(
'
.ant-table-thead tr
'
)
.
first
()
.
locator
(
'
th
'
)
.
filter
({
hasText
:
columnName
})
.
first
()
.
locator
(
'
.ant-table-column-sorter
'
);
return
(
await
sorter
.
count
())
>
0
&&
(
await
sorter
.
isVisible
().
catch
(()
=>
false
));
}
/** 透视表排序列在 thead 首行,与其它报表一样按 DOM 行序校验。 */
async
expectColumnSortToggle
(
columnName
:
string
,
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
if
(
columnName
!==
'
科室名称
'
)
{
return
super
.
expectColumnSortToggle
(
columnName
,
tableWrapperIndex
);
}
const
wrapper
=
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
();
await
expectColumnSortByDom
({
wrapper
,
columnName
,
waitAfterClick
:
()
=>
waitAntSpinGone
(
wrapper
),
});
}
}
}
export
const
COSTDEPTCLASSIFYCHARGE_PATH
=
costDeptClassifyChargeData
.
reportPath
;
export
const
COSTDEPTCLASSIFYCHARGE_PATH
=
costDeptClassifyChargeData
.
reportPath
;
pages/spd/cost-fin-in.page.ts
View file @
6c31a476
...
@@ -3,6 +3,7 @@ import type { Page } from '@playwright/test';
...
@@ -3,6 +3,7 @@ import type { Page } from '@playwright/test';
import
{
costFinInData
}
from
'
../../data/cost-fin-in.data
'
;
import
{
costFinInData
}
from
'
../../data/cost-fin-in.data
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
/** 财务入库单:默认财务月份区间见 costFinInData.defaultQuickDateRange(open/search 统一注入)。 */
export
class
CostFinInPage
extends
ReconCostReportSharedPage
{
export
class
CostFinInPage
extends
ReconCostReportSharedPage
{
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
costFinInData
);
super
(
page
,
costFinInData
);
...
...
pages/spd/flow-inst.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
flowData
}
from
'
../../data/flow.data
'
;
import
{
flowData
}
from
'
../../data/flow.data
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
FlowInstLocators
}
from
'
../../locators/spd/flow-inst.locator
'
;
import
{
FlowInstLocators
}
from
'
../../locators/spd/flow-inst.locator
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
isColumnSettingsTriggerUsable
,
}
from
'
./helpers/column-settings.helper
'
;
import
{
WhFlowSharedPage
}
from
'
./wh-flow-shared.page
'
;
import
{
WhFlowSharedPage
}
from
'
./wh-flow-shared.page
'
;
export
type
FlowInstFirstRowCombo
=
{
orderNo
:
string
;
status
:
string
;
bizType
:
string
;
summary
:
string
;
startTime
:
string
;
};
/** 审批流实例:动作与取数;断言在 tests/spd/flow-inst.spec.ts。 */
/** 审批流实例:动作与取数;断言在 tests/spd/flow-inst.spec.ts。 */
export
class
FlowInstPage
extends
WhFlowSharedPage
{
export
class
FlowInstPage
extends
WhFlowSharedPage
{
override
readonly
locators
:
FlowInstLocators
;
override
readonly
locators
:
FlowInstLocators
;
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
{
super
(
page
,
{
...
@@ -17,6 +33,177 @@ export class FlowInstPage extends WhFlowSharedPage {
...
@@ -17,6 +33,177 @@ export class FlowInstPage extends WhFlowSharedPage {
forbidden
:
[
'
flowSet
'
],
forbidden
:
[
'
flowSet
'
],
});
});
this
.
locators
=
new
FlowInstLocators
(
page
);
this
.
locators
=
new
FlowInstLocators
(
page
);
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
locators
.
orderNo
,
tableClickTarget
:
this
.
tableWrapper
(),
});
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
{
candidates
.
push
(
loc
);
}
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
if
(
await
isColumnSettingsTriggerUsable
(
loc
))
{
trigger
=
loc
;
break
;
}
}
}
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
}
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
expectColumnSettingsPanel
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
await
expect
(
panel
).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
列展示
'
).
first
()).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
字段设置
'
).
first
()).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
重置
'
,
{
exact
:
true
})).
toBeVisible
();
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
flowData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
return
flowData
.
columnSettingsToggleCandidates
[
0
];
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
const
isChecked
=
await
checkbox
.
isChecked
();
if
(
isChecked
!==
visible
)
{
await
row
.
click
();
}
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
{
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
}
else
{
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
{
await
expect
(
checkbox
).
toBeChecked
();
}
else
{
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
}
async
executeColumnSettingsToggleRestoreTest
():
Promise
<
void
>
{
await
executeColumnSettingsToggleRestore
({
pickToggleColumnName
:
()
=>
this
.
pickToggleColumnName
(),
openColumnSettings
:
()
=>
this
.
openColumnSettings
(),
setColumnVisibleInSettings
:
(
name
,
visible
)
=>
this
.
setColumnVisibleInSettings
(
name
,
visible
),
closeColumnSettingsPanel
:
()
=>
this
.
closeColumnSettingsPanel
(),
expectTableHeaderVisible
:
(
name
,
visible
)
=>
this
.
expectTableHeaderVisible
(
name
,
visible
),
expectSettingsCheckboxChecked
:
(
name
,
checked
)
=>
this
.
expectSettingsCheckboxChecked
(
name
,
checked
),
});
}
async
fillInitiateDateRangeForDay
(
dayText
:
string
):
Promise
<
void
>
{
const
day
=
dayText
.
replace
(
/
\s
.*/
,
''
).
slice
(
0
,
10
);
if
(
!
day
||
day
.
length
<
8
)
{
await
this
.
skipWhenDataInsufficient
(
'
首行发起时间为空,无法组合日期筛选
'
);
}
await
this
.
fill
(
this
.
locators
.
startDate
,
day
,
'
发起开始日期
'
);
await
this
.
fill
(
this
.
locators
.
endDate
,
day
,
'
发起截止日期
'
);
await
this
.
waitSettle
(
200
);
}
/** CSV FLOW-INS-009:首行单据编号、审批状态、发起时间、业务类型、摘要组合查询。 */
async
queryFirstRowFiveFieldCombo
():
Promise
<
FlowInstFirstRowCombo
>
{
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法首行组合查询
'
);
}
const
row
=
await
this
.
firstRow
();
const
orderNo
=
(
await
this
.
tryCell
(
row
,
'
单据编号
'
)).
replace
(
/
\s
/g
,
''
);
const
status
=
(
await
this
.
tryCell
(
row
,
'
审批状态
'
)).
replace
(
/
\s
/g
,
''
);
const
bizType
=
(
await
this
.
tryCell
(
row
,
'
业务类型
'
)).
replace
(
/
\s
/g
,
''
);
const
summaryRaw
=
(
await
this
.
tryCell
(
row
,
'
摘要
'
)).
trim
();
const
startTime
=
(
await
this
.
tryCell
(
row
,
'
发起时间
'
)).
trim
();
if
(
!
orderNo
)
await
this
.
skipWhenDataInsufficient
(
'
首行单据编号为空
'
);
if
(
!
status
)
await
this
.
skipWhenDataInsufficient
(
'
首行审批状态为空
'
);
if
(
!
bizType
)
await
this
.
skipWhenDataInsufficient
(
'
首行业务类型为空
'
);
if
(
!
startTime
)
await
this
.
skipWhenDataInsufficient
(
'
首行发起时间为空
'
);
await
this
.
reset
();
await
this
.
fillOrderNo
(
orderNo
);
await
this
.
selectFormOption
(
'
审批状态
'
,
status
);
await
this
.
selectFormOption
(
'
业务类型
'
,
bizType
);
await
this
.
fillInitiateDateRangeForDay
(
startTime
);
if
(
summaryRaw
&&
summaryRaw
!==
'
--
'
&&
summaryRaw
!==
'
-
'
)
{
const
keyword
=
summaryRaw
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
Math
.
min
(
summaryRaw
.
length
,
12
));
await
this
.
fillSummary
(
keyword
);
}
await
this
.
query
();
return
{
orderNo
,
status
,
bizType
,
summary
:
summaryRaw
,
startTime
};
}
}
override
async
waitContentReady
():
Promise
<
void
>
{
override
async
waitContentReady
():
Promise
<
void
>
{
...
...
pages/spd/helpers/column-resize.helper.ts
View file @
6c31a476
...
@@ -112,17 +112,51 @@ export class ColumnResizeControls {
...
@@ -112,17 +112,51 @@ export class ColumnResizeControls {
const
scrollContainers
=
root
.
querySelectorAll
(
const
scrollContainers
=
root
.
querySelectorAll
(
'
.ant-table-body, .ant-table-header .ant-table-content, .ant-table-content
'
,
'
.ant-table-body, .ant-table-header .ant-table-content, .ant-table-content
'
,
);
);
// 只改 scrollLeft。clipped 表头上 scrollIntoView 会在 Chromium 里挂死,拖垮 120s 用例。
// 只改 scrollLeft;扣除 fixed-right(如「操作」列)避免表头/排序钮被遮挡。
const
thRect
=
matchedTh
.
getBoundingClientRect
();
const
measureInsets
=
(
hostRect
:
DOMRect
)
=>
{
for
(
const
container
of
Array
.
from
(
scrollContainers
))
{
let
leftPad
=
48
;
const
host
=
container
as
HTMLElement
;
let
rightPad
=
48
;
if
(
host
.
scrollWidth
<=
host
.
clientWidth
+
4
)
continue
;
const
fixedLeft
=
root
.
querySelector
(
'
.ant-table-fixed-left
'
)
as
HTMLElement
|
null
;
const
hostRect
=
host
.
getBoundingClientRect
();
const
fixedRight
=
root
.
querySelector
(
'
.ant-table-fixed-right
'
)
as
HTMLElement
|
null
;
if
(
thRect
.
left
<
hostRect
.
left
+
48
)
{
if
(
fixedLeft
)
{
host
.
scrollLeft
-=
hostRect
.
left
-
thRect
.
left
+
96
;
const
r
=
fixedLeft
.
getBoundingClientRect
();
}
else
if
(
thRect
.
right
>
hostRect
.
right
-
48
)
{
if
(
r
.
width
>
0
&&
r
.
left
<=
hostRect
.
left
+
4
)
{
host
.
scrollLeft
+=
thRect
.
right
-
hostRect
.
right
+
96
;
leftPad
=
Math
.
max
(
leftPad
,
r
.
width
+
16
);
}
}
}
if
(
fixedRight
)
{
const
r
=
fixedRight
.
getBoundingClientRect
();
if
(
r
.
width
>
0
&&
r
.
right
>=
hostRect
.
right
-
4
)
{
rightPad
=
Math
.
max
(
rightPad
,
r
.
width
+
16
);
}
}
return
{
leftPad
,
rightPad
};
};
const
fixedRight
=
root
.
querySelector
(
'
.ant-table-fixed-right
'
)
as
HTMLElement
|
null
;
const
fixedRightRect
=
fixedRight
?
fixedRight
.
getBoundingClientRect
()
:
null
;
for
(
let
pass
=
0
;
pass
<
3
;
pass
+=
1
)
{
const
thRect
=
matchedTh
.
getBoundingClientRect
();
let
moved
=
false
;
for
(
const
container
of
Array
.
from
(
scrollContainers
))
{
const
host
=
container
as
HTMLElement
;
if
(
host
.
scrollWidth
<=
host
.
clientWidth
+
4
)
continue
;
const
hostRect
=
host
.
getBoundingClientRect
();
const
{
leftPad
,
rightPad
}
=
measureInsets
(
hostRect
);
if
(
thRect
.
left
<
hostRect
.
left
+
leftPad
)
{
host
.
scrollLeft
=
Math
.
max
(
0
,
host
.
scrollLeft
-
(
hostRect
.
left
-
thRect
.
left
+
leftPad
));
moved
=
true
;
}
else
if
(
thRect
.
right
>
hostRect
.
right
-
rightPad
)
{
host
.
scrollLeft
+=
thRect
.
right
-
(
hostRect
.
right
-
rightPad
);
moved
=
true
;
}
if
(
fixedRightRect
&&
fixedRightRect
.
width
>
0
&&
thRect
.
right
>
fixedRightRect
.
left
+
2
)
{
host
.
scrollLeft
+=
thRect
.
right
-
fixedRightRect
.
left
+
32
;
moved
=
true
;
}
}
if
(
!
moved
)
break
;
const
after
=
matchedTh
.
getBoundingClientRect
();
if
(
!
fixedRightRect
||
after
.
right
<=
fixedRightRect
.
left
+
2
)
break
;
}
}
}
}
return
bestIndex
;
return
bestIndex
;
...
...
pages/spd/helpers/column-sort.helper.ts
View file @
6c31a476
...
@@ -126,7 +126,9 @@ export async function clickColumnSortControl(options: {
...
@@ -126,7 +126,9 @@ export async function clickColumnSortControl(options: {
const
{
header
,
sorter
,
up
,
down
,
order
}
=
options
;
const
{
header
,
sorter
,
up
,
down
,
order
}
=
options
;
const
indicator
=
order
===
'
ascend
'
?
up
:
order
===
'
descend
'
?
down
:
null
;
const
indicator
=
order
===
'
ascend
'
?
up
:
order
===
'
descend
'
?
down
:
null
;
if
(
indicator
&&
(
await
indicator
.
count
().
catch
(()
=>
0
))
>
0
)
{
if
(
indicator
&&
(
await
indicator
.
count
().
catch
(()
=>
0
))
>
0
)
{
await
indicator
.
click
({
force
:
true
});
await
indicator
.
evaluate
((
el
)
=>
(
el
as
HTMLElement
).
click
()).
catch
(
async
()
=>
{
await
indicator
.
click
({
force
:
true
});
});
return
;
return
;
}
}
if
((
await
sorter
.
count
().
catch
(()
=>
0
))
>
0
)
{
if
((
await
sorter
.
count
().
catch
(()
=>
0
))
>
0
)
{
...
@@ -633,14 +635,46 @@ async function clickDomColumnSort(
...
@@ -633,14 +635,46 @@ async function clickDomColumnSort(
)
as
HTMLElement
|
undefined
;
)
as
HTMLElement
|
undefined
;
if
(
sample
)
{
if
(
sample
)
{
const
hostRect
=
sample
.
getBoundingClientRect
();
const
hostRect
=
sample
.
getBoundingClientRect
();
let
scrollLeft
=
sample
.
scrollLeft
;
let
leftPad
=
48
;
if
(
thRect
.
right
>
hostRect
.
right
-
24
)
{
let
rightPad
=
48
;
scrollLeft
=
sample
.
scrollLeft
+
(
thRect
.
right
-
hostRect
.
right
)
+
80
;
const
fixedLeft
=
searchRoot
.
querySelector
(
'
.ant-table-fixed-left
'
)
as
HTMLElement
|
null
;
}
else
if
(
thRect
.
left
<
hostRect
.
left
+
24
)
{
const
fixedRight
=
searchRoot
.
querySelector
(
'
.ant-table-fixed-right
'
)
as
HTMLElement
|
null
;
scrollLeft
=
Math
.
max
(
0
,
sample
.
scrollLeft
-
(
hostRect
.
left
-
thRect
.
left
)
-
80
);
if
(
fixedLeft
)
{
const
r
=
fixedLeft
.
getBoundingClientRect
();
if
(
r
.
width
>
0
&&
r
.
left
<=
hostRect
.
left
+
4
)
{
leftPad
=
Math
.
max
(
leftPad
,
r
.
width
+
16
);
}
}
if
(
fixedRight
)
{
const
r
=
fixedRight
.
getBoundingClientRect
();
if
(
r
.
width
>
0
&&
r
.
right
>=
hostRect
.
right
-
4
)
{
rightPad
=
Math
.
max
(
rightPad
,
r
.
width
+
16
);
}
}
}
for
(
const
host
of
Array
.
from
(
hosts
))
{
const
applyScroll
=
(
baseLeft
:
number
)
=>
{
(
host
as
HTMLElement
).
scrollLeft
=
scrollLeft
;
let
scrollLeft
=
baseLeft
;
const
th
=
best
.
getBoundingClientRect
();
if
(
th
.
right
>
hostRect
.
right
-
rightPad
)
{
scrollLeft
=
baseLeft
+
(
th
.
right
-
(
hostRect
.
right
-
rightPad
));
}
else
if
(
th
.
left
<
hostRect
.
left
+
leftPad
)
{
scrollLeft
=
Math
.
max
(
0
,
baseLeft
-
(
hostRect
.
left
-
th
.
left
+
leftPad
));
}
if
(
fixedRight
)
{
const
fr
=
fixedRight
.
getBoundingClientRect
();
if
(
fr
.
width
>
0
&&
th
.
right
>
fr
.
left
+
2
)
{
scrollLeft
=
Math
.
max
(
scrollLeft
,
baseLeft
+
(
th
.
right
-
fr
.
left
+
32
));
}
}
for
(
const
host
of
Array
.
from
(
hosts
))
{
(
host
as
HTMLElement
).
scrollLeft
=
scrollLeft
;
}
return
scrollLeft
;
};
let
scrollLeft
=
applyScroll
(
sample
.
scrollLeft
);
for
(
let
pass
=
0
;
pass
<
2
;
pass
+=
1
)
{
const
th
=
best
.
getBoundingClientRect
();
if
(
!
fixedRight
||
th
.
right
<=
fixedRight
.
getBoundingClientRect
().
left
+
2
)
break
;
scrollLeft
=
applyScroll
(
scrollLeft
);
}
}
}
}
const
order
=
payload
.
order
;
const
order
=
payload
.
order
;
...
...
pages/spd/helpers/drawer-form-controls.helper.ts
View file @
6c31a476
...
@@ -404,7 +404,7 @@ export class DrawerFormControls {
...
@@ -404,7 +404,7 @@ export class DrawerFormControls {
return
;
return
;
}
}
await
this
.
dismissOverlay
();
await
this
.
dismissOverlay
();
return
;
throw
new
Error
(
`快捷/抽屉「
${
label
}
」未能选中级联选项「
${
trimmed
}
」`
)
;
}
}
if
(
leaf
)
{
if
(
leaf
)
{
...
...
pages/spd/helpers/mock-empty-route.helper.ts
View file @
6c31a476
...
@@ -156,30 +156,30 @@ export async function runWithMockEmptyListRoute(
...
@@ -156,30 +156,30 @@ export async function runWithMockEmptyListRoute(
/** mock 反例筛选项标签候选(现网文案差异)。 */
/** mock 反例筛选项标签候选(现网文案差异)。 */
export
function
mockFieldLabelCandidates
(
primary
:
string
):
readonly
string
[]
{
export
function
mockFieldLabelCandidates
(
primary
:
string
):
readonly
string
[]
{
const
ordered
=
new
Set
<
string
>
([
primary
]);
const
ordered
=
new
Set
<
string
>
([
primary
]);
if
(
primary
===
'
物料名称
'
||
primary
===
'
产品
名称
'
)
{
if
(
primary
===
'
物料名称
'
||
primary
===
'
物料
名称
'
)
{
ordered
.
add
(
'
物资名称
'
);
ordered
.
add
(
'
物资名称
'
);
ordered
.
add
(
'
物料名称
'
);
ordered
.
add
(
'
物料名称
'
);
ordered
.
add
(
'
产品
名称
'
);
ordered
.
add
(
'
物料
名称
'
);
ordered
.
add
(
'
产品简称
'
);
ordered
.
add
(
'
产品简称
'
);
ordered
.
add
(
'
耗材名称
'
);
ordered
.
add
(
'
耗材名称
'
);
}
}
if
(
primary
===
'
物料编号
'
||
primary
===
'
物料编码
'
)
{
if
(
primary
===
'
物料编号
'
||
primary
===
'
物料编码
'
)
{
ordered
.
add
(
primary
===
'
物料编码
'
?
'
物料编号
'
:
'
物料编码
'
);
ordered
.
add
(
primary
===
'
物料编码
'
?
'
物料编号
'
:
'
物料编码
'
);
ordered
.
add
(
'
产品
编码
'
);
ordered
.
add
(
'
物料
编码
'
);
}
}
if
(
/供应商/
.
test
(
primary
))
{
if
(
/供应商/
.
test
(
primary
))
{
ordered
.
add
(
'
进院供应商
名称
'
);
ordered
.
add
(
'
销售商(自定义)
名称
'
);
ordered
.
add
(
'
进院供应商
'
);
ordered
.
add
(
'
销售商(自定义)
'
);
ordered
.
add
(
'
供应商名称
'
);
ordered
.
add
(
'
供应商名称
'
);
ordered
.
add
(
'
默认
进院供应
商
'
);
ordered
.
add
(
'
默认
销售
商
'
);
}
}
if
(
primary
===
'
进院供应商
'
)
{
if
(
primary
===
'
销售商(自定义)
'
)
{
ordered
.
add
(
'
进院供应商
名称
'
);
ordered
.
add
(
'
销售商(自定义)
名称
'
);
ordered
.
add
(
'
供应商
'
);
ordered
.
add
(
'
供应商
'
);
ordered
.
add
(
'
供应商名称
'
);
ordered
.
add
(
'
供应商名称
'
);
}
}
if
(
primary
===
'
进院供应商
名称
'
)
{
if
(
primary
===
'
销售商(自定义)
名称
'
)
{
ordered
.
add
(
'
进院供应商
'
);
ordered
.
add
(
'
销售商(自定义)
'
);
ordered
.
add
(
'
供应商
'
);
ordered
.
add
(
'
供应商
'
);
}
}
if
(
/供应商/
.
test
(
primary
))
{
if
(
/供应商/
.
test
(
primary
))
{
...
...
Prev
1
2
3
4
5
6
7
8
9
…
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