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
79a08ff4
Commit
79a08ff4
authored
Sep 18, 2026
by
liguangyu06
Browse files
修复报表表头 strict mode、列设置关面板超时及汇总/排序定位,避免夜间全量被脚本误杀。
parent
35fe36af
Changes
87
Hide whitespace changes
Inline
Side-by-side
core/column-header-match.util.ts
View file @
79a08ff4
...
...
@@ -6,7 +6,6 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
院区名称
'
,
'
院区
'
],
[
'
27位医保编码
'
,
'
医保编码
'
,
'
医保码
'
],
[
'
医保单件名称
'
,
'
单件名称
'
,
'
医保名称
'
],
[
'
数量
'
,
'
净入数量
'
,
'
净出数量
'
,
'
入库数量
'
,
'
出库数量
'
,
'
成本数量
'
],
[
'
HIS编码
'
,
'
HIS码
'
,
'
His编码
'
],
[
'
结算单标签
'
,
'
标签
'
],
[
'
产品编号
'
,
'
产品编码
'
,
'
物资编码
'
,
'
商品编码
'
],
...
...
@@ -18,10 +17,7 @@ function aliasTokensFor(columnName: string): string[] {
const
normalized
=
columnName
.
replace
(
/
\s
+/g
,
''
).
trim
();
const
out
:
string
[]
=
[];
for
(
const
group
of
COLUMN_HEADER_ALIAS_GROUPS
)
{
const
hit
=
group
.
some
((
alias
)
=>
{
const
a
=
alias
.
replace
(
/
\s
+/g
,
''
);
return
a
===
normalized
||
a
.
includes
(
normalized
)
||
normalized
.
includes
(
a
);
});
const
hit
=
group
.
some
((
alias
)
=>
alias
.
replace
(
/
\s
+/g
,
''
)
===
normalized
);
if
(
hit
)
{
for
(
const
alias
of
group
)
{
const
a
=
alias
.
replace
(
/
\s
+/g
,
''
).
trim
();
...
...
data/ana-chg.data.ts
View file @
79a08ff4
...
...
@@ -110,6 +110,15 @@ export const anaChgData: CenterReportPageData & {
comboSamplePickKeys
:
[
'
field
'
,
'
productName
'
,
'
spec
'
,
'
supplierName
'
,
'
field2
'
,
'
consumableType
'
],
comboSampleMaxRowScan
:
5
,
drawerFields
:
anaChgComboDrawerFields
,
firstRowSummaryMatch
:
{
metrics
:
[
{
key
:
'
field8
'
,
label
:
'
选中总数量
'
,
summaryLabels
:
[
'
选中总数量
'
]
},
{
key
:
'
field9
'
,
label
:
'
选中总差额
'
,
summaryLabels
:
[
'
选中总差额
'
]
},
],
requiredKey
:
'
productName
'
,
fields
:
anaChgComboQuickFields
,
footerKeywords
:
[
'
选中总数量
'
,
'
选中总差额
'
,
'
合计
'
],
},
};
export
const
anaChgSortCases
=
buildSortColumnCases
(
'
STAT-ACH
'
,
anaChgSortColumns
,
10
);
data/center-report-net-out.data.ts
View file @
79a08ff4
...
...
@@ -24,7 +24,6 @@ export const centerReportNetOutSortColumns = [
'
科室名称
'
,
'
产品编码
'
,
'
产品名称
'
,
'
规格型号
'
,
'
单位
'
,
'
耗材类型
'
,
'
厂家
'
,
...
...
data/center-report-shared.types.ts
View file @
79a08ff4
...
...
@@ -26,6 +26,8 @@ export type CenterReportPageData = {
sortColumns
?:
readonly
string
[];
sortCasePrefix
?:
string
;
sortCaseStartIndex
?:
number
;
/** 列排序前需勾选的维度(如「产品批号」依赖「区分批号」) */
sortColumnPrerequisites
?:
Readonly
<
Record
<
string
,
readonly
string
[]
>>
;
mockNotExistProduct
:
string
;
mockNotExistCode
?:
string
;
mockNotExistMaterial
?:
string
;
...
...
data/cost-fin-in.data.ts
View file @
79a08ff4
...
...
@@ -44,6 +44,7 @@ export const costFinInData: CenterReportPageData = {
hasExpandRow
:
false
,
hasBatchPrint
:
false
,
hasToolbarPrint
:
true
,
printRequiresDetailSelection
:
true
,
hasFilterToggle
:
false
,
exportTaskName
:
'
财务入库单
'
,
exportDirectDownload
:
false
,
...
...
data/cost-finance-psi.data.ts
View file @
79a08ff4
...
...
@@ -66,7 +66,6 @@ export const costFinancePsiSortColumns = [
'
产品名称
'
,
'
规格型号
'
,
'
产品编号
'
,
'
生产厂家
'
,
'
耗材类型
'
,
'
单位
'
,
'
期初数量
'
,
...
...
data/cost-product-rank.data.ts
View file @
79a08ff4
...
...
@@ -37,11 +37,11 @@ export const costProductRankDetailHeaders = ['科室名称', '成本数量', '
export
const
costProductRankComboQuickFields
:
readonly
DeptQuickFieldDef
[]
=
[
{
key
:
'
statMonth
'
,
label
:
'
统计月份
'
,
type
:
'
date
'
},
{
key
:
'
campus
'
,
label
:
'
院区
'
,
type
:
'
select
'
},
{
key
:
'
consumableType
'
,
label
:
'
耗材类型
'
,
type
:
'
select
'
},
{
key
:
'
consumableCategory
'
,
label
:
'
耗材分类
'
,
type
:
'
select
'
},
];
export
const
costProductRankComboDrawerFields
:
readonly
CenterReportDrawerField
[]
=
[
{
key
:
'
consumableType
'
,
type
:
'
select
'
,
label
:
'
耗材类型
'
},
{
key
:
'
consumableCategory
'
,
type
:
'
select
'
,
label
:
'
耗材分类
'
},
{
key
:
'
isKeySupervised
'
,
type
:
'
select
'
,
label
:
'
是否重点监管
'
},
];
...
...
data/dept-report-out.data.ts
View file @
79a08ff4
...
...
@@ -51,7 +51,7 @@ export const deptReportOutData: CenterReportPageData = {
exportDirectDownload
:
false
,
exportSelectFirstRow
:
false
,
exportQueryFromSample
:
true
,
comboSampleScrollColumns
:
[
'
结算方式
'
,
'
单据形态
'
,
'
批号
'
,
'
科室名称
'
,
'
单号
'
,
'
进院供应商名称
'
],
comboSampleScrollColumns
:
[
'
产品编号
'
,
'
产品名称
'
,
'
结算方式
'
,
'
单据形态
'
,
'
批号
'
,
'
科室名称
'
,
'
单号
'
,
'
进院供应商名称
'
],
comboSamplePickKeys
:
[
'
productName
'
,
'
accountDate
'
,
'
billType
'
,
'
deptName
'
,
'
dept
'
,
'
orderNo
'
,
'
settlement
'
,
'
billMode
'
,
'
batchNo
'
],
comboSampleFieldAliases
:
{
dept
:
'
deptName
'
},
comboSampleMaxRowScan
:
5
,
...
...
data/dept-report-turnover.data.ts
View file @
79a08ff4
...
...
@@ -38,6 +38,9 @@ export const deptReportTurnoverData: CenterReportPageData = {
sortCasePrefix
:
'
STAT-DTR
'
,
sortCaseStartIndex
:
6
,
mockNotExistProduct
:
'
__NOT_EXIST_PRODUCT__99999
'
,
horizontalScrollAnchorColumn
:
'
产品编号
'
,
horizontalScrollTargetColumn
:
'
库存周转率
'
,
horizontalScrollDragDelta
:
320
,
paginationPageSizeFrom
:
'
50 条/页
'
,
paginationPageSizeTo
:
'
100 条/页
'
,
sampleRowIndex
:
0
,
...
...
data/invoice-settle.data.ts
View file @
79a08ff4
...
...
@@ -33,7 +33,7 @@ export const invoiceSettleComboAssertKeys = ['supplierName', 'field4'] as const;
export
const
invoiceSettleDomainQuickFields
:
readonly
DeptQuickFieldDef
[]
=
[
{
key
:
'
field
'
,
label
:
'
结算月份
'
,
type
:
'
date
'
},
{
key
:
'
supplierName
'
,
label
:
'
进院供应商
名称
'
,
type
:
'
select
'
},
{
key
:
'
supplierName
'
,
label
:
'
进院供应商
'
,
type
:
'
select
'
},
];
export
const
invoiceSettleDomainAssertKeys
=
[
'
supplierName
'
,
'
field4
'
]
as
const
;
...
...
@@ -84,8 +84,8 @@ export const invoiceSettleData: CenterReportPageData & {
exportQueryFromSample
:
true
,
columnSettingsToggleCandidates
:
[
'
线上线下
'
],
comboSampleScrollColumns
:
[
'
院区名称
'
],
comboSamplePickKeys
:
[
'
supplierName
'
,
'
field4
'
,
'
isVolume
'
,
'
field2
'
],
comboSampleScrollColumns
:
[
'
结算月份
'
,
'
进院供应商
'
,
'
院区名称
'
],
comboSamplePickKeys
:
[
'
field
'
,
'
supplierName
'
,
'
field4
'
,
'
isVolume
'
,
'
field2
'
],
drawerFields
:
invoiceSettleComboDrawerFields
,
};
...
...
data/pol-consumer.data.ts
View file @
79a08ff4
...
...
@@ -103,7 +103,7 @@ export const polConsumerData: CenterReportPageData & {
},
horizontalScrollAnchorColumn
:
'
消耗日期
'
,
horizontalScrollTargetColumn
:
'
带量项目合同号
'
,
horizontalScrollTargetColumn
:
'
完成数量
'
,
horizontalScrollDragDelta
:
320
,
mockNotExistProduct
:
'
__NOT_EXIST_PRODUCT__99999
'
,
mockFieldLabel
:
'
带量项目
'
,
...
...
data/recon-purchase-entry.data.ts
View file @
79a08ff4
...
...
@@ -55,6 +55,9 @@ export const reconPurchaseEntryData: CenterReportPageData = {
sortColumns
:
reconPurchaseEntrySortColumns
,
sortCasePrefix
:
'
STAT-RPE
'
,
sortCaseStartIndex
:
16
,
sortColumnPrerequisites
:
{
产品批号
:
[
'
区分批号
'
],
},
hasAdvancedFilter
:
true
,
hasExport
:
true
,
hasExpandRow
:
false
,
...
...
@@ -69,7 +72,8 @@ export const reconPurchaseEntryData: CenterReportPageData = {
horizontalScrollAnchorColumn
:
'
产品名称
'
,
horizontalScrollTargetColumn
:
'
产品类型
'
,
horizontalScrollDragDelta
:
320
,
comboSamplePickKeys
:
[
'
productName
'
,
'
statDate
'
,
'
supplierName
'
,
'
subSupplierName
'
],
comboSamplePickKeys
:
[
'
productName
'
,
'
statDate
'
,
'
supplierName
'
,
'
subSupplierName
'
,
'
inboundQty
'
],
comboSampleScrollColumns
:
[
'
采购入库数量
'
,
'
产品类型
'
],
drawerFields
:
reconPurchaseEntryComboDrawerFields
,
firstRowSummaryMatch
:
{
metrics
:
[
...
...
data/warn-detain.data.ts
View file @
79a08ff4
...
...
@@ -72,7 +72,7 @@ export const warnDetainData: CenterReportPageData & {
},
horizontalScrollAnchorColumn
:
'
院区名称
'
,
horizontalScrollTargetColumn
:
'
院区名称
'
,
horizontalScrollTargetColumn
:
'
待发数量
'
,
horizontalScrollDragDelta
:
320
,
mockNotExistProduct
:
'
__NOT_EXIST_PRODUCT__99999
'
,
mockFieldLabel
:
'
产品名称
'
,
...
...
docs/jenkins.md
View file @
79a08ff4
...
...
@@ -9,7 +9,7 @@
| 任务 | 套件 | 建议 cron | 说明 |
|------|------|-----------|------|
|
`spd-ui-smoke`
|
`smoke`
|
`H 8-18 * * 1-5`
| 工作时段冒烟:全部报表模块已打
`@smoke`
的用例 |
|
`spd-ui-nightly`
|
`nightly`
|
`H 2 * *
*`
| 夜间主 spec 回归,不含
`@full`
、不含调试
`*.stat-*.spec.ts`
|
|
`spd-ui-nightly`
/
`SPD`
|
`nightly`
/
`full`
|
`H 2
3
* *
1,3,5`
| 周一、三、五
**23 点**
回归(Jenkins 任务
`SPD`
当前按此时段跑 full)
|
|
`spd-ui-full`
|
`full`
|
`H 3 * * 0`
| 周末含全列排序,最慢 |
同一套
`Jenkinsfile`
用参数
`SUITE`
区分即可;定时任务在 Jenkins 里「Build periodically」分别写 cron。
...
...
@@ -84,7 +84,7 @@ npm run test:spd:shard
| Jenkins 任务名 | 默认
`SUITE`
| Build periodically | 建议参数 |
|----------------|--------------|--------------------|----------|
|
`spd-ui-smoke`
|
`smoke`
|
`H 8-18 * * 1-5`
|
`WORKERS=3`
`SHARDS=1`
|
|
`spd-ui-nightly`
|
`nightly`
|
`H 2 * *
*`
|
`WORKERS=2`
`SHARDS=2`
|
|
`spd-ui-nightly`
/
`SPD`
|
`nightly`
/
`full`
|
`H 2
3
* *
1,3,5`
| 周一、三、五 23 点;
`WORKERS=2`
`SHARDS=2`
|
|
`spd-ui-full`
|
`full`
|
`H 3 * * 0`
|
`WORKERS=2`
`SHARDS=2`
,勾选
`RUN_FULL`
|
每个任务:
...
...
locators/spd/center-report-entry.locator.ts
View file @
79a08ff4
...
...
@@ -88,19 +88,41 @@ export class CenterReportEntryLocators {
}
tableHeaderCells
():
Locator
{
return
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
().
locator
(
'
.ant-table-thead tr
'
).
last
().
locator
(
'
th
'
);
return
this
.
visibleThead
().
locator
(
'
tr
'
).
last
().
locator
(
'
th
'
);
}
/**
* 可见 sticky 表头。宽表会在 body / fixed 区再克隆 thead,
* `.ant-table-thead tr`.last() 会命中测宽空行或克隆列,waitFor 触发 strict mode。
*/
visibleThead
():
Locator
{
const
wrapper
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
();
return
wrapper
.
locator
(
'
.ant-table-container > .ant-table-header .ant-table-thead
'
)
.
or
(
wrapper
.
locator
(
'
.ant-table-header .ant-table-thead
'
))
.
or
(
wrapper
.
locator
(
'
.ant-table-content > table > .ant-table-thead
'
))
.
or
(
wrapper
.
locator
(
'
.ant-table-thead
'
))
.
first
();
}
/** 指定列的表头单元格;每条分支先 .first(),避免 or() 在多匹配 waitFor 时 strict mode。 */
columnHeaderCell
(
columnName
:
string
):
Locator
{
const
thead
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
().
locator
(
'
.ant-table-thead
tr
'
).
last
();
const
thead
=
this
.
visibleThead
().
locator
(
'
tr
'
).
last
();
const
byTitle
=
thead
.
locator
(
'
th:visible
'
)
.
filter
({
has
:
this
.
report
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
}),
})
.
filter
({
has
:
this
.
report
.
locator
(
'
.ant-table-column-sorter
'
)
});
const
byRole
=
this
.
report
.
getByRole
(
'
columnheader
'
,
{
name
:
columnName
,
exact
:
true
});
return
byTitle
.
or
(
byRole
).
first
();
.
locator
(
'
th
'
)
.
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
})
})
.
first
();
const
bySorter
=
thead
.
locator
(
'
th
'
)
.
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
})
})
.
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-sorter
'
)
})
.
first
();
const
byRole
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
)
.
first
()
.
getByRole
(
'
columnheader
'
,
{
name
:
columnName
,
exact
:
true
})
.
first
();
return
byTitle
.
or
(
bySorter
).
or
(
byRole
).
first
();
}
columnSorter
(
columnName
:
string
):
Locator
{
...
...
@@ -146,14 +168,21 @@ export class CenterReportEntryLocators {
}
subTableRow
(
rowIndex
=
0
):
Locator
{
return
this
.
expandedRow
.
locator
(
'
.ant-table tbody tr
'
).
nth
(
rowIndex
);
return
this
.
expandedRow
.
locator
(
'
.ant-table tbody tr.ant-table-row:not(.ant-table-measure-row)
'
)
.
nth
(
rowIndex
);
}
subTableViewButton
(
rowIndex
=
0
):
Locator
{
const
row
=
this
.
subTableRow
(
rowIndex
);
return
row
.
getByRole
(
'
button
'
,
{
name
:
'
查看
'
})
.
or
(
row
.
getByRole
(
'
link
'
,
{
name
:
'
查看
'
}))
.
first
();
const
fixedRightRow
=
this
.
expandedRow
.
locator
(
'
.ant-table-fixed-right tbody tr.ant-table-row:not(.ant-table-measure-row)
'
)
.
nth
(
rowIndex
);
const
inRow
=
(
host
:
Locator
)
=>
host
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*看/
})
.
or
(
host
.
getByRole
(
'
link
'
,
{
name
:
/查
\s
*看/
}))
.
or
(
host
.
locator
(
'
a, button, span, .ant-btn
'
).
filter
({
hasText
:
/查
\s
*看/
}));
return
inRow
(
fixedRightRow
).
or
(
inRow
(
row
)).
first
();
}
}
locators/spd/center-report-shared.locator.ts
View file @
79a08ff4
...
...
@@ -39,7 +39,6 @@ export class CenterReportSharedLocators {
readonly
tableBody
:
Locator
;
readonly
dataRows
:
Locator
;
sortDataRows
:
Locator
;
readonly
paginationTotal
:
Locator
;
readonly
drawerTitle
:
Locator
;
readonly
drawerFilterBtn
:
Locator
;
readonly
drawerResetBtn
:
Locator
;
...
...
@@ -50,6 +49,7 @@ export class CenterReportSharedLocators {
readonly
tableWrapperIndex
:
number
;
readonly
detailTableWrapperIndex
:
number
;
sortTableWrapperIndex
:
number
;
paginationTableWrapperIndex
:
number
;
readonly
detailDataRows
:
Locator
;
constructor
(
page
:
Page
,
options
:
CenterReportLocatorOptions
)
{
...
...
@@ -57,6 +57,7 @@ export class CenterReportSharedLocators {
this
.
tableWrapperIndex
=
options
.
tableWrapperIndex
??
0
;
this
.
detailTableWrapperIndex
=
options
.
detailTableWrapperIndex
??
this
.
tableWrapperIndex
;
this
.
sortTableWrapperIndex
=
options
.
sortTableWrapperIndex
??
this
.
tableWrapperIndex
;
this
.
paginationTableWrapperIndex
=
this
.
tableWrapperIndex
;
this
.
productName
=
this
.
report
.
locator
(
options
.
productNameSelector
).
first
();
this
.
goodsCode
=
options
.
goodsCodeSelector
?
this
.
report
.
locator
(
options
.
goodsCodeSelector
).
first
()
...
...
@@ -112,7 +113,6 @@ export class CenterReportSharedLocators {
'
.ant-table-scroll .ant-table-body table tbody > tr.ant-table-row
'
,
].
join
(
'
,
'
),
);
this
.
paginationTotal
=
this
.
report
.
locator
(
'
.ant-pagination-total-text
'
).
first
();
this
.
drawerTitle
=
this
.
report
.
locator
(
'
.ant-drawer-open .ant-drawer-title
'
);
this
.
drawerFilterBtn
=
this
.
report
.
getByRole
(
'
button
'
,
{
name
:
'
筛 选
'
});
this
.
drawerResetBtn
=
this
.
report
.
locator
(
'
.ant-drawer-footer
'
).
getByRole
(
'
button
'
,
{
name
:
'
重 置
'
});
...
...
@@ -241,6 +241,24 @@ export class CenterReportSharedLocators {
);
}
bindPaginationTable
(
index
:
number
):
void
{
this
.
paginationTableWrapperIndex
=
index
;
}
/** 按 tableWrapper 定位对应 ProTable 底部分页,避免左右双表互相抢页码。 */
paginationHost
():
Locator
{
const
wrapper
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
nth
(
this
.
paginationTableWrapperIndex
);
return
wrapper
.
locator
(
'
xpath=ancestor::*[contains(@class,"ant-pro-table")][1]//ul[contains(@class,"ant-pagination")]
'
)
.
first
()
.
or
(
wrapper
.
locator
(
'
xpath=following::ul[contains(@class,"ant-pagination")][1]
'
))
.
or
(
this
.
report
.
locator
(
'
.ant-pagination
'
).
nth
(
this
.
paginationTableWrapperIndex
));
}
get
paginationTotal
():
Locator
{
return
this
.
paginationHost
().
locator
(
'
.ant-pagination-total-text
'
).
first
();
}
private
sortTableWrapper
():
Locator
{
return
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
nth
(
this
.
sortTableWrapperIndex
);
}
...
...
@@ -257,17 +275,16 @@ export class CenterReportSharedLocators {
let
chain
:
Locator
|
undefined
;
const
append
=
(
loc
:
Locator
)
=>
{
chain
=
chain
?
chain
.
or
(
loc
)
:
loc
;
const
unique
=
loc
.
first
();
chain
=
chain
?
chain
.
or
(
unique
)
:
unique
;
};
append
(
thead
.
locator
(
'
th
:visible
'
).
filter
({
has
:
thead
.
getByText
(
columnName
,
{
exact
:
true
})
}));
append
(
withSorter
(
thead
.
locator
(
'
th
:visible
'
).
filter
({
has
:
thead
.
getByText
(
columnName
,
{
exact
:
true
})
})));
append
(
thead
.
locator
(
'
th
'
).
filter
({
has
:
thead
.
getByText
(
columnName
,
{
exact
:
true
})
}));
append
(
withSorter
(
thead
.
locator
(
'
th
'
).
filter
({
has
:
thead
.
getByText
(
columnName
,
{
exact
:
true
})
})));
append
(
withSorter
(
thead
.
locator
(
'
th:visible
'
).
filter
({
has
:
this
.
report
.
locator
(
'
.ant-table-column-title
'
)
.
getByText
(
columnName
,
{
exact
:
true
}),
thead
.
locator
(
'
th
'
).
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
}),
}),
),
);
...
...
@@ -283,11 +300,11 @@ export class CenterReportSharedLocators {
for
(
const
token
of
tokens
)
{
const
normalizedFull
=
columnName
.
replace
(
/
\s
+/g
,
''
);
if
(
token
===
normalizedFull
)
continue
;
append
(
withSorter
(
thead
.
locator
(
'
th
:visible
'
).
filter
({
hasText
:
token
})));
append
(
withSorter
(
thead
.
locator
(
'
th
'
).
filter
({
hasText
:
token
})));
append
(
withSorter
(
thead
.
locator
(
'
th
:visible
'
).
filter
({
has
:
th
is
.
report
.
locator
(
'
.ant-table-column-title
'
).
filter
({
hasText
:
token
}),
thead
.
locator
(
'
th
'
).
filter
({
has
:
th
ead
.
locator
(
'
.ant-table-column-title
'
).
filter
({
hasText
:
token
}),
}),
),
);
...
...
@@ -347,11 +364,11 @@ export class CenterReportSharedLocators {
}
paginationItem
(
pageNum
:
number
):
Locator
{
return
this
.
report
.
locator
(
`.ant-pagination-item-
${
pageNum
}
`
);
return
this
.
paginationHost
()
.
locator
(
`.ant-pagination-item-
${
pageNum
}
`
);
}
paginationActiveItem
():
Locator
{
return
this
.
report
.
locator
(
'
.ant-pagination-item-active
'
);
return
this
.
paginationHost
()
.
locator
(
'
.ant-pagination-item-active
'
)
.
first
()
;
}
expandedSubTableHeader
(
name
:
string
):
Locator
{
...
...
@@ -359,7 +376,9 @@ export class CenterReportSharedLocators {
}
subTableRow
(
rowIndex
=
0
):
Locator
{
return
this
.
expandedRow
.
locator
(
'
.ant-table tbody tr
'
).
nth
(
rowIndex
);
return
this
.
expandedRow
.
locator
(
'
.ant-table tbody tr.ant-table-row:not(.ant-table-measure-row)
'
)
.
nth
(
rowIndex
);
}
subTableRowCheckbox
(
rowIndex
=
0
):
Locator
{
...
...
@@ -368,10 +387,14 @@ export class CenterReportSharedLocators {
subTableViewButton
(
rowIndex
=
0
):
Locator
{
const
row
=
this
.
subTableRow
(
rowIndex
);
return
row
.
getByRole
(
'
button
'
,
{
name
:
'
查看
'
})
.
or
(
row
.
getByRole
(
'
link
'
,
{
name
:
'
查看
'
}))
.
or
(
row
.
locator
(
'
a, span, .ant-btn
'
).
filter
({
hasText
:
/^查看$/
}))
.
first
();
const
fixedRightRow
=
this
.
expandedRow
.
locator
(
'
.ant-table-fixed-right tbody tr.ant-table-row:not(.ant-table-measure-row)
'
)
.
nth
(
rowIndex
);
const
inRow
=
(
host
:
Locator
)
=>
host
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*看/
})
.
or
(
host
.
getByRole
(
'
link
'
,
{
name
:
/查
\s
*看/
}))
.
or
(
host
.
locator
(
'
a, button, span, .ant-btn
'
).
filter
({
hasText
:
/查
\s
*看/
}));
return
inRow
(
fixedRightRow
).
or
(
inRow
(
row
)).
first
();
}
}
locators/spd/center-report-sheet.locator.ts
View file @
79a08ff4
...
...
@@ -81,18 +81,41 @@ export class CenterReportSheetLocators {
}
tableHeaderCells
():
Locator
{
return
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
().
locator
(
'
.ant-table-thead tr
'
).
last
().
locator
(
'
th
'
);
return
this
.
visibleThead
().
locator
(
'
tr
'
).
last
().
locator
(
'
th
'
);
}
/**
* 可见 sticky 表头。宽表会在 body / fixed 区再克隆 thead,
* `.ant-table-thead tr`.last() 会命中测宽空行或克隆列,waitFor 触发 strict mode。
*/
visibleThead
():
Locator
{
const
wrapper
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
();
return
wrapper
.
locator
(
'
.ant-table-container > .ant-table-header .ant-table-thead
'
)
.
or
(
wrapper
.
locator
(
'
.ant-table-header .ant-table-thead
'
))
.
or
(
wrapper
.
locator
(
'
.ant-table-content > table > .ant-table-thead
'
))
.
or
(
wrapper
.
locator
(
'
.ant-table-thead
'
))
.
first
();
}
/** 指定列的表头单元格
(可见列 + 含排序控件,规避固定列重复表头)
。 */
/** 指定列的表头单元格
;每条分支先 .first(),避免 or() 在多匹配 waitFor 时 strict mode
。 */
columnHeaderCell
(
columnName
:
string
):
Locator
{
const
thead
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
().
locator
(
'
.ant-table-thead
tr
'
).
last
();
const
thead
=
this
.
visibleThead
().
locator
(
'
tr
'
).
last
();
const
byTitle
=
thead
.
locator
(
'
th:visible
'
)
.
filter
({
has
:
this
.
report
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
})
})
.
filter
({
has
:
this
.
report
.
locator
(
'
.ant-table-column-sorter
'
)
});
const
byRole
=
this
.
report
.
getByRole
(
'
columnheader
'
,
{
name
:
columnName
,
exact
:
true
});
return
byTitle
.
or
(
byRole
).
first
();
.
locator
(
'
th
'
)
.
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
})
})
.
first
();
const
bySorter
=
thead
.
locator
(
'
th
'
)
.
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-title
'
).
getByText
(
columnName
,
{
exact
:
true
})
})
.
filter
({
has
:
thead
.
locator
(
'
.ant-table-column-sorter
'
)
})
.
first
();
const
byRole
=
this
.
report
.
locator
(
'
.ant-table-wrapper
'
)
.
first
()
.
getByRole
(
'
columnheader
'
,
{
name
:
columnName
,
exact
:
true
})
.
first
();
return
byTitle
.
or
(
bySorter
).
or
(
byRole
).
first
();
}
/** 指定列的排序按钮(▲▼ 区域,见 ant-table-column-sorter)。 */
...
...
pages/spd/ana-chg.page.ts
View file @
79a08ff4
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
anaChgData
}
from
'
../../data/ana-chg.data
'
;
import
{
expectNumbersClose
,
parseReportNumber
,
}
from
'
./helpers/report-summary.helper
'
;
import
{
readSummaryMetrics
}
from
'
./helpers/report-summary-match.helper
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
export
class
AnaChgPage
extends
ReconCostReportSharedPage
{
...
...
@@ -17,35 +12,15 @@ export class AnaChgPage extends ReconCostReportSharedPage {
* STAT-ACH-007:勾选首行后,明细「数量/总差额」与底部「选中总数量/选中总差额」一致。
*/
async
expectSelectedSummaryMatchesFirstRow
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
void
>
{
const
cfg
=
this
.
data
.
firstRowSummaryMatch
;
expect
(
cfg
,
'
未配置 firstRowSummaryMatch,无法做选中汇总勾稽
'
).
toBeTruthy
();
if
(
!
cfg
)
return
;
await
this
.
waitForTableReady
();
const
rowCount
=
await
this
.
locators
.
dataRows
.
count
();
expect
(
rowCount
,
'
主表应至少有 1 条数据以便勾选
'
).
toBeGreaterThan
(
0
);
expect
(
await
this
.
locators
.
dataRows
.
count
(),
'
主表应至少有 1 条数据以便勾选
'
).
toBeGreaterThan
(
0
);
await
this
.
checkMainTableRow
(
rowIndex
);
const
sample
=
await
this
.
extractSampleRow
(
rowIndex
);
const
expectedQty
=
parseReportNumber
(
this
.
getSampleValue
(
sample
,
'
field8
'
));
const
expectedDiff
=
parseReportNumber
(
this
.
getSampleValue
(
sample
,
'
field9
'
));
await
expect
.
poll
(
async
()
=>
{
const
summary
=
await
readSummaryMetrics
(
this
.
locators
.
report
.
locator
(
'
body
'
),
[
'
选中总数量
'
,
'
选中总差额
'
,
]);
return
Math
.
abs
(
summary
[
'
选中总数量
'
]
-
expectedQty
)
<=
0.01
*
Math
.
max
(
Math
.
abs
(
expectedQty
),
1
)
&&
Math
.
abs
(
summary
[
'
选中总差额
'
]
-
expectedDiff
)
<=
0.01
*
Math
.
max
(
Math
.
abs
(
expectedDiff
),
1
);
},
{
timeout
:
15
_000
,
message
:
'
勾选后底部选中汇总未与明细数量/总差额一致
'
},
)
.
toBe
(
true
);
const
summary
=
await
readSummaryMetrics
(
this
.
locators
.
report
.
locator
(
'
body
'
),
[
'
选中总数量
'
,
'
选中总差额
'
,
]);
expectNumbersClose
(
summary
[
'
选中总数量
'
],
expectedQty
,
0.01
,
'
选中总数量
'
);
expectNumbersClose
(
summary
[
'
选中总差额
'
],
expectedDiff
,
0.01
,
'
选中总差额
'
);
await
this
.
assertVisibleRowsMatchFooter
(
cfg
.
metrics
,
{
rowIndexes
:
[
rowIndex
]
});
}
}
...
...
pages/spd/center-report-entry.page.ts
View file @
79a08ff4
...
...
@@ -11,9 +11,10 @@ import { BasePage } from '../../core/base.page';
import
{
columnHeaderMatchTokens
}
from
'
../../core/column-header-match.util
'
;
import
{
CenterReportEntryLocators
}
from
'
../../locators/spd/center-report-entry.locator
'
;
import
{
DrawerFormControls
}
from
'
./helpers/drawer-form-controls.helper
'
;
import
{
ColumnSortControls
}
from
'
./helpers/column-sort.helper
'
;
import
{
ColumnSortControls
,
clickColumnSortControl
}
from
'
./helpers/column-sort.helper
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
revealColumnSettingsTriggerByScroll
,
}
from
'
./helpers/column-settings.helper
'
;
...
...
@@ -68,7 +69,7 @@ export class CenterReportEntryPage extends BasePage {
columnSorter
:
(
columnName
)
=>
this
.
locators
.
columnSorter
(
columnName
),
columnSorterUp
:
(
columnName
)
=>
this
.
locators
.
columnSorterUp
(
columnName
),
columnSorterDown
:
(
columnName
)
=>
this
.
locators
.
columnSorterDown
(
columnName
),
clickColumnSort
:
(
columnName
)
=>
this
.
clickColumnSort
(
columnName
),
clickColumnSort
:
(
columnName
,
order
)
=>
this
.
clickColumnSort
(
columnName
,
order
),
getFirstRowText
:
()
=>
this
.
getFirstRowText
(),
getColumnValues
:
(
columnName
,
maxRows
)
=>
this
.
getColumnValues
(
columnName
,
maxRows
),
getRowSignatures
:
(
maxRows
)
=>
this
.
getRowSignatures
(
maxRows
),
...
...
@@ -668,8 +669,20 @@ export class CenterReportEntryPage extends BasePage {
async
clickSubTableViewDetail
(
rowIndex
=
0
):
Promise
<
string
>
{
const
orderNo
=
await
this
.
getSubTableInboundOrderNo
(
rowIndex
);
const
expanded
=
this
.
locators
.
expandedRow
;
const
scrollHosts
=
expanded
.
locator
(
'
.ant-table-body, .ant-table-content, .ant-table-scroll
'
);
const
hostCount
=
await
scrollHosts
.
count
();
for
(
let
i
=
0
;
i
<
hostCount
;
i
+=
1
)
{
await
scrollHosts
.
nth
(
i
)
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
}
const
viewBtn
=
this
.
locators
.
subTableViewButton
(
rowIndex
);
await
expect
(
viewBtn
).
toBeVisible
();
await
viewBtn
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
expect
(
viewBtn
,
'
子表「查看」按钮应可见
'
).
toBeVisible
({
timeout
:
10
_000
});
await
this
.
click
(
viewBtn
,
'
点击子表查看
'
);
return
orderNo
;
}
...
...
@@ -888,19 +901,43 @@ export class CenterReportEntryPage extends BasePage {
async
isColumnSortable
(
columnName
:
string
):
Promise
<
boolean
>
{
await
this
.
scrollColumnIntoView
(
columnName
);
const
sorter
=
this
.
locators
.
columnSorter
(
columnName
);
const
sortable
=
(
await
sorter
.
count
())
>
0
&&
(
await
sorter
.
isVisible
());
if
(
!
sortable
)
{
if
((
await
sorter
.
count
())
>
0
)
{
return
true
;
}
const
header
=
this
.
locators
.
columnHeaderCell
(
columnName
);
const
hasSorter
=
await
header
.
evaluate
((
el
)
=>
{
const
th
=
((
el
as
HTMLElement
).
closest
(
'
th
'
)
??
el
)
as
HTMLElement
;
return
Boolean
(
th
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
th
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
);
})
.
catch
(()
=>
false
);
if
(
!
hasSorter
)
{
await
this
.
assertPageHealthy
(
`列「
${
columnName
}
」不可排序`
);
}
return
sortable
;
return
hasSorter
;
}
async
clickColumnSort
(
columnName
:
string
):
Promise
<
void
>
{
async
clickColumnSort
(
columnName
:
string
,
order
?:
'
ascend
'
|
'
descend
'
):
Promise
<
void
>
{
await
this
.
scrollColumnIntoView
(
columnName
);
const
header
=
this
.
locators
.
columnHeaderCell
(
columnName
);
const
sorter
=
this
.
locators
.
columnSorter
(
columnName
);
await
this
.
waitVisible
(
sorter
,
15
_000
);
await
test
.
step
(
`点击
${
columnName
}
列排序`
,
async
()
=>
{
await
sorter
.
click
({
force
:
true
});
if
((
await
sorter
.
count
())
>
0
)
{
await
sorter
.
waitFor
({
state
:
'
attached
'
,
timeout
:
15
_000
}).
catch
(()
=>
undefined
);
}
else
{
await
this
.
waitVisible
(
header
,
15
_000
);
}
const
label
=
order
===
'
descend
'
?
'
降序
'
:
order
===
'
ascend
'
?
'
升序
'
:
'
排序
'
;
await
test
.
step
(
`点击
${
columnName
}
列
${
label
}
`
,
async
()
=>
{
await
clickColumnSortControl
({
header
,
sorter
,
up
:
this
.
locators
.
columnSorterUp
(
columnName
),
down
:
this
.
locators
.
columnSorterDown
(
columnName
),
order
,
});
});
await
this
.
locators
.
report
.
locator
(
'
.ant-spin-spinning
'
).
waitFor
({
state
:
'
hidden
'
,
timeout
:
30
_000
}).
catch
(()
=>
undefined
);
}
...
...
@@ -940,26 +977,32 @@ export class CenterReportEntryPage extends BasePage {
}
async
openColumnSettings
():
Promise
<
void
>
{
await
this
.
closeColumnSettingsPanel
();
await
this
.
locators
.
columnSettingsPanel
().
waitFor
({
state
:
'
hidden
'
,
timeout
:
3
_000
}).
catch
(()
=>
undefined
);
await
revealColumnSettingsTriggerByScroll
({
scroll
:
this
.
tableScrollLayoutControls
,
tableWrapper
:
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
(),
triggerCandidates
:
[
this
.
locators
.
columnSettingsTrigger
],
horizontalScrollTargetColumn
:
centerReportEntryData
.
horizontalScrollTargetColumn
,
horizontalScrollDragDelta
:
centerReportEntryData
.
horizontalScrollDragDelta
,
scrollColumnIntoView
:
async
(
column
)
=>
{
await
this
.
locators
.
tableHeader
(
column
).
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
},
});
if
(
!
(
await
findUsableColumnSettingsTrigger
([
this
.
locators
.
columnSettingsTrigger
])))
{
const
panel
=
this
.
locators
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
}
const
triggerCandidates
=
[
this
.
locators
.
columnSettingsTrigger
];
let
trigger
=
await
findUsableColumnSettingsTrigger
(
triggerCandidates
);
if
(
!
trigger
)
{
await
revealColumnSettingsTriggerByScroll
({
scroll
:
this
.
tableScrollLayoutControls
,
tableWrapper
:
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
(),
triggerCandidates
,
horizontalScrollTargetColumn
:
centerReportEntryData
.
horizontalScrollTargetColumn
,
horizontalScrollDragDelta
:
centerReportEntryData
.
horizontalScrollDragDelta
,
scrollColumnIntoView
:
async
(
column
)
=>
{
await
this
.
locators
.
tableHeader
(
column
).
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
},
});
trigger
=
await
findUsableColumnSettingsTrigger
(
triggerCandidates
);
}
if
(
!
trigger
)
{
await
this
.
guardedSkip
(
'
列设置齿轮不可见
'
);
}
await
this
.
waitVisible
(
this
.
locators
.
columnSettingsTrigger
,
15
_000
);
await
test
.
step
(
'
打开列设置
'
,
async
()
=>
{
await
t
his
.
locators
.
columnSettingsT
rigger
.
click
({
force
:
true
});
await
trigger
!
.
click
({
force
:
true
});
});
await
this
.
waitVisible
(
this
.
locators
.
columnSettingsP
anel
()
,
10
_000
);
await
this
.
waitVisible
(
p
anel
,
10
_000
);
}
async
expectColumnSettingsPanel
():
Promise
<
void
>
{
...
...
@@ -1010,6 +1053,19 @@ export class CenterReportEntryPage extends BasePage {
return
centerReportEntryData
.
columnSettingsToggleCandidates
[
0
];
}
/** 场景:取消勾选非核心列后表头隐藏,再勾选后表头恢复。 */
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
expectPaginationTotal
():
Promise
<
void
>
{
await
expect
(
this
.
locators
.
paginationTotal
).
toHaveText
(
/共
\s
+
\d
+
\s
+条/
);
}
...
...
pages/spd/center-report-shared.page.ts
View file @
79a08ff4
...
...
@@ -18,9 +18,10 @@ import {
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
retryReportLoad
}
from
'
./helpers/report-load-retry.helper
'
;
import
{
DrawerFormControls
}
from
'
./helpers/drawer-form-controls.helper
'
;
import
{
ColumnSortControls
,
isColumnValuesSorted
,
type
ColumnSortOrder
}
from
'
./helpers/column-sort.helper
'
;
import
{
ColumnSortControls
,
isColumnValuesSorted
,
clickColumnSortControl
,
readColumnSortOrder
,
type
ColumnSortOrder
}
from
'
./helpers/column-sort.helper
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
revealColumnSettingsTriggerByScroll
as
scrollToRevealColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
...
...
@@ -104,6 +105,14 @@ export class CenterReportSharedPage extends BasePage {
* 未列入的列无控件则 skip。
*/
async
expectColumnSortCase
(
columnName
:
string
,
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
const
prereqs
=
this
.
data
.
sortColumnPrerequisites
?.[
columnName
];
if
(
prereqs
?.
length
)
{
for
(
const
label
of
prereqs
)
{
await
this
.
checkDimension
(
label
);
}
await
this
.
search
();
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
}
const
required
=
(
this
.
data
.
sortColumns
??
[]).
includes
(
columnName
);
const
sortable
=
await
this
.
isColumnSortable
(
columnName
,
tableWrapperIndex
);
if
(
!
sortable
)
{
...
...
@@ -137,7 +146,11 @@ export class CenterReportSharedPage extends BasePage {
protected
async
assertComboQueryHasMatchingRows
(
_rowCount
?:
number
):
Promise
<
void
>
{
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
const
count
=
await
this
.
countVisibleDataRows
();
let
count
=
await
this
.
countVisibleDataRows
();
if
(
count
>
0
)
return
;
await
this
.
search
();
await
this
.
waitForTableReady
().
catch
(()
=>
undefined
);
count
=
await
this
.
countVisibleDataRows
();
if
(
count
>
0
)
return
;
expect
(
this
.
hadInitialListDataForCombo
,
...
...
@@ -170,7 +183,7 @@ export class CenterReportSharedPage extends BasePage {
columnSorter
:
(
columnName
)
=>
this
.
locators
.
columnSorter
(
columnName
),
columnSorterUp
:
(
columnName
)
=>
this
.
locators
.
columnSorterUp
(
columnName
),
columnSorterDown
:
(
columnName
)
=>
this
.
locators
.
columnSorterDown
(
columnName
),
clickColumnSort
:
(
columnName
)
=>
this
.
clickColumnSort
(
columnName
),
clickColumnSort
:
(
columnName
,
order
)
=>
this
.
clickColumnSort
(
columnName
,
order
),
getFirstRowText
:
()
=>
this
.
getFirstRowText
(),
getColumnValues
:
(
columnName
,
maxRows
)
=>
this
.
getColumnValues
(
columnName
,
maxRows
),
getRowSignatures
:
(
maxRows
)
=>
this
.
getRowSignatures
(
maxRows
),
...
...
@@ -179,10 +192,18 @@ export class CenterReportSharedPage extends BasePage {
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
locators
.
columnSettingsPanel
(),
columnSettingsTrigger
:
this
.
locators
.
columnSettingsTrigger
,
resolveTrigger
:
async
()
=>
this
.
resolveColumnSettingsTrigger
(
this
.
defaultColumnSettingsTableIndex
()),
resolveTrigger
:
async
()
=>
{
const
idx
=
this
.
defaultColumnSettingsTableIndex
();
const
found
=
await
findUsableColumnSettingsTrigger
(
this
.
columnSettingsTriggerCandidates
(
idx
),
);
if
(
found
)
{
return
found
;
}
return
this
.
resolveColumnSettingsTrigger
(
idx
);
},
reportBody
:
this
.
locators
.
report
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
locators
.
searchBtn
,
clickOutsideTarget
:
this
.
locators
.
productName
,
tableClickTarget
:
this
.
locators
.
tableBody
,
});
this
.
tableScrollLayoutControls
=
new
TableScrollLayoutControls
({
...
...
@@ -208,7 +229,7 @@ export class CenterReportSharedPage extends BasePage {
this
.
summaryMatchControls
=
data
.
summaryMatch
?
new
ReportSummaryMatchControls
({
profile
:
data
.
summaryMatch
,
reportBody
:
this
.
locators
.
report
.
locator
(
'
body
'
)
,
reportBody
:
this
.
locators
.
report
,
dataRows
:
this
.
locators
.
dataRows
,
paginationTotal
:
this
.
locators
.
paginationTotal
,
sampleRowIndex
:
data
.
sampleRowIndex
,
...
...
@@ -419,7 +440,20 @@ export class CenterReportSharedPage extends BasePage {
}
protected
async
getTableRowCount
():
Promise
<
number
>
{
return
this
.
locators
.
dataRows
.
count
();
return
this
.
paginationDataRows
().
count
();
}
/** 当前分页绑定表的数据行(双表页右侧分页读明细行)。 */
protected
paginationDataRows
():
Locator
{
const
idx
=
this
.
locators
.
paginationTableWrapperIndex
;
if
(
this
.
data
.
detailTableWrapperIndex
!==
undefined
&&
idx
===
this
.
data
.
detailTableWrapperIndex
&&
idx
!==
(
this
.
data
.
tableWrapperIndex
??
0
)
)
{
return
this
.
locators
.
detailDataRows
;
}
return
this
.
locators
.
dataRows
;
}
protected
async
readPaginationTotalCount
():
Promise
<
number
>
{
...
...
@@ -1223,18 +1257,34 @@ export class CenterReportSharedPage extends BasePage {
async
getFirstRowText
():
Promise
<
string
>
{
await
this
.
waitForTableReady
();
await
expect
.
poll
(
async
()
=>
this
.
locators
.
d
ataRows
.
count
(),
{
timeout
:
20
_000
})
.
poll
(
async
()
=>
this
.
paginationD
ataRows
()
.
count
(),
{
timeout
:
20
_000
})
.
toBeGreaterThan
(
0
);
await
expect
(
this
.
locators
.
dataRows
.
first
()).
toBeVisible
({
timeout
:
15
_000
});
return
(
await
this
.
locators
.
dataRows
.
first
().
innerText
()).
trim
();
const
row
=
await
this
.
firstVisibleDataRow
();
await
row
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
const
text
=
((
await
row
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
();
expect
(
text
,
'
列表首行应有文本
'
).
toBeTruthy
();
return
text
;
}
protected
async
firstVisibleDataRow
():
Promise
<
Locator
>
{
const
rows
=
this
.
paginationDataRows
();
const
count
=
await
rows
.
count
();
for
(
let
i
=
0
;
i
<
Math
.
min
(
count
,
20
);
i
+=
1
)
{
const
row
=
rows
.
nth
(
i
);
if
(
await
row
.
isVisible
().
catch
(()
=>
false
))
{
return
row
;
}
}
return
rows
.
first
();
}
async
expectFirstRowContains
(
keyword
:
string
|
RegExp
):
Promise
<
void
>
{
if
(
typeof
keyword
===
'
string
'
&&
isDateRangeFilterValue
(
keyword
))
{
return
;
}
await
expect
(
this
.
locators
.
dataRows
.
first
()).
toBeVisible
({
timeout
:
15
_000
});
await
expect
(
this
.
locators
.
dataRows
.
first
()).
toContainText
(
keyword
);
const
row
=
await
this
.
firstVisibleDataRow
();
await
row
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
expect
(
row
).
toContainText
(
keyword
);
}
async
expandFirstRow
():
Promise
<
void
>
{
...
...
@@ -1283,9 +1333,9 @@ export class CenterReportSharedPage extends BasePage {
hits
.
length
,
`子表缺少列,期望之一:
${
required
.
join
(
'
、
'
)}
,实际表头:
${
headerText
}
`
,
).
toBeGreaterThan
(
0
);
const
viewBtn
=
this
.
locators
.
subTableViewButton
(
0
);
const
viewBtn
=
await
this
.
revealSubTableViewButton
(
0
).
catch
(()
=>
this
.
locators
.
subTableViewButton
(
0
)
)
;
if
((
await
viewBtn
.
count
())
>
0
)
{
await
expect
(
viewBtn
).
toBeVisible
();
await
expect
(
viewBtn
).
toBeVisible
(
{
timeout
:
10
_000
}
);
}
}
...
...
@@ -1313,15 +1363,17 @@ export class CenterReportSharedPage extends BasePage {
}
/** 场景1:切换至第2页标准流程(数据不足 skip,否则校验两页首行不同)。 */
async
executePage2SwitchTest
():
Promise
<
void
>
{
await
this
.
skipIfInsufficientForPage2
();
const
first
=
await
this
.
getFirstRowText
();
await
this
.
goToPage
(
2
);
await
this
.
expectActivePage
(
2
);
const
second
=
await
this
.
getFirstRowText
();
expect
(
first
,
'
第1页首行应有数据
'
).
toBeTruthy
();
expect
(
second
,
'
第2页首行应有数据
'
).
toBeTruthy
();
expect
(
second
).
not
.
toEqual
(
first
);
async
executePage2SwitchTest
(
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
return
this
.
withPaginationTableIndex
(
tableWrapperIndex
,
async
()
=>
{
await
this
.
skipIfInsufficientForPage2
();
const
first
=
await
this
.
getFirstRowText
();
await
this
.
goToPage
(
2
);
await
this
.
expectActivePage
(
2
);
const
second
=
await
this
.
getFirstRowText
();
expect
(
first
,
'
第1页首行应有数据
'
).
toBeTruthy
();
expect
(
second
,
'
第2页首行应有数据
'
).
toBeTruthy
();
expect
(
second
).
not
.
toEqual
(
first
);
});
}
async
goToPage
(
pageNum
:
number
):
Promise
<
void
>
{
...
...
@@ -1379,15 +1431,21 @@ export class CenterReportSharedPage extends BasePage {
fromLabel
:
string
,
toLabel
:
string
,
maxRows
:
number
,
options
?:
{
requireMoreThanOnePage
?:
boolean
},
options
?:
{
requireMoreThanOnePage
?:
boolean
;
tableWrapperIndex
?:
number
},
):
Promise
<
void
>
{
await
this
.
skipIfInsufficientForPageSizeChange
(
options
);
await
this
.
changePageSize
(
fromLabel
,
toLabel
);
await
this
.
expectVisibleRowCountAtMost
(
maxRows
);
return
this
.
withPaginationTableIndex
(
options
?.
tableWrapperIndex
,
async
()
=>
{
await
this
.
skipIfInsufficientForPageSizeChange
(
options
);
await
this
.
changePageSize
(
fromLabel
,
toLabel
);
await
this
.
expectVisibleRowCountAtMost
(
maxRows
);
});
}
async
changePageSize
(
fromLabel
:
string
,
toLabel
:
string
):
Promise
<
void
>
{
const
changer
=
this
.
locators
.
report
.
locator
(
'
.ant-pagination-options-size-changer
'
).
first
();
const
changer
=
this
.
locators
.
paginationHost
()
.
locator
(
'
.ant-pagination-options-size-changer
'
)
.
first
()
.
or
(
this
.
locators
.
report
.
locator
(
'
.ant-pagination-options-size-changer
'
).
nth
(
this
.
locators
.
paginationTableWrapperIndex
));
if
((
await
changer
.
count
())
===
0
)
{
await
this
.
guardedSkip
(
'
当前页面无每页条数切换
'
);
}
...
...
@@ -1996,8 +2054,15 @@ export class CenterReportSharedPage extends BasePage {
return
sample
;
}
/** 组合查询后页面无错误提示。 */
/** 组合查询后页面无错误提示。
瞬时「未知错误」先关 toast 再查一次。
*/
async
expectQueryWithoutError
():
Promise
<
void
>
{
if
(
await
this
.
hasVisibleErrorNotice
())
{
const
transient
=
await
this
.
isTransientUnknownError
();
if
(
transient
)
{
await
this
.
dismissVisibleErrorNotice
();
await
this
.
search
();
}
}
expect
(
await
this
.
hasVisibleErrorNotice
(),
'
查询后页面出现错误提示
'
).
toBeFalsy
();
}
...
...
@@ -2176,6 +2241,23 @@ export class CenterReportSharedPage extends BasePage {
}
}
/** 临时切换分页目标表,避免左右双表页码互相干扰。 */
protected
async
withPaginationTableIndex
<
T
>
(
tableWrapperIndex
:
number
|
undefined
,
fn
:
()
=>
Promise
<
T
>
,
):
Promise
<
T
>
{
if
(
tableWrapperIndex
===
undefined
)
{
return
fn
();
}
const
prev
=
this
.
locators
.
paginationTableWrapperIndex
;
this
.
locators
.
bindPaginationTable
(
tableWrapperIndex
);
try
{
return
await
fn
();
}
finally
{
this
.
locators
.
bindPaginationTable
(
prev
);
}
}
/** 按表头文本+排序图标精确定位列索引(排除空表头/无 sorter 列)。 */
protected
async
headerHasSortControl
(
header
:
Locator
):
Promise
<
boolean
>
{
const
sorterCount
=
await
header
.
locator
(
'
.ant-table-column-sorter
'
).
count
().
catch
(()
=>
0
);
...
...
@@ -2214,7 +2296,8 @@ export class CenterReportSharedPage extends BasePage {
}
if
(
index
>=
0
)
{
const
headerAtIndex
=
this
.
locators
.
sortColumnHeaderAt
(
index
);
if
(
await
this
.
headerHasSortControl
(
headerAtIndex
))
{
const
text
=
((
await
headerAtIndex
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
if
(
scoreColumnHeaderMatch
(
text
,
columnName
)
>
0
&&
(
await
this
.
headerHasSortControl
(
headerAtIndex
)))
{
return
index
;
}
}
...
...
@@ -2253,6 +2336,16 @@ export class CenterReportSharedPage extends BasePage {
);
const
header
=
this
.
locators
.
sortColumnHeaderAt
(
columnIndex
);
const
resolved
=
((
await
header
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
if
(
!
resolved
)
{
const
retried
=
await
this
.
findSortColumnHeaderIndex
(
columnName
);
const
retryHeader
=
this
.
locators
.
sortColumnHeaderAt
(
retried
);
const
retryText
=
((
await
retryHeader
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
expect
(
scoreColumnHeaderMatch
(
retryText
,
columnName
),
`表头定位偏差:期望「
${
columnName
}
」,实际「
${
retryText
}
」`
,
).
toBeGreaterThan
(
0
);
return
;
}
expect
(
scoreColumnHeaderMatch
(
resolved
,
columnName
),
`表头定位偏差:期望「
${
columnName
}
」,实际「
${
resolved
}
」`
,
...
...
@@ -2284,19 +2377,23 @@ export class CenterReportSharedPage extends BasePage {
});
}
async
clickColumnSort
(
columnName
:
string
):
Promise
<
void
>
{
async
clickColumnSort
(
columnName
:
string
,
order
?:
'
ascend
'
|
'
descend
'
):
Promise
<
void
>
{
await
this
.
scrollColumnIntoView
(
columnName
);
const
headerCell
=
this
.
locators
.
columnHeaderCell
(
columnName
);
const
columnIndex
=
await
this
.
findSortColumnHeaderIndex
(
columnName
);
const
header
=
(
await
headerCell
.
count
())
>
0
?
headerCell
:
this
.
locators
.
sortColumnHeaderAt
(
columnIndex
);
const
sorter
=
this
.
locators
.
columnSorter
(
columnName
);
await
this
.
waitVisible
(
header
,
15
_000
);
await
test
.
step
(
`点击
${
columnName
}
列排序`
,
async
()
=>
{
if
((
await
sorter
.
count
())
>
0
&&
(
await
sorter
.
isVisible
().
catch
(()
=>
false
)))
{
await
sorter
.
click
({
force
:
true
});
}
else
{
await
header
.
click
({
force
:
true
});
}
const
header
=
this
.
locators
.
sortColumnHeaderAt
(
columnIndex
);
const
sorter
=
this
.
locators
.
sortColumnSorterAt
(
columnIndex
);
const
up
=
header
.
locator
(
'
.ant-table-column-sorter-up
'
).
first
();
const
down
=
header
.
locator
(
'
.ant-table-column-sorter-down
'
).
first
();
await
header
.
waitFor
({
state
:
'
attached
'
,
timeout
:
15
_000
});
const
label
=
order
===
'
descend
'
?
'
降序
'
:
order
===
'
ascend
'
?
'
升序
'
:
'
排序
'
;
await
test
.
step
(
`点击
${
columnName
}
列
${
label
}
`
,
async
()
=>
{
await
clickColumnSortControl
({
header
,
sorter
,
up
,
down
,
order
,
});
await
this
.
waitForSortDone
();
});
}
...
...
@@ -2309,61 +2406,29 @@ export class CenterReportSharedPage extends BasePage {
await
this
.
locators
.
tableBody
.
waitFor
({
state
:
'
visible
'
,
timeout
:
15
_000
}).
catch
(()
=>
undefined
);
await
this
.
waitForSearchLoadingDone
().
catch
(()
=>
undefined
);
await
waitAntSpinGone
(
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
first
());
if
(
await
this
.
isTransientUnknownError
())
{
await
this
.
dismissVisibleErrorNotice
();
}
}
private
async
readSortOrderAt
(
columnIndex
:
number
):
Promise
<
ColumnSortOrder
>
{
const
header
=
this
.
locators
.
sortColumnHeaderAt
(
columnIndex
);
const
ariaSort
=
await
header
.
getAttribute
(
'
aria-sort
'
);
if
(
ariaSort
===
'
ascending
'
)
return
'
ascend
'
;
if
(
ariaSort
===
'
descending
'
)
return
'
descend
'
;
const
upActive
=
await
header
.
locator
(
'
.ant-table-column-sorter-up
'
)
.
evaluate
((
el
)
=>
el
.
classList
.
contains
(
'
active
'
)
||
el
.
classList
.
contains
(
'
on
'
))
.
catch
(()
=>
false
);
if
(
upActive
)
return
'
ascend
'
;
const
downActive
=
await
header
.
locator
(
'
.ant-table-column-sorter-down
'
)
.
evaluate
((
el
)
=>
el
.
classList
.
contains
(
'
active
'
)
||
el
.
classList
.
contains
(
'
on
'
))
.
catch
(()
=>
false
);
if
(
downActive
)
return
'
descend
'
;
const
thSorted
=
await
header
.
evaluate
((
th
)
=>
(
th
as
HTMLElement
).
classList
.
contains
(
'
ant-table-column-sort
'
))
.
catch
(()
=>
false
);
if
(
thSorted
)
return
'
ascend
'
;
return
'
none
'
;
return
readColumnSortOrder
(
this
.
locators
.
sortColumnHeaderAt
(
columnIndex
));
}
private
async
clickSortUntilOrder
(
columnIndex
:
number
,
columnName
:
string
,
target
:
ColumnSortOrder
,
target
:
'
ascend
'
|
'
descend
'
,
):
Promise
<
void
>
{
const
header
=
this
.
locators
.
columnHeaderCell
(
columnName
);
const
headerAtIndex
=
this
.
locators
.
sortColumnHeaderAt
(
columnIndex
);
const
clickTarget
=
(
await
header
.
count
())
>
0
?
header
:
headerAtIndex
;
const
sorter
=
this
.
locators
.
columnSorter
(
columnName
);
const
sorterAtIndex
=
this
.
locators
.
sortColumnSorterAt
(
columnIndex
);
await
expect
(
clickTarget
,
`列「
${
columnName
}
」表头不可见`
).
toBeVisible
({
timeout
:
15
_000
});
await
headerAtIndex
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
headerAtIndex
.
waitFor
({
state
:
'
attached
'
,
timeout
:
15
_000
});
for
(
let
attempt
=
0
;
attempt
<
4
;
attempt
+=
1
)
{
for
(
let
attempt
=
0
;
attempt
<
6
;
attempt
+=
1
)
{
if
((
await
this
.
readSortOrderAt
(
columnIndex
))
===
target
)
{
return
;
}
await
test
.
step
(
`点击
${
columnName
}
列排序(第
${
attempt
+
1
}
次)`
,
async
()
=>
{
if
((
await
sorter
.
count
())
>
0
&&
(
await
sorter
.
isVisible
().
catch
(()
=>
false
)))
{
await
sorter
.
click
({
force
:
true
});
}
else
if
((
await
sorterAtIndex
.
count
())
>
0
&&
(
await
sorterAtIndex
.
isVisible
().
catch
(()
=>
false
)))
{
await
sorterAtIndex
.
click
({
force
:
true
});
}
else
{
await
clickTarget
.
click
({
force
:
true
});
}
await
this
.
waitForSortDone
();
});
await
this
.
clickColumnSort
(
columnName
,
target
);
}
expect
(
await
this
.
readSortOrderAt
(
columnIndex
),
`列「
${
columnName
}
」未能切换到
${
target
===
'
ascend
'
?
'
升序
'
:
'
降序
'
}
`
).
toBe
(
target
,
...
...
@@ -3220,8 +3285,14 @@ export class CenterReportSharedPage extends BasePage {
});
}
/** 解析列设置齿轮(
多策略 + 横向滚动 + 等待工具条渲染
)。 */
/** 解析列设置齿轮(
齿轮已在视口则跳过整表横滚
)。 */
protected
async
resolveColumnSettingsTrigger
(
tableWrapperIndex
:
number
):
Promise
<
Locator
>
{
const
already
=
await
findUsableColumnSettingsTrigger
(
this
.
columnSettingsTriggerCandidates
(
tableWrapperIndex
),
);
if
(
already
)
{
return
already
;
}
await
this
.
revealColumnSettingsTriggerByScroll
(
tableWrapperIndex
);
let
trigger
=
this
.
columnSettingsTriggerCandidates
(
tableWrapperIndex
)[
0
]
!
;
let
scrollPasses
=
0
;
...
...
@@ -3244,27 +3315,27 @@ export class CenterReportSharedPage extends BasePage {
return
trigger
;
}
/** 面板仍可见时强制关闭(Escape / 点
外侧
/ 再点齿轮)。 */
/** 面板仍可见时强制关闭(Escape / 点
产品名称
/ 再点齿轮
;禁止点查询
)。 */
protected
async
forceDismissColumnSettingsPanel
(
tableWrapperIndex
:
number
):
Promise
<
void
>
{
const
panel
=
this
.
locators
.
columnSettingsPanel
();
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
{
return
;
}
await
test
.
step
(
`强制关闭列设置(table=
${
tableWrapperIndex
}
)`
,
async
()
=>
{
for
(
let
i
=
0
;
i
<
5
;
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
4
;
i
+=
1
)
{
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
{
break
;
}
await
this
.
locators
.
report
.
locator
(
'
body
'
).
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
await
this
.
locators
.
searchBtn
.
click
({
force
:
true
}).
catch
(()
=>
undefined
);
await
this
.
locators
.
productName
.
click
({
force
:
true
,
timeout
:
3
_000
}).
catch
(()
=>
undefined
);
for
(
const
loc
of
this
.
columnSettingsTriggerCandidates
(
tableWrapperIndex
))
{
if
((
await
loc
.
count
())
>
0
)
{
await
loc
.
first
().
click
({
force
:
true
}).
catch
(()
=>
undefined
);
await
loc
.
first
().
click
({
force
:
true
,
timeout
:
3
_000
}).
catch
(()
=>
undefined
);
break
;
}
}
await
panel
.
waitFor
({
state
:
'
hidden
'
,
timeout
:
2
_000
}).
catch
(()
=>
undefined
);
await
panel
.
waitFor
({
state
:
'
hidden
'
,
timeout
:
1
_000
}).
catch
(()
=>
undefined
);
}
});
}
...
...
@@ -3273,12 +3344,8 @@ export class CenterReportSharedPage extends BasePage {
async
openColumnSettingsForTable
(
tableWrapperIndex
:
number
):
Promise
<
void
>
{
const
panel
=
this
.
locators
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
closeColumnSettingsForTable
(
tableWrapperIndex
);
}
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
forceDismissColumnSettingsPanel
(
tableWrapperIndex
);
return
;
}
await
panel
.
waitFor
({
state
:
'
hidden
'
,
timeout
:
3
_000
}).
catch
(()
=>
undefined
);
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
(
tableWrapperIndex
);
await
test
.
step
(
`打开列设置(table=
${
tableWrapperIndex
}
)`
,
async
()
=>
{
await
trigger
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
...
...
@@ -3300,17 +3367,19 @@ export class CenterReportSharedPage extends BasePage {
return
;
}
await
test
.
step
(
`关闭列设置(table=
${
tableWrapperIndex
}
)`
,
async
()
=>
{
for
(
let
i
=
0
;
i
<
6
;
i
+=
1
)
{
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
await
this
.
locators
.
report
.
locator
(
'
body
'
).
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
for
(
let
i
=
0
;
i
<
4
;
i
+=
1
)
{
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
{
break
;
}
for
(
const
loc
of
this
.
columnSettingsTriggerCandidates
(
tableWrapperIndex
))
{
if
((
await
loc
.
count
())
>
0
)
{
await
loc
.
first
().
click
({
force
:
true
}).
catch
(()
=>
undefined
);
await
loc
.
first
().
click
({
force
:
true
,
timeout
:
3
_000
}).
catch
(()
=>
undefined
);
break
;
}
}
await
panel
.
waitFor
({
state
:
'
hidden
'
,
timeout
:
2
_000
}).
catch
(()
=>
undefined
);
await
panel
.
waitFor
({
state
:
'
hidden
'
,
timeout
:
1
_000
}).
catch
(()
=>
undefined
);
}
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
forceDismissColumnSettingsPanel
(
tableWrapperIndex
);
...
...
@@ -3397,16 +3466,20 @@ export class CenterReportSharedPage extends BasePage {
.
filter
({
hasText
:
headerLabel
});
if
(
visible
)
{
const
isShown
=
async
()
=>
(
await
sorterOrLeaf
.
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
anyHeader
.
first
().
isVisible
().
catch
(()
=>
false
));
await
this
.
scrollTableColumnIntoView
(
headerLabel
).
catch
(()
=>
undefined
);
await
expect
.
poll
(
async
()
=>
{
if
(
await
isShown
())
{
return
true
;
}
await
this
.
scrollTableColumnIntoView
(
headerLabel
).
catch
(()
=>
undefined
);
return
(
(
await
sorterOrLeaf
.
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
anyHeader
.
first
().
isVisible
().
catch
(()
=>
false
))
);
return
isShown
();
},
{
timeout
:
15
_000
},
{
timeout
:
15
_000
,
intervals
:
[
500
,
1
_000
,
2
_000
]
},
)
.
toBeTruthy
();
return
;
...
...
@@ -3441,25 +3514,8 @@ export class CenterReportSharedPage extends BasePage {
async
pickToggleColumnNameForTable
(
tableWrapperIndex
:
number
):
Promise
<
string
>
{
const
panel
=
this
.
locators
.
columnSettingsPanel
();
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
(
tableWrapperIndex
);
const
wasOpen
=
await
panel
.
isVisible
().
catch
(()
=>
false
);
if
(
!
wasOpen
)
{
await
trigger
.
click
({
force
:
true
});
await
panel
.
waitFor
({
state
:
'
visible
'
,
timeout
:
15
_000
});
}
const
finishPick
=
async
(
name
:
string
):
Promise
<
string
>
=>
{
if
(
!
wasOpen
)
{
for
(
let
i
=
0
;
i
<
4
;
i
+=
1
)
{
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
break
;
await
trigger
.
click
({
force
:
true
});
}
}
return
name
;
};
await
this
.
openColumnSettingsForTable
(
tableWrapperIndex
);
const
mainIdx
=
this
.
data
.
tableWrapperIndex
??
0
;
const
detailIdx
=
this
.
data
.
detailTableWrapperIndex
??
mainIdx
;
const
candidates
=
tableWrapperIndex
===
mainIdx
?
(
this
.
data
.
leftColumnSettingsToggleCandidates
??
this
.
data
.
columnSettingsToggleCandidates
??
[])
...
...
@@ -3490,7 +3546,7 @@ export class CenterReportSharedPage extends BasePage {
.
catch
(()
=>
false
));
if
(
!
leafVisible
)
return
undefined
;
}
return
finishPick
(
name
)
;
return
name
;
};
for
(
const
name
of
candidates
)
{
...
...
@@ -3516,7 +3572,7 @@ export class CenterReportSharedPage extends BasePage {
if
(
Object
.
keys
(
assertMap
).
length
===
0
)
{
const
serialRow
=
this
.
locators
.
columnSettingsCheckboxRow
(
'
序号列
'
);
if
(
await
serialRow
.
isVisible
().
catch
(()
=>
false
))
{
return
await
finishPick
(
'
序号列
'
)
;
return
'
序号列
'
;
}
}
...
...
@@ -3533,6 +3589,28 @@ export class CenterReportSharedPage extends BasePage {
return
'
序号列
'
;
}
/** 场景:取消勾选非核心列后表头隐藏,再勾选后表头恢复。 */
async
executeColumnSettingsToggleRestoreTest
(
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
const
idx
=
tableWrapperIndex
??
this
.
defaultColumnSettingsTableIndex
();
await
executeColumnSettingsToggleRestore
({
pickToggleColumnName
:
()
=>
this
.
pickToggleColumnName
(
idx
),
openColumnSettings
:
()
=>
this
.
openColumnSettings
(
idx
),
setColumnVisibleInSettings
:
(
name
,
visible
)
=>
this
.
setColumnVisibleInSettings
(
name
,
visible
),
closeColumnSettingsPanel
:
()
=>
this
.
closeColumnSettingsPanel
(
idx
),
expectTableHeaderVisible
:
(
name
,
visible
)
=>
tableWrapperIndex
===
undefined
?
this
.
expectTableHeaderVisible
(
name
,
visible
)
:
this
.
expectTableHeaderVisibleInTable
(
name
,
visible
,
idx
),
expectSettingsCheckboxChecked
:
(
name
,
checked
)
=>
this
.
expectSettingsCheckboxChecked
(
name
,
checked
),
});
}
/** 双表页按 tableWrapperIndex 执行列设置恢复。 */
async
executeColumnSettingsToggleRestoreForTable
(
tableWrapperIndex
:
number
):
Promise
<
void
>
{
await
this
.
executeColumnSettingsToggleRestoreTest
(
tableWrapperIndex
);
}
async
getSubTableOrderNo
(
rowIndex
=
0
):
Promise
<
string
>
{
const
keywords
=
this
.
data
.
subTableOrderNoKeywords
??
[
'
单号
'
];
const
subTable
=
this
.
locators
.
expandedRow
.
locator
(
'
.ant-table
'
).
first
();
...
...
@@ -3563,6 +3641,34 @@ export class CenterReportSharedPage extends BasePage {
return
false
;
}
private
errorNoticeLocators
()
{
return
[
this
.
page
.
locator
(
'
.ant-message-error
'
),
this
.
page
.
locator
(
'
.ant-notification-notice-error
'
),
this
.
locators
.
report
.
locator
(
'
.ant-message-error
'
),
this
.
locators
.
report
.
locator
(
'
.ant-notification-notice-error
'
),
];
}
protected
async
isTransientUnknownError
():
Promise
<
boolean
>
{
for
(
const
locator
of
this
.
errorNoticeLocators
())
{
if
((
await
locator
.
count
())
===
0
)
continue
;
const
text
=
((
await
locator
.
first
().
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
''
);
if
(
/未知错误|请重试/
.
test
(
text
))
return
true
;
}
return
false
;
}
protected
async
dismissVisibleErrorNotice
():
Promise
<
void
>
{
for
(
const
locator
of
this
.
errorNoticeLocators
())
{
const
closer
=
locator
.
locator
(
'
.anticon-close, .ant-notification-notice-close
'
).
first
();
if
((
await
closer
.
count
())
>
0
)
{
await
closer
.
click
({
force
:
true
,
timeout
:
2
_000
}).
catch
(()
=>
undefined
);
}
}
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
).
catch
(()
=>
undefined
);
}
private
async
isOrderNoVisibleOnDetailSurfaces
(
orderNo
:
string
):
Promise
<
boolean
>
{
const
popupPages
=
this
.
page
.
context
().
pages
().
filter
((
p
)
=>
p
!==
this
.
page
);
const
surfaces
:
Array
<
{
getByText
:
(
text
:
string
,
options
?:
{
exact
:
boolean
})
=>
Locator
}
>
=
[
...
...
@@ -3631,31 +3737,52 @@ export class CenterReportSharedPage extends BasePage {
}
async
openSubTableViewDetail
(
rowIndex
=
0
):
Promise
<
void
>
{
const
viewBtn
=
this
.
locators
.
subTableViewButton
(
rowIndex
);
let
actionBtn
=
viewBtn
;
if
((
await
viewBtn
.
count
())
===
0
||
!
(
await
viewBtn
.
isVisible
().
catch
(()
=>
false
)))
{
const
link
=
this
.
locators
.
expandedRow
.
locator
(
'
a, button, span
'
)
.
filter
({
hasText
:
/^查看$/
})
.
first
();
if
((
await
link
.
count
())
>
0
&&
(
await
link
.
isVisible
().
catch
(()
=>
false
)))
{
actionBtn
=
link
;
}
else
{
await
this
.
guardedSkip
(
'
子表无「查看」按钮,无法跳转详情
'
);
}
}
const
actionBtn
=
await
this
.
revealSubTableViewButton
(
rowIndex
);
const
tabCountBefore
=
await
this
.
page
.
locator
(
'
.ant-tabs-tab
'
).
count
();
const
urlBefore
=
this
.
page
.
url
();
const
popupPromise
=
this
.
page
.
waitForEvent
(
'
popup
'
,
{
timeout
:
20
_000
}).
catch
(()
=>
null
);
const
orderNo
=
await
this
.
getSubTableOrderNo
(
rowIndex
);
await
expect
(
actionBtn
).
toBeVisible
(
);
await
expect
(
actionBtn
,
'
子表「查看」按钮应可见
'
).
toBeVisible
({
timeout
:
10
_000
}
);
await
this
.
click
(
actionBtn
,
'
点击子表查看
'
);
const
popup
=
await
popupPromise
;
await
this
.
expectViewDetailNavigation
(
orderNo
,
{
tabCountBefore
,
urlBefore
,
popup
});
}
/** 子表明细过宽时「查看」可能在视口外,先横向滚到操作列。 */
protected
async
revealSubTableViewButton
(
rowIndex
=
0
):
Promise
<
Locator
>
{
const
expanded
=
this
.
locators
.
expandedRow
;
await
expect
(
expanded
,
'
应已展开子表
'
).
toBeVisible
({
timeout
:
15
_000
});
const
scrollHosts
=
expanded
.
locator
(
'
.ant-table-body, .ant-table-content, .ant-table-scroll
'
);
const
hostCount
=
await
scrollHosts
.
count
();
for
(
let
i
=
0
;
i
<
hostCount
;
i
+=
1
)
{
await
scrollHosts
.
nth
(
i
)
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
}
const
row
=
this
.
locators
.
subTableRow
(
rowIndex
);
await
row
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
const
viewBtn
=
this
.
locators
.
subTableViewButton
(
rowIndex
);
await
viewBtn
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
if
((
await
viewBtn
.
count
())
>
0
&&
(
await
viewBtn
.
isVisible
().
catch
(()
=>
false
)))
{
return
viewBtn
;
}
const
fallback
=
expanded
.
locator
(
'
a, button, span, .ant-btn
'
)
.
filter
({
hasText
:
/查
\s
*看/
})
.
nth
(
rowIndex
);
await
fallback
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
if
((
await
fallback
.
count
())
>
0
&&
(
await
fallback
.
isVisible
().
catch
(()
=>
false
)))
{
return
fallback
;
}
await
this
.
guardedSkip
(
'
子表无「查看」按钮,无法跳转详情
'
);
return
viewBtn
;
}
/** 子表「查看」:详情可能以弹窗/抽屉/新 Tab 打开,放宽导航断言。 */
async
openSubTableViewDetailSoft
(
rowIndex
=
0
):
Promise
<
void
>
{
const
viewBtn
=
this
.
locators
.
subTableViewButton
(
rowIndex
);
...
...
Prev
1
2
3
4
5
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