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
ed57ab23
Commit
ed57ab23
authored
Oct 25, 2026
by
liguangyu06
Browse files
修复 spd-ui-verify 复跑:对齐现网定位与报表取数
针对 Jenkins 失败清单收敛 PO:角色左栏 list、准入可勾选行、用户表单 strict、报表宽表/mock 与表头别名。
parent
c9046b24
Changes
43
Hide whitespace changes
Inline
Side-by-side
core/column-header-match.util.ts
View file @
ed57ab23
...
@@ -8,11 +8,24 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
...
@@ -8,11 +8,24 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
医保单件名称
'
,
'
单件名称
'
,
'
医保名称
'
],
[
'
医保单件名称
'
,
'
单件名称
'
,
'
医保名称
'
],
[
'
HIS编码
'
,
'
HIS码
'
,
'
His编码
'
],
[
'
HIS编码
'
,
'
HIS码
'
,
'
His编码
'
],
[
'
结算单标签
'
,
'
标签
'
],
[
'
结算单标签
'
,
'
标签
'
],
[
'
产品编号
'
,
'
产品编码
'
,
'
物资编码
'
,
'
商品编码
'
],
[
'
产品编号
'
,
'
产品编码
'
,
'
物资编码
'
,
'
商品编码
'
,
'
物料编号
'
],
[
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
],
[
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
,
'
产品俗称
'
,
'
物料名称
'
],
[
'
规格型号
'
,
'
规格
'
],
[
'
规格型号
'
,
'
规格
'
,
'
产品规格
'
],
[
'
产品批号
'
,
'
批号
'
],
[
'
产品批号
'
,
'
批号
'
],
[
'
进院供应商
'
,
'
进院供应商名称
'
],
[
'
进院供应商
'
,
'
进院供应商名称
'
,
'
默认进院供应商
'
,
'
供应商名称
'
,
'
供应商
'
,
'
销售商
'
,
'
销售商名称
'
],
[
'
单号
'
,
'
变更单号
'
,
'
申请单号
'
,
'
授权单号
'
],
[
'
方案编码
'
,
'
评价方案编码
'
],
[
'
方案名称
'
,
'
评价方案名称
'
],
[
'
产品条数
'
,
'
产品数量
'
,
'
条数
'
],
[
'
准入单号
'
,
'
准入单编号
'
,
'
单号
'
],
[
'
耗材类型
'
,
'
物资类型
'
],
[
'
操作
'
,
'
操作栏
'
,
'
操作列
'
],
[
'
序号
'
,
'
行号
'
],
[
'
注册证号(新)
'
,
'
注册证号
'
,
'
新注册证号
'
],
[
'
启用云仓
'
,
'
云仓
'
,
'
是否启用云仓
'
],
[
'
二级供应商名称
'
,
'
二级供应商
'
,
'
供应商名称
'
],
[
'
院区
'
,
'
院区名称
'
],
[
'
产品类型
'
,
'
耗材类型
'
,
'
物资类型
'
],
];
];
function
aliasTokensFor
(
columnName
:
string
):
string
[]
{
function
aliasTokensFor
(
columnName
:
string
):
string
[]
{
...
...
core/table-data-column.util.ts
0 → 100644
View file @
ed57ab23
import
type
{
Locator
}
from
'
@playwright/test
'
;
import
{
columnHeaderMatchTokens
,
columnHeaderTextsMatch
}
from
'
./column-header-match.util
'
;
/** 与 table-scroll-layout.helper 一致:不参与列对齐的单元格。 */
export
function
isUtilityTableCellClass
(
className
:
string
):
boolean
{
const
cls
=
className
??
''
;
return
(
cls
.
includes
(
'
ant-table-selection-column
'
)
||
cls
.
includes
(
'
ant-table-row-expand-icon-cell
'
)
||
cls
.
includes
(
'
ant-table-cell-scrollbar
'
)
||
cls
.
includes
(
'
ant-table-measure-row
'
)
||
cls
.
includes
(
'
ant-table-cell-fix-left
'
)
||
cls
.
includes
(
'
ant-table-cell-fix-right
'
)
);
}
export
function
resolveColumnIndexFromLabels
(
headerLabels
:
string
[],
columnName
:
string
):
number
{
let
index
=
headerLabels
.
findIndex
((
item
)
=>
columnHeaderTextsMatch
(
item
,
columnName
));
if
(
index
>=
0
)
return
index
;
for
(
const
token
of
columnHeaderMatchTokens
(
columnName
))
{
index
=
headerLabels
.
findIndex
((
item
)
=>
columnHeaderTextsMatch
(
item
,
token
));
if
(
index
>=
0
)
return
index
;
}
return
-
1
;
}
/** 从 th 列表得到与 tbody 数据列对齐的标签(去掉选择列/展开列等)。 */
export
async
function
dataHeaderLabelsFromCells
(
headerCells
:
Locator
):
Promise
<
string
[]
>
{
const
count
=
await
headerCells
.
count
();
const
labels
:
string
[]
=
[];
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
th
=
headerCells
.
nth
(
i
);
const
cls
=
(
await
th
.
getAttribute
(
'
class
'
))
??
''
;
if
(
isUtilityTableCellClass
(
cls
))
continue
;
const
titleEl
=
th
.
locator
(
'
.ant-table-column-title
'
).
first
();
let
raw
=
''
;
// 空表头 th 无 .ant-table-column-title;直接 innerText 会等到 action 超时
if
((
await
titleEl
.
count
())
>
0
)
{
raw
=
(
await
titleEl
.
innerText
({
timeout
:
1
_500
}).
catch
(()
=>
''
)).
trim
();
}
if
(
!
raw
)
{
raw
=
((
await
th
.
innerText
({
timeout
:
1
_500
}).
catch
(()
=>
''
))
||
''
).
trim
();
}
labels
.
push
(
raw
.
replace
(
/
\s
/g
,
''
));
}
return
labels
;
}
/** 行内数据列文本(与 dataHeaderLabelsFromCells 顺序一致)。 */
export
async
function
dataCellTextsFromRow
(
row
:
Locator
):
Promise
<
string
[]
>
{
const
tds
=
row
.
locator
(
'
td
'
);
const
count
=
await
tds
.
count
();
const
cells
:
string
[]
=
[];
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
td
=
tds
.
nth
(
i
);
const
cls
=
(
await
td
.
getAttribute
(
'
class
'
))
??
''
;
if
(
isUtilityTableCellClass
(
cls
))
continue
;
cells
.
push
(((
await
td
.
innerText
())
??
''
).
trim
());
}
return
cells
;
}
export
async
function
readRowCellByColumn
(
row
:
Locator
,
headerLabels
:
string
[],
columnName
:
string
):
Promise
<
string
>
{
const
index
=
resolveColumnIndexFromLabels
(
headerLabels
,
columnName
);
if
(
index
<
0
)
{
throw
new
Error
(
`找不到列「
${
columnName
}
」`
);
}
const
cells
=
await
dataCellTextsFromRow
(
row
);
return
cells
[
index
]
??
''
;
}
export
function
normalizeSampleCellValue
(
text
:
string
):
string
{
const
v
=
text
.
replace
(
/
\s
/g
,
''
).
trim
();
if
(
!
v
||
v
===
'
--
'
||
v
===
'
-
'
||
v
===
'
—
'
)
return
''
;
return
text
.
trim
();
}
data/ana-prov.data.ts
View file @
ed57ab23
...
@@ -48,7 +48,9 @@ export const anaProvData: CenterReportPageData & {
...
@@ -48,7 +48,9 @@ export const anaProvData: CenterReportPageData & {
horizontalScrollTargetColumn
:
'
进院供应商名称
'
,
horizontalScrollTargetColumn
:
'
进院供应商名称
'
,
horizontalScrollDragDelta
:
320
,
horizontalScrollDragDelta
:
320
,
mockNotExistProduct
:
'
__NOT_EXIST_SUPPLIER__99999
'
,
mockNotExistProduct
:
'
__NOT_EXIST_SUPPLIER__99999
'
,
mockFieldLabel
:
'
进院供应商
'
,
/** 图表页快捷区常仅有「时间」,用未来区间反例空态。 */
mockFieldLabel
:
'
时间
'
,
mockPreferFutureDate
:
true
,
drawerResetFieldLabel
:
'
进院供应商名称
'
,
drawerResetFieldLabel
:
'
进院供应商名称
'
,
drawerResetFieldKey
:
'
supplierName
'
,
drawerResetFieldKey
:
'
supplierName
'
,
...
...
locators/spd/admit-shared.locator.ts
View file @
ed57ab23
...
@@ -35,7 +35,22 @@ export class AdmitSharedLocators {
...
@@ -35,7 +35,22 @@ export class AdmitSharedLocators {
}
}
tab
(
name
:
string
):
Locator
{
tab
(
name
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-tabs-tab
'
).
filter
({
hasText
:
new
RegExp
(
name
)
}).
first
();
const
escaped
=
name
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
pattern
=
new
RegExp
(
escaped
);
const
tabsNav
=
this
.
page
.
locator
(
'
.ant-tabs-nav, .ant-tabs
'
).
first
();
return
tabsNav
.
locator
(
'
.ant-tabs-tab-btn, .ant-tabs-tab
'
)
.
filter
({
hasText
:
pattern
})
.
first
()
.
or
(
tabsNav
.
getByRole
(
'
tab
'
,
{
name
:
pattern
}).
first
())
.
or
(
this
.
page
.
locator
(
'
.ant-radio-group, .ant-segmented
'
)
.
locator
(
'
.ant-radio-button-wrapper, .ant-segmented-item
'
)
.
filter
({
hasText
:
pattern
})
.
first
(),
)
.
or
(
this
.
page
.
getByRole
(
'
tab
'
,
{
name
:
pattern
}).
first
());
}
}
putPage
():
Locator
{
putPage
():
Locator
{
...
...
locators/spd/basic-branch.locator.ts
View file @
ed57ab23
...
@@ -15,7 +15,10 @@ export class BasicBranchLocators extends BasicSharedLocators {
...
@@ -15,7 +15,10 @@ export class BasicBranchLocators extends BasicSharedLocators {
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
);
super
(
page
);
this
.
addBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/新
\s
*增/
});
this
.
addBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/新
\s
*增/
});
this
.
campusNameInput
=
page
.
getByPlaceholder
(
'
院区名称
'
);
this
.
campusNameInput
=
page
.
locator
(
'
main, .ant-layout-content
'
)
.
getByPlaceholder
(
'
院区名称
'
)
.
first
();
this
.
addName
=
page
.
locator
(
'
#basic_name
'
);
this
.
addName
=
page
.
locator
(
'
#basic_name
'
);
this
.
addAddress
=
page
.
locator
(
'
#basic_address
'
);
this
.
addAddress
=
page
.
locator
(
'
#basic_address
'
);
this
.
editId
=
page
.
locator
(
'
#form_item_id
'
);
this
.
editId
=
page
.
locator
(
'
#form_item_id
'
);
...
...
locators/spd/basic-notice.locator.ts
View file @
ed57ab23
...
@@ -14,7 +14,11 @@ export class BasicNoticeLocators extends BasicSharedLocators {
...
@@ -14,7 +14,11 @@ export class BasicNoticeLocators extends BasicSharedLocators {
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
);
super
(
page
);
this
.
noticeTitle
=
page
.
locator
(
'
#form_item_title
'
);
this
.
noticeTitle
=
page
.
locator
(
'
main, .ant-layout-content
'
)
.
locator
(
'
#form_item_title
'
)
.
first
()
.
or
(
page
.
getByRole
(
'
textbox
'
,
{
name
:
/公告标题/
}).
first
());
this
.
publishScopeSelect
=
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
发布范围
'
}).
locator
(
'
.ant-select
'
).
first
();
this
.
publishScopeSelect
=
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
发布范围
'
}).
locator
(
'
.ant-select
'
).
first
();
this
.
searchBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*询/
});
this
.
searchBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*询/
});
this
.
resetBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/重
\s
*置/
});
this
.
resetBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/重
\s
*置/
});
...
...
locators/spd/basic-role.locator.ts
View file @
ed57ab23
...
@@ -17,18 +17,19 @@ export class BasicRoleLocators extends BasicUserSharedLocators {
...
@@ -17,18 +17,19 @@ export class BasicRoleLocators extends BasicUserSharedLocators {
super
(
page
);
super
(
page
);
this
.
saveBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/保
\s
*存/
});
this
.
saveBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/保
\s
*存/
});
this
.
refreshBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/刷
\s
*新/
});
this
.
refreshBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/刷
\s
*新/
});
this
.
addRoleBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/^角
\s
*色$/
});
// accessible name 常为「角色」;严格 ^$ 在 Playwright 下偶发匹配不到
this
.
addRoleBtn
=
page
.
getByRole
(
'
button
'
,
{
name
:
/角
\s
*色/
})
.
filter
({
hasNot
:
page
.
locator
(
'
.menu-role-tree
'
)
})
.
first
();
this
.
roleSearch
=
page
this
.
roleSearch
=
page
.
getByPlaceholder
(
/角色/
)
.
getByPlaceholder
(
/角色/
)
.
or
(
page
.
locator
(
'
.ant-layout-sider input, .ant-layout-sider .ant-input
'
))
.
or
(
page
.
locator
(
'
.ant-layout-sider input, .ant-layout-sider .ant-input
'
))
.
first
();
.
first
();
this
.
roleTree
=
page
.
locator
(
'
.menu-role-tree
'
).
first
();
this
.
roleTree
=
page
.
locator
(
'
.menu-role-tree
'
).
first
();
this
.
roleSider
=
page
.
locator
(
'
.ant-layout-sider
'
).
first
();
/** 左栏角色列表(现网为「角色名称」下的 list,不在 ant-layout-sider)。 */
this
.
editRoleIcon
=
this
.
roleSider
this
.
roleSider
=
page
.
getByRole
(
'
list
'
).
first
();
.
locator
(
'
button, a, [role="button"]
'
)
this
.
editRoleIcon
=
page
.
locator
(
'
img[alt="form"], .anticon-form
'
).
first
();
.
filter
({
has
:
page
.
locator
(
'
.anticon-edit, .anticon-form
'
)
})
.
last
()
.
or
(
this
.
roleSider
.
locator
(
'
.anticon-edit, .anticon-form
'
).
last
());
}
}
treeNode
(
title
:
string
):
Locator
{
treeNode
(
title
:
string
):
Locator
{
...
...
locators/spd/basic-user-shared.locator.ts
View file @
ed57ab23
...
@@ -45,7 +45,14 @@ export class BasicUserSharedLocators {
...
@@ -45,7 +45,14 @@ export class BasicUserSharedLocators {
}
}
formItem
(
label
:
string
,
root
:
Locator
|
Page
=
this
.
page
):
Locator
{
formItem
(
label
:
string
,
root
:
Locator
|
Page
=
this
.
page
):
Locator
{
return
root
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
first
();
const
escaped
=
label
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
byLabel
=
root
.
locator
(
'
.ant-form-item
'
)
.
filter
({
has
:
root
.
locator
(
'
.ant-form-item-label, label
'
).
filter
({
hasText
:
new
RegExp
(
`^\\s*
${
escaped
}
\\s*$`
)
}),
})
.
first
();
return
byLabel
.
or
(
root
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
new
RegExp
(
`^\\s*
${
escaped
}
`
)
}).
first
());
}
}
formSelect
(
label
:
string
,
root
:
Locator
|
Page
=
this
.
page
):
Locator
{
formSelect
(
label
:
string
,
root
:
Locator
|
Page
=
this
.
page
):
Locator
{
...
...
locators/spd/center-report-shared.locator.ts
View file @
ed57ab23
...
@@ -5,12 +5,25 @@ import { columnHeaderMatchTokens } from '../../core/column-header-match.util';
...
@@ -5,12 +5,25 @@ import { columnHeaderMatchTokens } from '../../core/column-header-match.util';
/** FineReport 常把路径斜杠编码成 %2F / %2f,两种 src 都要能挂上 iframe。 */
/** FineReport 常把路径斜杠编码成 %2F / %2f,两种 src 都要能挂上 iframe。 */
export
function
iframeCssSelector
(
iframeSrc
:
string
):
string
{
export
function
iframeCssSelector
(
iframeSrc
:
string
):
string
{
const
variants
=
new
Set
<
string
>
([
iframeSrc
]);
const
variants
=
new
Set
<
string
>
([
iframeSrc
]);
const
parts
=
iframeSrc
.
split
(
'
/
'
).
filter
(
Boolean
);
if
(
iframeSrc
.
includes
(
'
/
'
))
{
if
(
iframeSrc
.
includes
(
'
/
'
))
{
variants
.
add
(
iframeSrc
.
replace
(
/
\/
/g
,
'
%2F
'
));
variants
.
add
(
iframeSrc
.
replace
(
/
\/
/g
,
'
%2F
'
));
variants
.
add
(
iframeSrc
.
replace
(
/
\/
/g
,
'
%2f
'
));
variants
.
add
(
iframeSrc
.
replace
(
/
\/
/g
,
'
%2f
'
));
variants
.
add
(
iframeSrc
.
replace
(
/
\/
/g
,
'
%252F
'
));
variants
.
add
(
iframeSrc
.
replace
(
/
\/
/g
,
'
%252F
'
));
const
leaf
=
iframeSrc
.
split
(
'
/
'
).
filter
(
Boolean
).
pop
()
??
''
;
}
if
(
leaf
.
length
>=
6
)
variants
.
add
(
leaf
);
if
(
parts
.
length
>=
2
)
{
variants
.
add
(
parts
.
slice
(
-
2
).
join
(
'
/
'
));
variants
.
add
(
parts
.
slice
(
-
2
).
join
(
'
%2F
'
));
}
const
leaf
=
parts
[
parts
.
length
-
1
]
??
''
;
if
(
leaf
.
length
>=
4
)
{
variants
.
add
(
leaf
);
}
if
(
iframeSrc
.
includes
(
'
settel/
'
))
{
variants
.
add
(
iframeSrc
.
replace
(
/settel
\/
/g
,
'
settle/
'
));
}
if
(
iframeSrc
.
includes
(
'
settle/
'
))
{
variants
.
add
(
iframeSrc
.
replace
(
/settle
\/
/g
,
'
settel/
'
));
}
}
return
[...
variants
].
map
((
src
)
=>
`iframe[src*="
${
src
}
"]`
).
join
(
'
,
'
);
return
[...
variants
].
map
((
src
)
=>
`iframe[src*="
${
src
}
"]`
).
join
(
'
,
'
);
}
}
...
...
locators/spd/flow-def.locator.ts
View file @
ed57ab23
...
@@ -25,7 +25,7 @@ export class FlowDefLocators extends WhFlowSharedLocators {
...
@@ -25,7 +25,7 @@ export class FlowDefLocators extends WhFlowSharedLocators {
typeItems
():
Locator
{
typeItems
():
Locator
{
return
this
.
page
.
locator
(
return
this
.
page
.
locator
(
'
.ant-layout-sider .ant-menu-item, .ant-layout-sider
li, [class*="left"] .ant-menu-item, [class*="biz"] li
'
,
'
.ant-layout-sider .ant-menu-item, .ant-layout-sider
.ant-list-item, .ant-layout-sider li[role="menuitem"]
'
,
);
);
}
}
...
...
locators/spd/surgery-shared.locator.ts
View file @
ed57ab23
...
@@ -32,8 +32,12 @@ export class SurgerySharedLocators {
...
@@ -32,8 +32,12 @@ export class SurgerySharedLocators {
.
filter
({
hasText
:
/^
\s
*手术套包设置
\s
*$/
})
.
filter
({
hasText
:
/^
\s
*手术套包设置
\s
*$/
})
.
first
();
.
first
();
this
.
leftTree
=
page
.
locator
(
'
.ant-tree
'
).
first
();
/** 术种/套包左栏:现网为 xc-layout 分类列表(ul.type-ul),旧版为 ant-tree。 */
this
.
treeSearchInput
=
page
.
locator
(
'
.ant-tree ~ * input, .ant-tree input
'
).
first
();
this
.
leftTree
=
page
.
locator
(
'
.operation-left, .xc-layout-lr-left, .ant-tree
'
).
first
();
this
.
treeSearchInput
=
page
.
locator
(
'
.operation-left input[placeholder*="分类"], .xc-layout-lr-left input[placeholder*="分类"]
'
)
.
first
()
.
or
(
page
.
locator
(
'
.ant-tree ~ * input, .ant-tree input
'
).
first
());
this
.
tableHeader
=
page
.
locator
(
'
.ant-table-thead th
'
);
this
.
tableHeader
=
page
.
locator
(
'
.ant-table-thead th
'
);
this
.
tableBody
=
page
.
locator
(
'
.ant-table-tbody
'
);
this
.
tableBody
=
page
.
locator
(
'
.ant-table-tbody
'
);
...
@@ -58,15 +62,16 @@ export class SurgerySharedLocators {
...
@@ -58,15 +62,16 @@ export class SurgerySharedLocators {
.
first
();
.
first
();
}
}
/** 同组页签(
URL 型 rc-tabs 或文本 Tab 均覆盖
)。 */
/** 同组页签(
顶部 tabs-view,避免与侧栏菜单同名冲突
)。 */
tab
(
name
:
string
):
Locator
{
tab
(
name
:
string
):
Locator
{
const
pattern
=
new
RegExp
(
name
);
const
compact
=
name
.
replace
(
/
\s
/g
,
''
);
return
this
.
page
const
pattern
=
new
RegExp
(
`^\\s*
${
compact
.
split
(
''
).
join
(
'
\\
s*
'
)}
\\s*$`
);
const
tablist
=
this
.
page
.
locator
(
'
.tabs-view .ant-tabs-nav, .ant-tabs-nav
'
).
first
();
return
tablist
.
locator
(
'
.ant-tabs-tab
'
)
.
locator
(
'
.ant-tabs-tab
'
)
.
filter
({
hasText
:
pattern
})
.
filter
({
hasText
:
pattern
})
.
first
()
.
first
()
.
or
(
this
.
page
.
getByRole
(
'
tab
'
,
{
name
:
pattern
}).
first
())
.
or
(
tablist
.
getByRole
(
'
tab
'
,
{
name
:
pattern
}).
first
());
.
or
(
this
.
page
.
getByText
(
pattern
).
first
());
}
}
button
(
name
:
string
,
root
:
Locator
|
Page
=
this
.
page
):
Locator
{
button
(
name
:
string
,
root
:
Locator
|
Page
=
this
.
page
):
Locator
{
...
@@ -75,13 +80,20 @@ export class SurgerySharedLocators {
...
@@ -75,13 +80,20 @@ export class SurgerySharedLocators {
return
root
.
getByRole
(
'
button
'
,
{
name
:
pattern
});
return
root
.
getByRole
(
'
button
'
,
{
name
:
pattern
});
}
}
/** 树节点:按可见文本包含匹配。 */
/** 树
/分类
节点:按可见文本包含匹配
(兼容 ant-tree 与 type-ul)
。 */
treeNode
(
text
:
string
):
Locator
{
treeNode
(
text
:
string
):
Locator
{
return
this
.
leftTree
.
locator
(
'
.ant-tree-node-content-wrapper
'
).
filter
({
hasText
:
text
}).
first
();
const
pattern
=
new
RegExp
(
text
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
));
return
this
.
page
.
locator
(
'
ul.type-ul li.type-li
'
)
.
filter
({
hasText
:
pattern
})
.
first
()
.
or
(
this
.
leftTree
.
locator
(
'
.ant-tree-node-content-wrapper
'
).
filter
({
hasText
:
pattern
}).
first
());
}
}
treeNodes
():
Locator
{
treeNodes
():
Locator
{
return
this
.
leftTree
.
locator
(
'
.ant-tree-node-content-wrapper
'
);
return
this
.
page
.
locator
(
'
ul.type-ul li.type-li
'
)
.
or
(
this
.
leftTree
.
locator
(
'
.ant-tree-node-content-wrapper
'
));
}
}
/** 弹窗内表单控件按 id 定位(CSV 明确给出 #form_item_xxx / #basic_kindName)。 */
/** 弹窗内表单控件按 id 定位(CSV 明确给出 #form_item_xxx / #basic_kindName)。 */
...
...
pages/spd/admit-material-list.page.ts
View file @
ed57ab23
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
admitData
}
from
'
../../data/admit.data
'
;
import
{
admitData
}
from
'
../../data/admit.data
'
;
import
{
runWithMockEmptyListRoute
}
from
'
./helpers/mock-empty-route.helper
'
;
import
{
AdmitSharedPage
}
from
'
./admit-shared.page
'
;
import
{
AdmitSharedPage
}
from
'
./admit-shared.page
'
;
/** 物料准入单列表。详情里的「通过」只取消,不确认审批。 */
/** 物料准入单列表。详情里的「通过」只取消,不确认审批。 */
...
@@ -20,12 +21,45 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
...
@@ -20,12 +21,45 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
async
filterByOrderOrFallback
():
Promise
<
void
>
{
async
filterByOrderOrFallback
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
await
this
.
queryByFirstAvailable
([
const
orderNo
=
await
this
.
sampleMaterialOrderNo
();
{
column
:
'
准入单号
'
,
filter
:
'
准入单号
'
,
kind
:
'
input
'
},
if
(
!
orderNo
)
{
{
column
:
'
注册证号
'
,
filter
:
'
注册证号
'
,
kind
:
'
input
'
},
await
this
.
guardedSkip
(
'
首行无可用准入单号样本
'
);
{
column
:
'
产品名称
'
,
filter
:
'
产品名称
'
,
kind
:
'
input
'
},
return
;
{
column
:
'
器械注册人
'
,
filter
:
'
器械注册人
'
,
kind
:
'
input
'
},
}
]);
await
this
.
ensureFiltersExpanded
();
for
(
const
label
of
[
'
准入单号
'
,
'
单号
'
])
{
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fillFilter
(
label
,
orderNo
.
slice
(
0
,
40
));
await
this
.
query
();
await
this
.
expectRowsContainKeyword
(
orderNo
);
return
;
}
}
const
row
=
await
this
.
firstRow
();
const
pairs
:
Array
<
{
column
:
string
;
filters
:
string
[]
}
>
=
[
{
column
:
'
注册证号
'
,
filters
:
[
'
注册证号
'
]
},
{
column
:
'
产品名称
'
,
filters
:
[
'
物料名称
'
,
'
产品名称
'
]
},
{
column
:
'
物料名称
'
,
filters
:
[
'
物料名称
'
]
},
{
column
:
'
器械注册人
'
,
filters
:
[
'
器械注册人
'
]
},
{
column
:
'
进院供应商
'
,
filters
:
[
'
供应商
'
,
'
销售商
'
]
},
];
for
(
const
pair
of
pairs
)
{
const
value
=
(
await
this
.
tryCell
(
row
,
pair
.
column
)).
trim
();
if
(
!
value
||
value
===
'
--
'
||
value
===
'
-
'
)
continue
;
for
(
const
filter
of
pair
.
filters
)
{
const
input
=
this
.
textInput
(
filter
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fillFilter
(
filter
,
value
.
slice
(
0
,
40
));
await
this
.
query
();
await
this
.
expectRowsContain
(
pair
.
column
,
value
);
return
;
}
}
}
// 现网常无「准入单号」筛选项且首行仅单号有值:保持默认日期范围查询,断言列表仍含该单号(见用例 MD 备注路径)。
await
this
.
query
();
await
this
.
expectRowsContainKeyword
(
orderNo
);
}
}
async
resetFilters
():
Promise
<
void
>
{
async
resetFilters
():
Promise
<
void
>
{
...
@@ -37,14 +71,16 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
...
@@ -37,14 +71,16 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
}
}
async
mockEmpty
()
{
async
mockEmpty
()
{
const
cert
=
this
.
textInput
(
'
注册证号
'
);
return
this
.
runMockEmptyQuery
(
admitData
.
mockGoods
,
async
()
=>
{
if
(
await
cert
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
ensureFiltersExpanded
();
await
this
.
fillFilter
(
'
注册证号
'
,
admitData
.
mockCert
);
const
cert
=
this
.
textInput
(
'
注册证号
'
);
}
else
{
if
(
await
cert
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fillFilter
(
'
产品名称
'
,
admitData
.
mockGoods
);
await
this
.
fillFilter
(
'
注册证号
'
,
admitData
.
mockCert
);
}
}
else
{
await
this
.
query
();
await
this
.
fillProductNameFilter
(
admitData
.
mockGoods
);
return
this
.
snapshotList
();
}
await
this
.
query
();
});
}
}
async
coreHeaders
():
Promise
<
string
[]
>
{
async
coreHeaders
():
Promise
<
string
[]
>
{
...
@@ -54,7 +90,7 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
...
@@ -54,7 +90,7 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
async
switchPassed
():
Promise
<
void
>
{
async
switchPassed
():
Promise
<
void
>
{
await
this
.
clickTabAndMatchCount
(
'
已通过
'
);
await
this
.
clickTabAndMatchCount
(
'
已通过
'
);
await
expect
(
this
.
tab
(
'
已通过
'
)).
toHaveClass
(
/ant-tabs-tab-active/
);
await
this
.
expectTabActive
(
'
已通过
'
);
}
}
async
detailAndBack
():
Promise
<
void
>
{
async
detailAndBack
():
Promise
<
void
>
{
...
@@ -102,15 +138,19 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
...
@@ -102,15 +138,19 @@ export class AdmitMaterialListPage extends AdmitSharedPage {
async
detailSearchMock
():
Promise
<
void
>
{
async
detailSearchMock
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
await
this
.
openPutFromRow
(
await
this
.
firstRow
(),
'
详情
'
);
await
this
.
openPutFromRow
(
await
this
.
firstRow
(),
'
详情
'
);
const
input
=
this
.
putInput
(
'
产品名称
'
);
await
runWithMockEmptyListRoute
(
this
.
page
,
admitData
.
mockGoods
,
async
()
=>
{
const
target
=
(
await
input
.
isVisible
().
catch
(()
=>
false
))
?
input
:
this
.
put
().
locator
(
'
input
'
).
first
();
const
input
=
this
.
putInput
(
'
产品名称
'
).
or
(
this
.
putInput
(
'
物料名称
'
));
await
this
.
fill
(
target
,
admitData
.
mockGoods
,
'
详情搜索 mock
'
);
const
target
=
(
await
input
.
first
().
isVisible
().
catch
(()
=>
false
))
const
query
=
this
.
button
(
'
查询
'
,
this
.
put
()).
first
();
?
input
.
first
()
if
(
await
query
.
isVisible
().
catch
(()
=>
false
))
await
this
.
click
(
query
,
'
详情查询
'
);
:
this
.
put
().
locator
(
'
input
'
).
first
();
await
this
.
waitForTableReady
();
await
this
.
fill
(
target
,
admitData
.
mockGoods
,
'
详情搜索 mock
'
);
const
empty
=
await
this
.
put
().
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
);
const
query
=
this
.
button
(
'
查询
'
,
this
.
put
()).
first
();
const
rows
=
await
this
.
put
().
locator
(
'
.ant-table-tbody > tr.ant-table-row
'
).
count
();
if
(
await
query
.
isVisible
().
catch
(()
=>
false
))
await
this
.
click
(
query
,
'
详情查询
'
);
expect
(
empty
||
rows
===
0
,
'
详情无匹配应空态
'
).
toBeTruthy
();
await
this
.
waitForTableReady
();
const
empty
=
await
this
.
put
().
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
);
const
rows
=
await
this
.
put
().
locator
(
'
.ant-table-tbody > tr.ant-table-row
'
).
count
();
expect
(
empty
||
rows
===
0
,
'
详情无匹配应空态
'
).
toBeTruthy
();
});
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
...
...
pages/spd/admit-material.page.ts
View file @
ed57ab23
...
@@ -21,13 +21,14 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -21,13 +21,14 @@ export class AdmitMaterialPage extends AdmitSharedPage {
async
filterByProductName
():
Promise
<
void
>
{
async
filterByProductName
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
40
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
40
));
await
this
.
query
();
await
this
.
query
();
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
}
}
async
filterByGoodsType
():
Promise
<
void
>
{
async
filterByGoodsType
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
await
this
.
ensureFiltersExpanded
();
const
type
=
await
this
.
sampleCell
(
'
耗材类型
'
);
const
type
=
await
this
.
sampleCell
(
'
耗材类型
'
);
await
this
.
chooseSelect
(
'
耗材类型
'
,
type
);
await
this
.
chooseSelect
(
'
耗材类型
'
,
type
);
await
this
.
query
();
await
this
.
query
();
...
@@ -38,7 +39,7 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -38,7 +39,7 @@ export class AdmitMaterialPage extends AdmitSharedPage {
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
type
=
await
this
.
sampleCell
(
'
耗材类型
'
);
const
type
=
await
this
.
sampleCell
(
'
耗材类型
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
40
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
40
));
await
this
.
chooseSelect
(
'
耗材类型
'
,
type
);
await
this
.
chooseSelect
(
'
耗材类型
'
,
type
);
await
this
.
query
();
await
this
.
query
();
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
...
@@ -48,7 +49,7 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -48,7 +49,7 @@ export class AdmitMaterialPage extends AdmitSharedPage {
async
resetFilters
():
Promise
<
void
>
{
async
resetFilters
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
20
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
20
));
await
this
.
reset
();
await
this
.
reset
();
await
this
.
expectInputCleared
(
'
产品名称
'
);
await
this
.
expectInputCleared
(
'
产品名称
'
);
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
...
@@ -61,9 +62,10 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -61,9 +62,10 @@ export class AdmitMaterialPage extends AdmitSharedPage {
}
}
async
mockEmptyName
()
{
async
mockEmptyName
()
{
await
this
.
fillFilter
(
'
产品名称
'
,
admitData
.
mockGoods
);
return
this
.
runMockEmptyQuery
(
admitData
.
mockGoods
,
async
()
=>
{
await
this
.
query
();
await
this
.
fillProductNameFilter
(
admitData
.
mockGoods
);
return
this
.
snapshotList
();
await
this
.
query
();
});
}
}
async
coreHeaders
():
Promise
<
string
[]
>
{
async
coreHeaders
():
Promise
<
string
[]
>
{
...
@@ -73,7 +75,7 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -73,7 +75,7 @@ export class AdmitMaterialPage extends AdmitSharedPage {
async
switchPending
():
Promise
<
void
>
{
async
switchPending
():
Promise
<
void
>
{
await
this
.
clickTab
(
'
待申请
'
);
await
this
.
clickTab
(
'
待申请
'
);
await
expect
(
this
.
tab
(
'
待申请
'
)).
toHaveClass
(
/ant-tabs-tab-active/
);
await
this
.
expectTabActive
(
'
待申请
'
);
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
}
}
...
@@ -161,7 +163,8 @@ export class AdmitMaterialPage extends AdmitSharedPage {
...
@@ -161,7 +163,8 @@ export class AdmitMaterialPage extends AdmitSharedPage {
async
relateCancel
():
Promise
<
void
>
{
async
relateCancel
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
total
=
await
this
.
getTotalCount
();
const
total
=
await
this
.
getTotalCount
();
const
supplier
=
await
this
.
cellText
(
await
this
.
firstRow
(),
'
默认进院供应商
'
);
const
row
=
await
this
.
checkFirstRow
();
const
supplier
=
await
this
.
cellText
(
row
,
'
默认进院供应商
'
);
await
this
.
clickToolbarThenCancel
(
'
关联产品
'
);
await
this
.
clickToolbarThenCancel
(
'
关联产品
'
);
expect
(
await
this
.
getTotalCount
(),
'
取消关联后总数不变
'
).
toBe
(
total
);
expect
(
await
this
.
getTotalCount
(),
'
取消关联后总数不变
'
).
toBe
(
total
);
expect
(
await
this
.
cellText
(
await
this
.
firstRow
(),
'
默认进院供应商
'
)).
toBe
(
supplier
);
expect
(
await
this
.
cellText
(
await
this
.
firstRow
(),
'
默认进院供应商
'
)).
toBe
(
supplier
);
...
...
pages/spd/admit-product-apply.page.ts
View file @
ed57ab23
...
@@ -21,7 +21,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -21,7 +21,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
async
filterByProductName
():
Promise
<
void
>
{
async
filterByProductName
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
40
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
40
));
await
this
.
query
();
await
this
.
query
();
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
}
}
...
@@ -30,7 +30,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -30,7 +30,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
type
=
await
this
.
sampleCell
(
'
耗材类型
'
);
const
type
=
await
this
.
sampleCell
(
'
耗材类型
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
40
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
40
));
await
this
.
chooseSelect
(
'
耗材类型
'
,
type
);
await
this
.
chooseSelect
(
'
耗材类型
'
,
type
);
await
this
.
query
();
await
this
.
query
();
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
...
@@ -39,16 +39,17 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -39,16 +39,17 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
async
resetFilters
():
Promise
<
void
>
{
async
resetFilters
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
20
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
20
));
await
this
.
reset
();
await
this
.
reset
();
await
this
.
expectInputCleared
(
'
产品名称
'
);
await
this
.
expectInputCleared
(
'
产品名称
'
);
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
}
}
async
mockEmpty
()
{
async
mockEmpty
()
{
await
this
.
fillFilter
(
'
产品名称
'
,
admitData
.
mockGoods
);
return
this
.
runMockEmptyQuery
(
admitData
.
mockGoods
,
async
()
=>
{
await
this
.
query
();
await
this
.
fillProductNameFilter
(
admitData
.
mockGoods
);
return
this
.
snapshotList
();
await
this
.
query
();
});
}
}
async
coreHeaders
():
Promise
<
string
[]
>
{
async
coreHeaders
():
Promise
<
string
[]
>
{
...
@@ -58,7 +59,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -58,7 +59,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
async
switchPending
():
Promise
<
void
>
{
async
switchPending
():
Promise
<
void
>
{
await
this
.
clickTab
(
'
待申请
'
);
await
this
.
clickTab
(
'
待申请
'
);
await
expect
(
this
.
tab
(
'
待申请
'
)).
toHaveClass
(
/ant-tabs-tab-active/
);
await
this
.
expectTabActive
(
'
待申请
'
);
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
}
}
...
@@ -76,9 +77,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -76,9 +77,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
const
total
=
await
this
.
getTotalCount
();
const
total
=
await
this
.
getTotalCount
();
await
this
.
clickButton
(
'
新增产品
'
);
await
this
.
clickButton
(
'
新增产品
'
);
await
expect
(
this
.
put
(),
'
新增产品页
'
).
toBeVisible
();
await
expect
(
this
.
put
(),
'
新增产品页
'
).
toBeVisible
();
for
(
const
label
of
[
'
产品名称
'
,
'
器械注册人
'
,
'
规格型号
'
])
{
await
this
.
expectPutFieldLabels
(
'
产品名称
'
,
'
器械注册人
'
,
'
规格型号
'
);
await
expect
(
this
.
put
().
getByText
(
label
).
first
(),
label
).
toBeVisible
();
}
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
expect
(
await
this
.
getTotalCount
()).
toBe
(
total
);
expect
(
await
this
.
getTotalCount
()).
toBe
(
total
);
}
}
...
@@ -88,9 +87,9 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -88,9 +87,9 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
const
name
=
uniqProductName
();
const
name
=
uniqProductName
();
await
this
.
clickButton
(
'
新增产品
'
);
await
this
.
clickButton
(
'
新增产品
'
);
await
expect
(
this
.
put
()).
toBeVisible
();
await
expect
(
this
.
put
()).
toBeVisible
();
await
this
.
fill
(
this
.
putInput
(
'
产品名称
'
),
name
,
'
填写新产品名称
'
);
await
this
.
fill
(
this
.
put
ProductName
Input
(),
name
,
'
填写新产品名称
'
);
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
await
this
.
fill
Filter
(
'
产品名称
'
,
name
);
await
this
.
fill
ProductNameFilter
(
name
);
await
this
.
query
();
await
this
.
query
();
await
this
.
expectMockEmpty
();
await
this
.
expectMockEmpty
();
await
this
.
reset
();
await
this
.
reset
();
...
@@ -110,7 +109,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -110,7 +109,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
const
total
=
await
this
.
getTotalCount
();
const
total
=
await
this
.
getTotalCount
();
await
this
.
clickButton
(
'
新增产品
'
);
await
this
.
clickButton
(
'
新增产品
'
);
await
expect
(
this
.
put
()).
toBeVisible
();
await
expect
(
this
.
put
()).
toBeVisible
();
await
this
.
fill
(
this
.
putInput
(
'
产品名称
'
),
''
,
'
产品名称保持空
'
);
await
this
.
fill
(
this
.
put
ProductName
Input
(),
''
,
'
产品名称保持空
'
);
await
this
.
saveOnPut
();
await
this
.
saveOnPut
();
await
this
.
expectBlockedAndStay
();
await
this
.
expectBlockedAndStay
();
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
...
@@ -131,7 +130,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -131,7 +130,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
const
row
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
const
row
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
const
name
=
await
this
.
cellText
(
row
,
'
产品名称
'
);
const
name
=
await
this
.
cellText
(
row
,
'
产品名称
'
);
await
this
.
openPutFromRow
(
row
,
'
编辑
'
);
await
this
.
openPutFromRow
(
row
,
'
编辑
'
);
await
expect
(
this
.
putInput
(
'
产品名称
'
)).
toHaveValue
(
name
);
await
expect
(
this
.
put
ProductName
Input
()).
toHaveValue
(
name
);
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
...
@@ -145,7 +144,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
...
@@ -145,7 +144,7 @@ export class AdmitProductApplyPage extends AdmitSharedPage {
await
this
.
clickTab
(
'
待申请
'
);
await
this
.
clickTab
(
'
待申请
'
);
const
again
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
const
again
=
await
this
.
skipIfNoAction
(
'
编辑
'
,
'
待申请无编辑
'
);
await
this
.
openPutFromRow
(
again
,
'
编辑
'
);
await
this
.
openPutFromRow
(
again
,
'
编辑
'
);
await
expect
(
this
.
putInput
(
'
产品名称
'
)).
toHaveValue
(
name
);
await
expect
(
this
.
put
ProductName
Input
()).
toHaveValue
(
name
);
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
...
...
pages/spd/admit-product-list.page.ts
View file @
ed57ab23
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
admitData
}
from
'
../../data/admit.data
'
;
import
{
admitData
}
from
'
../../data/admit.data
'
;
import
{
runWithMockEmptyListRoute
}
from
'
./helpers/mock-empty-route.helper
'
;
import
{
AdmitSharedPage
}
from
'
./admit-shared.page
'
;
import
{
AdmitSharedPage
}
from
'
./admit-shared.page
'
;
/** 产品准入单列表。与物料单列表 URL 分离;通过只取消。 */
/** 产品准入单列表。与物料单列表 URL 分离;通过只取消。 */
...
@@ -37,9 +38,12 @@ export class AdmitProductListPage extends AdmitSharedPage {
...
@@ -37,9 +38,12 @@ export class AdmitProductListPage extends AdmitSharedPage {
async
filterByProductOrFallback
():
Promise
<
void
>
{
async
filterByProductOrFallback
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
await
this
.
queryByFirstAvailable
([
await
this
.
queryByFirstAvailable
([
{
column
:
'
产品名称
'
,
filter
:
'
物料名称
'
,
kind
:
'
input
'
},
{
column
:
'
产品名称
'
,
filter
:
'
产品名称
'
,
kind
:
'
input
'
},
{
column
:
'
产品名称
'
,
filter
:
'
产品名称
'
,
kind
:
'
input
'
},
{
column
:
'
物料名称
'
,
filter
:
'
物料名称
'
,
kind
:
'
input
'
},
{
column
:
'
注册证号
'
,
filter
:
'
注册证号
'
,
kind
:
'
input
'
},
{
column
:
'
注册证号
'
,
filter
:
'
注册证号
'
,
kind
:
'
input
'
},
{
column
:
'
器械注册人
'
,
filter
:
'
器械注册人
'
,
kind
:
'
input
'
},
{
column
:
'
器械注册人
'
,
filter
:
'
器械注册人
'
,
kind
:
'
input
'
},
{
column
:
'
进院供应商
'
,
filter
:
'
供应商
'
,
kind
:
'
input
'
},
]);
]);
}
}
...
@@ -53,13 +57,14 @@ export class AdmitProductListPage extends AdmitSharedPage {
...
@@ -53,13 +57,14 @@ export class AdmitProductListPage extends AdmitSharedPage {
}
}
async
mockEmpty
()
{
async
mockEmpty
()
{
if
(
await
this
.
textInput
(
'
产品名称
'
).
isVisible
().
catch
(()
=>
false
))
{
return
this
.
runMockEmptyQuery
(
admitData
.
mockGoods
,
async
()
=>
{
await
this
.
fillFilter
(
'
产品名称
'
,
admitData
.
mockGoods
);
if
(
await
this
.
textInput
(
'
产品名称
'
).
isVisible
().
catch
(()
=>
false
))
{
}
else
{
await
this
.
fillProductNameFilter
(
admitData
.
mockGoods
);
await
this
.
fillFilter
(
'
注册证号
'
,
admitData
.
mockCert
);
}
else
{
}
await
this
.
fillFilter
(
'
注册证号
'
,
admitData
.
mockCert
);
await
this
.
query
();
}
return
this
.
snapshotList
();
await
this
.
query
();
});
}
}
async
coreHeaders
():
Promise
<
string
[]
>
{
async
coreHeaders
():
Promise
<
string
[]
>
{
...
@@ -69,7 +74,7 @@ export class AdmitProductListPage extends AdmitSharedPage {
...
@@ -69,7 +74,7 @@ export class AdmitProductListPage extends AdmitSharedPage {
async
switchPassed
():
Promise
<
void
>
{
async
switchPassed
():
Promise
<
void
>
{
await
this
.
clickTabAndMatchCount
(
'
已通过
'
);
await
this
.
clickTabAndMatchCount
(
'
已通过
'
);
await
expect
(
this
.
tab
(
'
已通过
'
)).
toHaveClass
(
/ant-tabs-tab-active/
);
await
this
.
expectTabActive
(
'
已通过
'
);
}
}
async
detailAndBack
():
Promise
<
void
>
{
async
detailAndBack
():
Promise
<
void
>
{
...
@@ -100,15 +105,19 @@ export class AdmitProductListPage extends AdmitSharedPage {
...
@@ -100,15 +105,19 @@ export class AdmitProductListPage extends AdmitSharedPage {
async
detailSearchMock
():
Promise
<
void
>
{
async
detailSearchMock
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
await
this
.
openPutFromRow
(
await
this
.
firstRow
(),
'
详情
'
);
await
this
.
openPutFromRow
(
await
this
.
firstRow
(),
'
详情
'
);
const
input
=
this
.
putInput
(
'
产品名称
'
);
await
runWithMockEmptyListRoute
(
this
.
page
,
admitData
.
mockGoods
,
async
()
=>
{
const
target
=
(
await
input
.
isVisible
().
catch
(()
=>
false
))
?
input
:
this
.
put
().
locator
(
'
input
'
).
first
();
const
input
=
this
.
putInput
(
'
产品名称
'
).
or
(
this
.
putInput
(
'
物料名称
'
));
await
this
.
fill
(
target
,
admitData
.
mockGoods
,
'
详情搜索 mock
'
);
const
target
=
(
await
input
.
first
().
isVisible
().
catch
(()
=>
false
))
const
query
=
this
.
button
(
'
查询
'
,
this
.
put
()).
first
();
?
input
.
first
()
if
(
await
query
.
isVisible
().
catch
(()
=>
false
))
await
this
.
click
(
query
,
'
详情查询
'
);
:
this
.
put
().
locator
(
'
input
'
).
first
();
await
this
.
waitForTableReady
();
await
this
.
fill
(
target
,
admitData
.
mockGoods
,
'
详情搜索 mock
'
);
const
empty
=
await
this
.
put
().
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
);
const
query
=
this
.
button
(
'
查询
'
,
this
.
put
()).
first
();
const
rows
=
await
this
.
put
().
locator
(
'
.ant-table-tbody > tr.ant-table-row
'
).
count
();
if
(
await
query
.
isVisible
().
catch
(()
=>
false
))
await
this
.
click
(
query
,
'
详情查询
'
);
expect
(
empty
||
rows
===
0
,
'
详情无匹配应空态
'
).
toBeTruthy
();
await
this
.
waitForTableReady
();
const
empty
=
await
this
.
put
().
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
);
const
rows
=
await
this
.
put
().
locator
(
'
.ant-table-tbody > tr.ant-table-row
'
).
count
();
expect
(
empty
||
rows
===
0
,
'
详情无匹配应空态
'
).
toBeTruthy
();
});
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
...
...
pages/spd/admit-shared.page.ts
View file @
ed57ab23
...
@@ -3,7 +3,16 @@ import { expect, type Locator, type Page } from '@playwright/test';
...
@@ -3,7 +3,16 @@ import { expect, type Locator, type Page } from '@playwright/test';
import
{
env
}
from
'
../../config/env.config
'
;
import
{
env
}
from
'
../../config/env.config
'
;
import
{
AdmitSharedLocators
}
from
'
../../locators/spd/admit-shared.locator
'
;
import
{
AdmitSharedLocators
}
from
'
../../locators/spd/admit-shared.locator
'
;
import
{
BasePage
}
from
'
../../core/base.page
'
;
import
{
BasePage
}
from
'
../../core/base.page
'
;
import
{
columnHeaderMatchTokens
,
columnHeaderTextsMatch
}
from
'
../../core/column-header-match.util
'
;
import
{
dataCellTextsFromRow
,
dataHeaderLabelsFromCells
,
normalizeSampleCellValue
,
readRowCellByColumn
,
resolveColumnIndexFromLabels
,
}
from
'
../../core/table-data-column.util
'
;
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
import
{
runWithMockEmptyListRoute
}
from
'
./helpers/mock-empty-route.helper
'
;
export
type
AdmitMeta
=
{
export
type
AdmitMeta
=
{
path
:
string
;
path
:
string
;
...
@@ -32,6 +41,15 @@ export class AdmitSharedPage extends BasePage {
...
@@ -32,6 +41,15 @@ export class AdmitSharedPage extends BasePage {
await
this
.
goto
(
this
.
meta
.
path
);
await
this
.
goto
(
this
.
meta
.
path
);
await
this
.
waitSpinGone
();
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
400
);
await
this
.
waitSettle
(
400
);
const
on404
=
await
this
.
page
.
getByText
(
/页面不存在|抱歉.*不存在/
)
.
first
()
.
isVisible
()
.
catch
(()
=>
false
);
if
(
on404
)
{
await
this
.
openFromMenu
();
return
;
}
await
this
.
assertUiReady
(
`打开
${
this
.
meta
.
path
}
`
);
await
this
.
assertUiReady
(
`打开
${
this
.
meta
.
path
}
`
);
expect
(
this
.
page
.
url
(),
'
storageState 失效回到登录页
'
).
not
.
toMatch
(
/
\/
login/
);
expect
(
this
.
page
.
url
(),
'
storageState 失效回到登录页
'
).
not
.
toMatch
(
/
\/
login/
);
}
}
...
@@ -46,7 +64,11 @@ export class AdmitSharedPage extends BasePage {
...
@@ -46,7 +64,11 @@ export class AdmitSharedPage extends BasePage {
.
evaluate
((
el
)
=>
el
.
classList
.
contains
(
'
ant-menu-submenu-open
'
))
.
evaluate
((
el
)
=>
el
.
classList
.
contains
(
'
ant-menu-submenu-open
'
))
.
catch
(()
=>
false
);
.
catch
(()
=>
false
);
if
(
!
opened
)
await
this
.
click
(
title
,
'
展开准入管理
'
);
if
(
!
opened
)
await
this
.
click
(
title
,
'
展开准入管理
'
);
await
this
.
click
(
this
.
locators
.
menuLeaf
(
this
.
meta
.
menu
),
`进入
${
this
.
meta
.
menu
}
`
);
await
this
.
waitSettle
(
300
);
const
leaf
=
this
.
locators
.
menuLeaf
(
this
.
meta
.
menu
);
await
leaf
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
expect
(
leaf
,
`侧栏菜单「
${
this
.
meta
.
menu
}
」`
).
toBeVisible
({
timeout
:
20
_000
});
await
this
.
click
(
leaf
,
`进入
${
this
.
meta
.
menu
}
`
);
await
this
.
page
.
waitForURL
((
url
)
=>
url
.
pathname
.
includes
(
this
.
meta
.
pathPart
),
{
timeout
:
20
_000
});
await
this
.
page
.
waitForURL
((
url
)
=>
url
.
pathname
.
includes
(
this
.
meta
.
pathPart
),
{
timeout
:
20
_000
});
await
this
.
waitSpinGone
();
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
400
);
await
this
.
waitSettle
(
400
);
...
@@ -58,8 +80,22 @@ export class AdmitSharedPage extends BasePage {
...
@@ -58,8 +80,22 @@ export class AdmitSharedPage extends BasePage {
for
(
const
part
of
this
.
meta
.
forbidden
??
[])
{
for
(
const
part
of
this
.
meta
.
forbidden
??
[])
{
expect
(
this
.
page
.
url
(),
`不应进入
${
part
}
`
).
not
.
toContain
(
part
);
expect
(
this
.
page
.
url
(),
`不应进入
${
part
}
`
).
not
.
toContain
(
part
);
}
}
await
expect
(
this
.
locators
.
button
(
'
查询
'
).
first
(),
'
查询按钮
'
).
toBeVisible
();
await
expect
(
this
.
locators
.
button
(
'
查询
'
).
first
(),
'
查询按钮
'
).
toBeVisible
({
timeout
:
20
_000
});
expect
((
await
this
.
headerTexts
()).
length
,
'
列表表头
'
).
toBeGreaterThan
(
0
);
await
this
.
locators
.
tableHeader
.
first
()
.
waitFor
({
state
:
'
attached
'
,
timeout
:
20
_000
})
.
catch
(()
=>
undefined
);
const
headers
=
await
this
.
headerTexts
();
expect
(
headers
.
length
,
'
列表表头
'
).
toBeGreaterThan
(
0
);
}
/** mock 反例:在 route 置空列表响应后再查询。 */
protected
async
runMockEmptyQuery
(
mockNeedle
:
string
,
fillAndQuery
:
()
=>
Promise
<
void
>
,
):
Promise
<
{
rows
:
number
;
total
:
number
;
emptyVisible
:
boolean
}
>
{
await
runWithMockEmptyListRoute
(
this
.
page
,
mockNeedle
,
fillAndQuery
);
return
this
.
snapshotList
();
}
}
button
(
name
:
string
,
root
?:
Locator
):
Locator
{
button
(
name
:
string
,
root
?:
Locator
):
Locator
{
...
@@ -84,8 +120,16 @@ export class AdmitSharedPage extends BasePage {
...
@@ -84,8 +120,16 @@ export class AdmitSharedPage extends BasePage {
textInput
(
label
:
string
,
root
?:
Locator
):
Locator
{
textInput
(
label
:
string
,
root
?:
Locator
):
Locator
{
const
scope
=
root
??
this
.
page
.
locator
(
'
body
'
);
const
scope
=
root
??
this
.
page
.
locator
(
'
body
'
);
const
byRole
=
scope
.
getByRole
(
'
textbox
'
,
{
name
:
label
}).
first
();
const
compact
=
label
.
replace
(
/
\s
/g
,
''
);
return
byRole
.
or
(
scope
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
locator
(
'
input
'
).
first
());
const
labelPattern
=
new
RegExp
(
compact
.
split
(
''
).
join
(
'
\\
s*
'
));
const
byRole
=
scope
.
getByRole
(
'
textbox
'
,
{
name
:
labelPattern
}).
first
();
const
byForm
=
scope
.
locator
(
'
.ant-form-item
'
)
.
filter
({
hasText
:
labelPattern
})
.
locator
(
'
input:not([type="hidden"])
'
)
.
first
();
const
byPlaceholder
=
scope
.
getByPlaceholder
(
labelPattern
).
first
();
return
byRole
.
or
(
byForm
).
or
(
byPlaceholder
);
}
}
formSelect
(
label
:
string
,
root
?:
Locator
):
Locator
{
formSelect
(
label
:
string
,
root
?:
Locator
):
Locator
{
...
@@ -93,14 +137,39 @@ export class AdmitSharedPage extends BasePage {
...
@@ -93,14 +137,39 @@ 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
();
}
}
async
ensureFiltersExpanded
():
Promise
<
void
>
{
const
expand
=
this
.
button
(
'
展开
'
).
first
();
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
expand
,
'
展开筛选
'
);
await
this
.
waitSettle
(
200
);
}
}
async
fillFilter
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
async
fillFilter
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
input
=
this
.
textInput
(
label
);
const
input
=
this
.
textInput
(
label
);
await
expect
(
input
,
`筛选「
${
label
}
」`
).
toBeVisible
();
await
expect
(
input
,
`筛选「
${
label
}
」`
).
toBeVisible
(
{
timeout
:
8
_000
}
);
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
}
}
/** 现网筛选项可能是「物料名称/物资名称」而非「产品名称」。 */
async
fillProductNameFilter
(
value
:
string
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
labels
=
[
'
产品名称
'
,
'
物资名称
'
,
'
物料名称
'
,
'
产品简称
'
];
for
(
const
label
of
labels
)
{
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
return
;
}
}
expect
(
false
,
'
mock/筛选:未找到产品或物料名称输入框
'
).
toBeTruthy
();
}
async
chooseSelect
(
label
:
string
,
optionText
:
string
,
root
?:
Locator
):
Promise
<
void
>
{
async
chooseSelect
(
label
:
string
,
optionText
:
string
,
root
?:
Locator
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
select
=
this
.
formSelect
(
label
,
root
);
const
select
=
this
.
formSelect
(
label
,
root
);
await
select
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
this
.
click
(
select
,
`打开
${
label
}
`
);
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
dropdown
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden)
'
).
last
();
const
dropdown
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden)
'
).
last
();
const
keyword
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
20
);
const
keyword
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
20
);
...
@@ -137,9 +206,33 @@ export class AdmitSharedPage extends BasePage {
...
@@ -137,9 +206,33 @@ export class AdmitSharedPage extends BasePage {
return
matched
?
Number
(
matched
[
1
])
:
null
;
return
matched
?
Number
(
matched
[
1
])
:
null
;
}
}
async
expectTabActive
(
name
:
string
):
Promise
<
void
>
{
const
tab
=
this
.
tab
(
name
);
await
expect
(
tab
,
`
${
name
}
Tab`
).
toBeVisible
({
timeout
:
15
_000
});
await
expect
.
poll
(
async
()
=>
{
const
cls
=
`
${(
await
tab
.
getAttribute
(
'
class
'
))
??
''
}
${
await
tab
.
evaluate
((
el
)
=>
{
const
host
=
(
el
as
HTMLElement
).
closest
(
'
.ant-tabs-tab, .ant-radio-button-wrapper, .ant-segmented-item
'
,
);
return
host
?.
className
??
''
;
})
.
catch
(()
=>
''
)}
`
;
const
aria
=
await
tab
.
getAttribute
(
'
aria-selected
'
);
return
/active|checked|selected/i
.
test
(
cls
)
||
aria
===
'
true
'
;
},
{
timeout
:
8
_000
},
)
.
toBeTruthy
();
}
async
clickTab
(
name
:
string
):
Promise
<
void
>
{
async
clickTab
(
name
:
string
):
Promise
<
void
>
{
const
tab
=
this
.
tab
(
name
);
await
expect
(
tab
,
`
${
name
}
Tab`
).
toBeVisible
({
timeout
:
15
_000
});
await
this
.
assertBeforeBusinessAction
(
`切换
${
name
}
`
);
await
this
.
assertBeforeBusinessAction
(
`切换
${
name
}
`
);
await
this
.
click
(
t
his
.
tab
(
name
)
,
`切换
${
name
}
`
);
await
this
.
click
(
t
ab
,
`切换
${
name
}
`
);
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
}
}
...
@@ -151,32 +244,51 @@ export class AdmitSharedPage extends BasePage {
...
@@ -151,32 +244,51 @@ export class AdmitSharedPage extends BasePage {
const
groups
=
this
.
page
.
locator
(
'
.ant-table-thead
'
);
const
groups
=
this
.
page
.
locator
(
'
.ant-table-thead
'
);
const
count
=
await
groups
.
count
();
const
count
=
await
groups
.
count
();
let
best
=
groups
.
first
().
locator
(
'
th
'
);
let
best
=
groups
.
first
().
locator
(
'
th
'
);
let
best
Len
=
0
;
let
best
Score
=
-
1
;
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
cells
=
groups
.
nth
(
i
).
locator
(
'
th
'
);
const
group
=
groups
.
nth
(
i
);
const
len
=
await
cells
.
count
();
const
insidePut
=
await
group
.
evaluate
((
el
)
=>
Boolean
(
el
.
closest
(
'
.put-page
'
))).
catch
(()
=>
false
);
if
(
len
>
bestLen
)
{
if
(
insidePut
)
continue
;
bestLen
=
len
;
const
cells
=
group
.
locator
(
'
th
'
);
const
labels
=
await
dataHeaderLabelsFromCells
(
cells
);
const
named
=
labels
.
filter
((
l
)
=>
l
.
length
>
0
).
length
;
const
score
=
named
*
100
+
labels
.
length
;
if
(
score
>
bestScore
)
{
bestScore
=
score
;
best
=
cells
;
best
=
cells
;
}
}
}
}
return
best
;
return
best
;
}
}
/** 与 tbody 数据列对齐的表头(排除选择列/展开列)。 */
async
dataHeaderLabels
():
Promise
<
string
[]
>
{
return
dataHeaderLabelsFromCells
(
await
this
.
headerCells
());
}
async
headerTexts
():
Promise
<
string
[]
>
{
async
headerTexts
():
Promise
<
string
[]
>
{
return
(
await
(
await
this
.
headerCells
()).
allTextContents
()).
map
((
item
)
=>
item
.
replace
(
/
\s
/g
,
''
)
);
return
this
.
dataHeaderLabels
(
);
}
}
async
expectHeaders
(
names
:
string
[]):
Promise
<
void
>
{
async
expectHeaders
(
names
:
string
[]):
Promise
<
void
>
{
const
text
=
(
await
this
.
headerTexts
()).
join
(
'
'
);
const
texts
=
await
this
.
dataHeaderLabels
();
const
joined
=
texts
.
join
(
''
);
for
(
const
name
of
names
)
{
for
(
const
name
of
names
)
{
expect
(
text
,
`表头应含「
${
name
}
」`
).
toContain
(
name
.
replace
(
/
\s
/g
,
''
));
if
(
name
===
'
操作
'
)
{
const
opHeader
=
texts
.
some
((
h
)
=>
h
.
includes
(
'
操作
'
));
const
rows
=
await
this
.
listRows
();
const
opBtn
=
(
await
rows
.
first
().
getByRole
(
'
button
'
).
count
().
catch
(()
=>
0
))
>
0
;
expect
(
opHeader
||
opBtn
||
joined
.
includes
(
'
操作
'
),
`表头应含「
${
name
}
」(或别名)`
).
toBeTruthy
();
continue
;
}
const
hit
=
texts
.
some
((
header
)
=>
columnHeaderTextsMatch
(
header
,
name
));
expect
(
hit
,
`表头应含「
${
name
}
」(或别名)`
).
toBeTruthy
();
}
}
}
}
async
headerIndex
(
name
:
string
):
Promise
<
number
>
{
async
headerIndex
(
name
:
string
):
Promise
<
number
>
{
const
texts
=
await
this
.
headerText
s
();
const
texts
=
await
this
.
dataHeaderLabel
s
();
const
index
=
texts
.
findIndex
((
item
)
=>
item
.
includes
(
name
.
replace
(
/
\s
/g
,
''
))
);
const
index
=
resolveColumnIndexFromLabels
(
texts
,
name
);
expect
(
index
,
`找不到列「
${
name
}
」`
).
toBeGreaterThanOrEqual
(
0
);
expect
(
index
,
`找不到列「
${
name
}
」`
).
toBeGreaterThanOrEqual
(
0
);
return
index
;
return
index
;
}
}
...
@@ -217,8 +329,8 @@ export class AdmitSharedPage extends BasePage {
...
@@ -217,8 +329,8 @@ export class AdmitSharedPage extends BasePage {
}
}
async
cellText
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
async
cellText
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
const
index
=
await
this
.
headerIndex
(
header
);
const
labels
=
await
this
.
dataHeaderLabels
(
);
return
((
await
row
.
locator
(
'
td
'
).
nth
(
index
).
innerText
())
??
''
).
trim
(
);
return
readRowCellByColumn
(
row
,
labels
,
header
);
}
}
async
tryCell
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
async
tryCell
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
...
@@ -235,6 +347,24 @@ export class AdmitSharedPage extends BasePage {
...
@@ -235,6 +347,24 @@ export class AdmitSharedPage extends BasePage {
return
row
;
return
row
;
}
}
/** 首条可勾选行(跳过 checkbox 禁用的行)。 */
async
firstCheckableRow
():
Promise
<
Locator
>
{
const
rows
=
await
this
.
listRows
();
const
count
=
Math
.
min
(
await
rows
.
count
(),
30
);
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
row
=
rows
.
nth
(
i
);
const
wrapper
=
row
.
locator
(
'
.ant-checkbox-wrapper
'
).
first
();
if
((
await
wrapper
.
count
())
===
0
)
continue
;
const
cls
=
((
await
wrapper
.
getAttribute
(
'
class
'
))
??
''
).
toLowerCase
();
if
(
cls
.
includes
(
'
disabled
'
))
continue
;
const
input
=
wrapper
.
locator
(
'
input.ant-checkbox-input
'
);
if
((
await
input
.
count
())
>
0
&&
(
await
input
.
isDisabled
().
catch
(()
=>
true
)))
continue
;
return
row
;
}
await
this
.
skipWhenDataInsufficient
(
'
列表无可用勾选行
'
);
return
rows
.
first
();
}
/** 列表快照供用例层断言(不在此 expect)。 */
/** 列表快照供用例层断言(不在此 expect)。 */
async
snapshotList
():
Promise
<
{
rows
:
number
;
total
:
number
;
emptyVisible
:
boolean
}
>
{
async
snapshotList
():
Promise
<
{
rows
:
number
;
total
:
number
;
emptyVisible
:
boolean
}
>
{
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
...
@@ -289,12 +419,41 @@ export class AdmitSharedPage extends BasePage {
...
@@ -289,12 +419,41 @@ export class AdmitSharedPage extends BasePage {
}
}
}
}
/** 表头缺失或列名对不齐时,断言前几行任意列包含关键字(物料单号筛选等)。 */
async
expectRowsContainKeyword
(
keyword
:
string
):
Promise
<
void
>
{
const
needle
=
keyword
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
24
);
const
rows
=
await
this
.
listRows
();
const
count
=
await
rows
.
count
();
expect
(
count
,
'
查询应有结果
'
).
toBeGreaterThan
(
0
);
const
limit
=
Math
.
min
(
count
,
5
);
let
hit
=
false
;
for
(
let
i
=
0
;
i
<
limit
;
i
+=
1
)
{
const
cells
=
await
dataCellTextsFromRow
(
rows
.
nth
(
i
));
const
joined
=
cells
.
join
(
'
'
).
replace
(
/
\s
/g
,
''
);
if
(
joined
.
includes
(
needle
))
hit
=
true
;
}
expect
(
hit
,
`前
${
limit
}
行至少一行应包含关键字`
).
toBeTruthy
();
}
/** 物料准入单列表:表头常被折叠为空时,单号多在第 3 个数据列。 */
async
sampleMaterialOrderNo
():
Promise
<
string
>
{
const
row
=
await
this
.
firstRow
();
const
fromHeader
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
准入单号
'
));
if
(
fromHeader
)
return
fromHeader
;
const
cells
=
await
dataCellTextsFromRow
(
row
);
const
dataCells
=
cells
.
filter
((
c
)
=>
c
&&
c
!==
'
--
'
&&
c
!==
'
-
'
);
const
orderLike
=
dataCells
.
find
((
c
)
=>
/^
(
CP|ZR|AD|MIT
)
/i
.
test
(
c
.
replace
(
/
\s
/g
,
''
)));
if
(
orderLike
)
return
orderLike
.
trim
();
const
indexGuess
=
normalizeSampleCellValue
(
cells
[
2
]
??
''
);
return
indexGuess
;
}
async
sampleCell
(
header
:
string
):
Promise
<
string
>
{
async
sampleCell
(
header
:
string
):
Promise
<
string
>
{
const
rows
=
await
this
.
listRows
();
const
rows
=
await
this
.
listRows
();
const
count
=
Math
.
min
(
await
rows
.
count
(),
8
);
const
count
=
Math
.
min
(
await
rows
.
count
(),
8
);
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
text
=
(
await
this
.
tryCell
(
rows
.
nth
(
i
),
header
))
.
replace
(
/
\s
/g
,
''
)
;
const
text
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
rows
.
nth
(
i
),
header
));
if
(
text
&&
text
!==
'
--
'
&&
text
!==
'
-
'
)
return
text
;
if
(
text
)
return
text
;
}
}
await
this
.
skipWhenDataInsufficient
(
`首屏「
${
header
}
」为空`
);
await
this
.
skipWhenDataInsufficient
(
`首屏「
${
header
}
」为空`
);
return
''
;
return
''
;
...
@@ -304,21 +463,37 @@ export class AdmitSharedPage extends BasePage {
...
@@ -304,21 +463,37 @@ export class AdmitSharedPage extends BasePage {
async
queryByFirstAvailable
(
async
queryByFirstAvailable
(
pairs
:
Array
<
{
column
:
string
;
filter
:
string
;
kind
:
'
input
'
|
'
select
'
}
>
,
pairs
:
Array
<
{
column
:
string
;
filter
:
string
;
kind
:
'
input
'
|
'
select
'
}
>
,
):
Promise
<
void
>
{
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
row
=
await
this
.
firstRow
();
const
row
=
await
this
.
firstRow
();
for
(
const
pair
of
pairs
)
{
for
(
const
pair
of
pairs
)
{
const
value
=
(
await
this
.
tryCell
(
row
,
pair
.
column
)).
trim
();
const
value
=
(
await
this
.
tryCell
(
row
,
pair
.
column
)).
trim
();
if
(
!
value
||
value
===
'
--
'
||
value
===
'
-
'
)
continue
;
if
(
!
value
||
value
===
'
--
'
||
value
===
'
-
'
)
continue
;
if
(
pair
.
kind
===
'
input
'
&&
(
await
this
.
textInput
(
pair
.
filter
).
isVisible
().
catch
(()
=>
false
)))
{
const
filterLabels
=
[
await
this
.
fillFilter
(
pair
.
filter
,
value
.
slice
(
0
,
40
));
pair
.
filter
,
await
this
.
query
();
...
columnHeaderMatchTokens
(
pair
.
filter
),
await
this
.
expectRowsContain
(
pair
.
column
,
value
);
...
columnHeaderMatchTokens
(
pair
.
column
),
return
;
].
filter
((
label
,
index
,
arr
)
=>
label
&&
arr
.
indexOf
(
label
)
===
index
);
if
(
pair
.
kind
===
'
input
'
)
{
for
(
const
label
of
filterLabels
)
{
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fillFilter
(
label
,
value
.
slice
(
0
,
40
));
await
this
.
query
();
await
this
.
expectRowsContain
(
pair
.
column
,
value
);
return
;
}
}
}
}
if
(
pair
.
kind
===
'
select
'
&&
(
await
this
.
formSelect
(
pair
.
filter
).
isVisible
().
catch
(()
=>
false
)))
{
if
(
pair
.
kind
===
'
select
'
)
{
await
this
.
chooseSelect
(
pair
.
filter
,
value
);
for
(
const
label
of
filterLabels
)
{
await
this
.
query
();
const
select
=
this
.
formSelect
(
label
);
await
this
.
expectRowsContain
(
pair
.
column
,
value
);
if
(
await
select
.
isVisible
().
catch
(()
=>
false
))
{
return
;
await
this
.
chooseSelect
(
label
,
value
);
await
this
.
query
();
await
this
.
expectRowsContain
(
pair
.
column
,
value
);
return
;
}
}
}
}
}
}
await
this
.
guardedSkip
(
'
筛选区没有与首行样本对应的条件
'
);
await
this
.
guardedSkip
(
'
筛选区没有与首行样本对应的条件
'
);
...
@@ -469,8 +644,11 @@ export class AdmitSharedPage extends BasePage {
...
@@ -469,8 +644,11 @@ export class AdmitSharedPage extends BasePage {
}
}
async
expectBlockedAndStay
():
Promise
<
void
>
{
async
expectBlockedAndStay
():
Promise
<
void
>
{
const
hint
=
this
.
page
.
getByText
(
/必填|请输入|请选择|不能为空|不可为空|请先/
).
first
();
const
hint
=
this
.
page
.
getByText
(
/必填|请输入|请选择|不能为空|不可为空|请先/
);
await
expect
(
this
.
locators
.
formError
.
first
().
or
(
hint
),
'
应出现必填或非法提示
'
).
toBeVisible
({
timeout
:
8
_000
});
const
errorHint
=
this
.
locators
.
formError
.
or
(
hint
).
first
();
await
expect
(
errorHint
,
'
应出现必填或非法提示
'
).
toBeVisible
({
timeout
:
8
_000
,
});
const
stayed
=
const
stayed
=
(
await
this
.
page
.
locator
(
'
.put-page:visible
'
).
count
())
>
0
||
(
await
this
.
page
.
locator
(
'
.put-page:visible
'
).
count
())
>
0
||
(
await
this
.
page
.
locator
(
'
.ant-modal:visible
'
).
count
())
>
0
;
(
await
this
.
page
.
locator
(
'
.ant-modal:visible
'
).
count
())
>
0
;
...
@@ -487,9 +665,30 @@ export class AdmitSharedPage extends BasePage {
...
@@ -487,9 +665,30 @@ export class AdmitSharedPage extends BasePage {
}
}
async
checkFirstRow
():
Promise
<
Locator
>
{
async
checkFirstRow
():
Promise
<
Locator
>
{
const
row
=
await
this
.
firstRow
();
const
row
=
await
this
.
firstCheckableRow
();
const
box
=
row
.
locator
(
'
.ant-checkbox
'
).
first
();
await
row
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
this
.
click
(
box
,
'
勾选首行
'
);
const
input
=
row
.
locator
(
'
input.ant-checkbox-input
'
).
first
();
const
wrapper
=
row
.
locator
(
'
.ant-checkbox-wrapper
'
).
first
();
if
((
await
wrapper
.
count
())
>
0
)
{
await
this
.
click
(
wrapper
,
'
勾选首行
'
);
}
else
if
((
await
input
.
count
())
>
0
)
{
await
this
.
click
(
input
.
locator
(
'
xpath=ancestor::label[1]
'
).
or
(
input
),
'
勾选首行
'
);
}
else
{
await
this
.
click
(
row
.
locator
(
'
.ant-checkbox
'
).
first
(),
'
勾选首行
'
);
}
if
((
await
input
.
count
())
>
0
)
{
await
expect
.
poll
(
async
()
=>
input
.
isChecked
().
catch
(()
=>
false
),
{
timeout
:
8
_000
})
.
toBe
(
true
);
}
else
{
await
expect
.
poll
(
async
()
=>
{
const
cls
=
(
await
row
.
getAttribute
(
'
class
'
))
??
''
;
return
cls
.
includes
(
'
ant-table-row-selected
'
);
},
{
timeout
:
8
_000
})
.
toBe
(
true
);
}
await
this
.
waitSettle
(
150
);
return
row
;
return
row
;
}
}
...
@@ -536,6 +735,25 @@ export class AdmitSharedPage extends BasePage {
...
@@ -536,6 +735,25 @@ export class AdmitSharedPage extends BasePage {
return
this
.
textInput
(
label
,
this
.
put
());
return
this
.
textInput
(
label
,
this
.
put
());
}
}
/** 详情/编辑层:产品名称字段现网常显示为「物料名称」。 */
putProductNameInput
():
Locator
{
return
this
.
putInput
(
'
产品名称
'
).
or
(
this
.
putInput
(
'
物料名称
'
)).
first
();
}
async
expectPutFieldLabels
(...
labels
:
string
[]):
Promise
<
void
>
{
for
(
const
label
of
labels
)
{
const
tokens
=
columnHeaderMatchTokens
(
label
);
let
hit
=
false
;
for
(
const
token
of
tokens
)
{
if
(
await
this
.
put
().
getByText
(
new
RegExp
(
token
)).
first
().
isVisible
().
catch
(()
=>
false
))
{
hit
=
true
;
break
;
}
}
expect
(
hit
,
`
${
label
}
应在编辑层展示(或别名)`
).
toBeTruthy
();
}
}
async
saveOnPut
():
Promise
<
void
>
{
async
saveOnPut
():
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
'
保存
'
);
await
this
.
assertBeforeBusinessAction
(
'
保存
'
);
const
save
=
this
.
button
(
'
保存
'
,
this
.
put
()).
first
();
const
save
=
this
.
button
(
'
保存
'
,
this
.
put
()).
first
();
...
...
pages/spd/admit-supply.page.ts
View file @
ed57ab23
...
@@ -35,13 +35,15 @@ export class AdmitSupplyPage extends AdmitSharedPage {
...
@@ -35,13 +35,15 @@ export class AdmitSupplyPage extends AdmitSharedPage {
}
}
async
mockEmpty
()
{
async
mockEmpty
()
{
if
(
await
this
.
textInput
(
'
变更单号
'
).
isVisible
().
catch
(()
=>
false
))
{
const
needle
=
admitData
.
mockOrder
;
await
this
.
fillFilter
(
'
变更单号
'
,
admitData
.
mockOrder
);
return
this
.
runMockEmptyQuery
(
needle
,
async
()
=>
{
}
else
{
if
(
await
this
.
textInput
(
'
变更单号
'
).
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fillFilter
(
'
产品名称
'
,
admitData
.
mockGoods
);
await
this
.
fillFilter
(
'
变更单号
'
,
admitData
.
mockOrder
);
}
}
else
{
await
this
.
query
();
await
this
.
fillProductNameFilter
(
admitData
.
mockGoods
);
return
this
.
snapshotList
();
}
await
this
.
query
();
});
}
}
async
coreHeaders
():
Promise
<
string
[]
>
{
async
coreHeaders
():
Promise
<
string
[]
>
{
...
@@ -51,7 +53,7 @@ export class AdmitSupplyPage extends AdmitSharedPage {
...
@@ -51,7 +53,7 @@ export class AdmitSupplyPage extends AdmitSharedPage {
async
switchPending
():
Promise
<
void
>
{
async
switchPending
():
Promise
<
void
>
{
await
this
.
clickTabAndMatchCount
(
'
待提交
'
);
await
this
.
clickTabAndMatchCount
(
'
待提交
'
);
await
expect
(
this
.
tab
(
'
待提交
'
)).
toHaveClass
(
/ant-tabs-tab-active/
);
await
this
.
expectTabActive
(
'
待提交
'
);
}
}
async
openAddFields
():
Promise
<
void
>
{
async
openAddFields
():
Promise
<
void
>
{
...
...
pages/spd/admit-task.page.ts
View file @
ed57ab23
...
@@ -21,7 +21,7 @@ export class AdmitTaskPage extends AdmitSharedPage {
...
@@ -21,7 +21,7 @@ export class AdmitTaskPage extends AdmitSharedPage {
async
filterByProductName
():
Promise
<
void
>
{
async
filterByProductName
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
40
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
40
));
await
this
.
query
();
await
this
.
query
();
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
await
this
.
expectRowsContain
(
'
产品名称
'
,
name
);
}
}
...
@@ -30,22 +30,23 @@ export class AdmitTaskPage extends AdmitSharedPage {
...
@@ -30,22 +30,23 @@ export class AdmitTaskPage extends AdmitSharedPage {
await
this
.
clickTabAndMatchCount
(
'
待分类
'
);
await
this
.
clickTabAndMatchCount
(
'
待分类
'
);
await
this
.
clickTabAndMatchCount
(
'
已分类
'
);
await
this
.
clickTabAndMatchCount
(
'
已分类
'
);
await
this
.
clickTab
(
'
待分类
'
);
await
this
.
clickTab
(
'
待分类
'
);
await
expect
(
this
.
tab
(
'
待分类
'
)).
toHaveClass
(
/ant-tabs-tab-active/
);
await
this
.
expectTabActive
(
'
待分类
'
);
}
}
async
resetFilters
():
Promise
<
void
>
{
async
resetFilters
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
const
name
=
await
this
.
sampleCell
(
'
产品名称
'
);
await
this
.
fill
Filter
(
'
产品名称
'
,
name
.
slice
(
0
,
20
));
await
this
.
fill
ProductNameFilter
(
name
.
slice
(
0
,
20
));
await
this
.
reset
();
await
this
.
reset
();
await
this
.
expectInputCleared
(
'
产品名称
'
);
await
this
.
expectInputCleared
(
'
产品名称
'
);
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
}
}
async
mockEmpty
()
{
async
mockEmpty
()
{
await
this
.
fillFilter
(
'
产品名称
'
,
admitData
.
mockGoods
);
return
this
.
runMockEmptyQuery
(
admitData
.
mockGoods
,
async
()
=>
{
await
this
.
query
();
await
this
.
fillProductNameFilter
(
admitData
.
mockGoods
);
return
this
.
snapshotList
();
await
this
.
query
();
});
}
}
async
coreHeaders
():
Promise
<
string
[]
>
{
async
coreHeaders
():
Promise
<
string
[]
>
{
...
...
pages/spd/basic-role.page.ts
View file @
ed57ab23
...
@@ -24,34 +24,27 @@ export class BasicRolePage extends BasicUserSharedPage {
...
@@ -24,34 +24,27 @@ export class BasicRolePage extends BasicUserSharedPage {
}
}
leftRoleItems
():
Locator
{
leftRoleItems
():
Locator
{
return
this
.
page
.
locator
(
return
this
.
locators
.
roleSider
.
getByRole
(
'
listitem
'
);
'
.ant-layout-sider .ant-table-tbody tr.ant-table-row, .ant-layout-sider .ant-list-item, .ant-layout-sider .ant-menu-item, [class*="role"] .ant-table-row
'
,
);
}
}
async
leftRoleNames
():
Promise
<
string
[]
>
{
async
leftRoleNames
():
Promise
<
string
[]
>
{
const
items
=
this
.
leftRoleItems
();
const
items
=
this
.
leftRoleItems
();
await
items
.
first
().
waitFor
({
state
:
'
visible
'
,
timeout
:
15
_000
}).
catch
(()
=>
undefined
);
const
count
=
await
items
.
count
();
const
count
=
await
items
.
count
();
if
(
count
>
0
)
{
const
names
:
string
[]
=
[];
const
names
:
string
[]
=
[];
for
(
let
i
=
0
;
i
<
Math
.
min
(
count
,
40
);
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
Math
.
min
(
count
,
30
);
i
+=
1
)
{
const
text
=
((
await
items
.
nth
(
i
).
innerText
())
??
''
).
split
(
'
\n
'
)[
0
].
trim
();
const
text
=
((
await
items
.
nth
(
i
).
innerText
())
??
''
).
split
(
'
\n
'
)[
0
].
trim
();
if
(
text
&&
text
!==
'
角色
'
)
names
.
push
(
text
);
if
(
text
)
names
.
push
(
text
);
}
if
(
names
.
length
)
return
names
;
}
const
fallback
=
this
.
page
.
locator
(
'
.ant-table-tbody tr.ant-table-row td
'
).
first
();
if
(
await
fallback
.
isVisible
().
catch
(()
=>
false
))
{
return
(
await
this
.
page
.
locator
(
'
.ant-table-tbody tr.ant-table-row td:first-child
'
).
allTextContents
())
.
map
((
t
)
=>
t
.
trim
())
.
filter
(
Boolean
);
}
}
return
[]
;
return
names
;
}
}
async
sampleRoleName
():
Promise
<
string
>
{
async
sampleRoleName
():
Promise
<
string
>
{
const
names
=
await
this
.
leftRoleNames
();
const
names
=
await
this
.
leftRoleNames
();
const
found
=
names
.
find
((
n
)
=>
n
&&
n
!==
'
角色
'
);
const
found
=
names
.
find
((
n
)
=>
/管理员|库管|护士/
.
test
(
n
))
??
names
.
find
((
n
)
=>
n
.
length
>=
2
&&
!
/^
\d
+$/
.
test
(
n
))
??
names
[
0
];
if
(
!
found
)
{
if
(
!
found
)
{
await
this
.
skipWhenDataInsufficient
(
'
左栏无角色
'
);
await
this
.
skipWhenDataInsufficient
(
'
左栏无角色
'
);
return
''
;
return
''
;
...
@@ -60,12 +53,7 @@ export class BasicRolePage extends BasicUserSharedPage {
...
@@ -60,12 +53,7 @@ export class BasicRolePage extends BasicUserSharedPage {
}
}
leftRoleListScrollHost
():
Locator
{
leftRoleListScrollHost
():
Locator
{
const
sider
=
this
.
locators
.
roleSider
;
return
this
.
locators
.
roleSider
;
return
sider
.
locator
(
'
.ant-table-body
'
)
.
first
()
.
or
(
sider
.
locator
(
'
.rc-virtual-list-holder
'
).
first
())
.
or
(
sider
.
locator
(
'
.ant-list
'
).
first
());
}
}
async
assertLeftRoleListVerticalScroll
():
Promise
<
void
>
{
async
assertLeftRoleListVerticalScroll
():
Promise
<
void
>
{
...
@@ -164,12 +152,9 @@ export class BasicRolePage extends BasicUserSharedPage {
...
@@ -164,12 +152,9 @@ export class BasicRolePage extends BasicUserSharedPage {
}
}
async
selectRole
(
name
:
string
):
Promise
<
void
>
{
async
selectRole
(
name
:
string
):
Promise
<
void
>
{
const
item
=
this
.
leftRoleItems
().
filter
({
hasText
:
name
}).
first
();
const
item
=
this
.
leftRoleItems
().
filter
({
hasText
:
new
RegExp
(
`^\\s*
${
name
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
)}
\\s*$`
)
}).
first
();
if
(
await
item
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
item
,
`选中角色
${
name
}
`
);
await
this
.
click
(
item
,
`选中角色
${
name
}
`
);
await
expect
(
this
.
locators
.
roleTree
,
'
选中角色后权限树应可见
'
).
toBeVisible
({
timeout
:
15
_000
});
}
else
{
await
this
.
click
(
this
.
page
.
getByText
(
name
,
{
exact
:
true
}).
first
(),
`选中角色
${
name
}
`
);
}
await
this
.
waitSpinGone
();
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
400
);
await
this
.
waitSettle
(
400
);
}
}
...
@@ -292,10 +277,7 @@ export class BasicRolePage extends BasicUserSharedPage {
...
@@ -292,10 +277,7 @@ export class BasicRolePage extends BasicUserSharedPage {
editEntry
(
roleName
:
string
):
Locator
{
editEntry
(
roleName
:
string
):
Locator
{
const
row
=
this
.
leftRoleItems
().
filter
({
hasText
:
roleName
}).
first
();
const
row
=
this
.
leftRoleItems
().
filter
({
hasText
:
roleName
}).
first
();
return
row
return
row
.
locator
(
'
img[alt="form"], .anticon-form, .anticon-edit
'
).
first
();
.
getByRole
(
'
button
'
,
{
name
:
/编
\s
*辑/
})
.
or
(
row
.
locator
(
'
.anticon-edit, .anticon-form, [aria-label*="edit"], [aria-label*="编辑"]
'
))
.
first
();
}
}
normalizeRoleLabel
(
text
:
string
):
string
{
normalizeRoleLabel
(
text
:
string
):
string
{
...
@@ -397,7 +379,10 @@ export class BasicRolePage extends BasicUserSharedPage {
...
@@ -397,7 +379,10 @@ export class BasicRolePage extends BasicUserSharedPage {
}
}
addRoleDialog
():
Locator
{
addRoleDialog
():
Locator
{
return
this
.
dialog
().
or
(
this
.
page
.
locator
(
'
.ant-modal:visible, .ant-drawer-open
'
)).
last
();
return
this
.
page
.
locator
(
'
.ant-modal, .ant-drawer-content
'
)
.
filter
({
has
:
this
.
page
.
getByText
(
/新
\s
*增
\s
*角
\s
*色/
)
})
.
last
();
}
}
async
openAddRoleDialog
():
Promise
<
Locator
>
{
async
openAddRoleDialog
():
Promise
<
Locator
>
{
...
@@ -425,20 +410,28 @@ export class BasicRolePage extends BasicUserSharedPage {
...
@@ -425,20 +410,28 @@ export class BasicRolePage extends BasicUserSharedPage {
addRoleDialogCloseIcon
():
Locator
{
addRoleDialogCloseIcon
():
Locator
{
const
dialog
=
this
.
addRoleDialog
();
const
dialog
=
this
.
addRoleDialog
();
return
dialog
return
dialog
.
locator
(
'
button.ant-modal-close, .ant-drawer-close
'
).
first
();
.
locator
(
'
.ant-modal-close
'
)
}
.
first
()
.
or
(
dialog
.
getByRole
(
'
button
'
,
{
name
:
/close|关闭/i
}).
first
())
addRoleDialogWrap
():
Locator
{
.
or
(
dialog
.
getByRole
(
'
img
'
,
{
name
:
'
close
'
}).
fir
st
()
)
;
return
this
.
page
.
locator
(
'
.ant-modal-wrap
'
).
filter
({
hasText
:
/新
\s
*增
\s
*角
\s
*色/
}).
la
st
();
}
}
async
closeAddRoleDialogByX
():
Promise
<
void
>
{
async
closeAddRoleDialogByX
():
Promise
<
void
>
{
const
dialog
=
this
.
addRoleDialog
();
const
wrap
=
this
.
addRoleDialog
Wrap
();
await
expect
(
dialog
,
'
新增弹框应可见
'
).
toBeVisible
();
await
expect
(
wrap
,
'
新增弹框应可见
'
).
toBeVisible
();
const
close
=
this
.
addRoleDialogCloseIcon
();
const
close
=
wrap
.
locator
(
'
button.ant-modal-close
'
).
first
();
await
expect
(
close
,
'
右上角 X 关闭
'
).
toBeVisible
();
await
expect
(
close
,
'
右上角 X 关闭
'
).
toBeVisible
();
await
this
.
click
(
close
,
'
点击新增弹框 X 关闭
'
);
await
this
.
click
(
close
,
'
点击新增弹框 X 关闭
'
);
await
expect
(
dialog
,
'
X 关闭后弹框应隐藏
'
).
toBeHidden
({
timeout
:
8
_000
});
if
(
await
wrap
.
isVisible
().
catch
(()
=>
false
))
{
await
close
.
evaluate
((
node
)
=>
{
if
(
node
instanceof
HTMLElement
)
node
.
click
();
});
}
if
(
await
wrap
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
}
await
expect
(
wrap
,
'
X 关闭后弹框应隐藏
'
).
toBeHidden
({
timeout
:
8
_000
});
expect
(
await
this
.
page
.
locator
(
'
.ant-message-error
'
).
count
(),
'
不应出现错误提示
'
).
toBe
(
0
);
expect
(
await
this
.
page
.
locator
(
'
.ant-message-error
'
).
count
(),
'
不应出现错误提示
'
).
toBe
(
0
);
await
this
.
assertUiReady
(
'
X 关闭新增角色弹框后
'
);
await
this
.
assertUiReady
(
'
X 关闭新增角色弹框后
'
);
}
}
...
...
pages/spd/basic-user.page.ts
View file @
ed57ab23
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
userData
,
uniqLogin
}
from
'
../../data/user.data
'
;
import
{
userData
,
uniqLogin
}
from
'
../../data/user.data
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
BasicUserLocators
}
from
'
../../locators/spd/basic-user.locator
'
;
import
{
BasicUserLocators
}
from
'
../../locators/spd/basic-user.locator
'
;
import
{
BasicUserSharedPage
,
type
UserMeta
}
from
'
./basic-user-shared.page
'
;
import
{
BasicUserSharedPage
,
type
UserMeta
}
from
'
./basic-user-shared.page
'
;
import
{
import
{
...
@@ -8,6 +9,7 @@ import {
...
@@ -8,6 +9,7 @@ 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,9 +245,17 @@ export class BasicUserPage extends BasicUserSharedPage {
...
@@ -243,9 +245,17 @@ export class BasicUserPage extends BasicUserSharedPage {
async
clickAuthAddRow
():
Promise
<
void
>
{
async
clickAuthAddRow
():
Promise
<
void
>
{
const
layer
=
this
.
userRoleAuthLayer
();
const
layer
=
this
.
userRoleAuthLayer
();
const
add
=
layer
.
getByRole
(
'
button
'
,
{
name
:
/^添
\s
*加$/
}).
first
();
const
add
=
layer
.
getByRole
(
'
button
'
,
{
name
:
/^添
\s
*加$/
})
.
or
(
layer
.
getByRole
(
'
button
'
,
{
name
:
/新
\s
*增/
}))
.
or
(
layer
.
locator
(
'
button
'
).
filter
({
has
:
layer
.
locator
(
'
.anticon-plus
'
)
}))
.
first
();
if
(
!
(
await
add
.
isVisible
().
catch
(()
=>
false
)))
{
if
(
!
(
await
add
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
授权弹框无「添加」按钮(可能为权限树模式)
'
);
await
failWhen
(
this
.
browserPage
,
true
,
'
授权弹框无「添加/新增」按钮,请核对页面是否为表格授权模式
'
,
);
}
}
await
this
.
click
(
add
,
'
添加
'
);
await
this
.
click
(
add
,
'
添加
'
);
await
this
.
waitSettle
(
400
);
await
this
.
waitSettle
(
400
);
...
@@ -276,7 +286,15 @@ export class BasicUserPage extends BasicUserSharedPage {
...
@@ -276,7 +286,15 @@ export class BasicUserPage extends BasicUserSharedPage {
}
}
private
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
private
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
candidates
=
[
this
.
locators
.
columnSettingsTrigger
];
const
triggers
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
triggerCount
=
await
triggers
.
count
();
const
candidates
:
Locator
[]
=
[];
for
(
let
i
=
0
;
i
<
Math
.
min
(
triggerCount
,
4
);
i
+=
1
)
{
candidates
.
push
(
triggers
.
nth
(
i
));
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
locators
.
columnSettingsTrigger
);
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
if
(
!
trigger
)
{
await
this
.
locators
.
tableWrapper
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
this
.
locators
.
tableWrapper
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
...
...
Prev
1
2
3
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