Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
test
autotest-playwright-web-spd
Commits
6c31a476
Commit
6c31a476
authored
Oct 28, 2026
by
liguangyu06
Browse files
同步基础管理与统计报表用例文档,并更新 Playwright 脚本与 PO。
对齐 CSV/MD/测试点与现网列名及组合查询;补充厂商资质子 Tab 与基础模块复跑脚本。
parent
aa8987ce
Changes
350
Hide whitespace changes
Inline
Side-by-side
pages/spd/helpers/mock-search-from-config.helper.ts
View file @
6c31a476
...
@@ -41,7 +41,7 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro
...
@@ -41,7 +41,7 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro
break
;
break
;
}
}
}
}
if
(
primaryLabel
===
'
物料名称
'
||
primaryLabel
===
'
产品
名称
'
)
{
if
(
primaryLabel
===
'
物料名称
'
||
primaryLabel
===
'
物料
名称
'
)
{
for
(
const
extra
of
[
'
物料名称
'
,
'
物资名称
'
]
as
const
)
{
for
(
const
extra
of
[
'
物料名称
'
,
'
物资名称
'
]
as
const
)
{
const
item
=
deps
.
locators
.
quickFormItem
(
extra
);
const
item
=
deps
.
locators
.
quickFormItem
(
extra
);
if
((
await
item
.
count
())
>
0
)
{
if
((
await
item
.
count
())
>
0
)
{
...
@@ -50,8 +50,8 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro
...
@@ -50,8 +50,8 @@ export async function executeSearchWithMockFromConfig(deps: MockSearchDeps): Pro
}
}
}
}
}
}
if
(
/物料编号|物料编码|
产品
编号|物资编码/
.
test
(
primaryLabel
))
{
if
(
/物料编号|物料编码|
物料
编号|物资编码/
.
test
(
primaryLabel
))
{
for
(
const
extra
of
[
'
物料编码
'
,
'
物料编号
'
,
'
产品
编码
'
]
as
const
)
{
for
(
const
extra
of
[
'
物料编码
'
,
'
物料编号
'
,
'
物料
编码
'
]
as
const
)
{
const
item
=
deps
.
locators
.
quickFormItem
(
extra
);
const
item
=
deps
.
locators
.
quickFormItem
(
extra
);
if
((
await
item
.
count
())
>
0
)
{
if
((
await
item
.
count
())
>
0
)
{
label
=
extra
;
label
=
extra
;
...
...
pages/spd/helpers/report-summary.helper.ts
View file @
6c31a476
...
@@ -52,6 +52,20 @@ function summaryMetricPattern(label: string): RegExp {
...
@@ -52,6 +52,20 @@ function summaryMetricPattern(label: string): RegExp {
);
);
}
}
/** 优先解析全部/合计口径,避免勾选行时底部「选中」小计干扰列表合计勾稽。 */
export
function
parseSummaryMetricListTotal
(
bodyText
:
string
,
label
:
string
):
number
[]
{
const
compact
=
bodyText
.
replace
(
/
\s
+/g
,
'
'
);
const
preferred
:
number
[]
=
[];
for
(
const
prefix
of
[
'
全部
'
,
'
合计
'
,
'
总计
'
,
'
当前页
'
])
{
for
(
const
alias
of
summaryMetricLabelFallbacks
(
label
))
{
preferred
.
push
(...
parseSummaryMetricAll
(
compact
,
`
${
prefix
}${
alias
}
`
));
}
}
const
uniq
=
[...
new
Set
(
preferred
.
filter
((
v
)
=>
Number
.
isFinite
(
v
)))];
if
(
uniq
.
length
>
0
)
return
uniq
;
return
parseSummaryMetricAll
(
compact
,
label
);
}
/** 同一标签在底部可能同时出现选中合计与全部合计,全部返回。 */
/** 同一标签在底部可能同时出现选中合计与全部合计,全部返回。 */
export
function
parseSummaryMetricAll
(
bodyText
:
string
,
label
:
string
):
number
[]
{
export
function
parseSummaryMetricAll
(
bodyText
:
string
,
label
:
string
):
number
[]
{
const
compact
=
bodyText
.
replace
(
/
\s
+/g
,
'
'
);
const
compact
=
bodyText
.
replace
(
/
\s
+/g
,
'
'
);
...
...
pages/spd/pol-consumer.page.ts
View file @
6c31a476
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
type
{
DeptQuickFieldDef
}
from
'
../../data/dept-report-field-defs
'
;
import
type
{
DeptQuickFieldDef
}
from
'
../../data/dept-report-field-defs
'
;
import
{
polConsumerData
,
polConsumerExpandedQuickLabels
}
from
'
../../data/pol-consumer.data
'
;
import
{
polConsumerColumns
,
polConsumerData
,
polConsumerExpandedQuickLabels
,
}
from
'
../../data/pol-consumer.data
'
;
import
type
{
SampleRow
}
from
'
./center-report-shared.page
'
;
import
type
{
SampleRow
}
from
'
./center-report-shared.page
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
...
@@ -10,9 +14,12 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
...
@@ -10,9 +14,12 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
super
(
page
,
polConsumerData
);
super
(
page
,
polConsumerData
);
}
}
/** 展开后才出现的筛选项(
产品名称
)。 */
/** 展开后才出现的筛选项(
物料名称,原「物料名称」
)。 */
private
expandedFilterItem
()
{
private
expandedFilterItem
()
{
return
this
.
locators
.
quickFormItem
(
'
产品名称
'
);
return
this
.
locators
.
quickFormItem
(
'
物料名称
'
)
.
or
(
this
.
locators
.
quickFormItem
(
'
物料名称
'
))
.
first
();
}
}
async
ensureQuickFilterExpanded
():
Promise
<
void
>
{
async
ensureQuickFilterExpanded
():
Promise
<
void
>
{
...
@@ -33,7 +40,7 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
...
@@ -33,7 +40,7 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
await
super
.
applyQuickFields
(
sample
,
fields
,
options
);
await
super
.
applyQuickFields
(
sample
,
fields
,
options
);
}
}
/** STAT-PVC-028:收起隐藏
产品
名称等扩展条件,再展开后字段值保留。 */
/** STAT-PVC-028:收起隐藏
物料
名称等扩展条件,再展开后字段值保留。 */
async
expectFilterCollapseExpand
():
Promise
<
void
>
{
async
expectFilterCollapseExpand
():
Promise
<
void
>
{
await
this
.
ensureQuickFilterExpanded
();
await
this
.
ensureQuickFilterExpanded
();
const
extra
=
this
.
expandedFilterItem
();
const
extra
=
this
.
expandedFilterItem
();
...
@@ -51,7 +58,7 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
...
@@ -51,7 +58,7 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
}
}
/**
/**
* STAT-PVC-029:点击「完成数量」链接按钮 → 弹出消耗明细弹窗 → 核验
产品
编码 → 点 x 关闭。
* STAT-PVC-029:点击「完成数量」链接按钮 → 弹出消耗明细弹窗 → 核验
物料
编码 → 点 x 关闭。
*/
*/
async
expectFirstRowCompleteQtyDetailAndClose
():
Promise
<
void
>
{
async
expectFirstRowCompleteQtyDetailAndClose
():
Promise
<
void
>
{
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
...
@@ -64,10 +71,12 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
...
@@ -64,10 +71,12 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
const
columnIndex
=
await
this
.
mainColumnResizeControls
.
findColumnHeaderIndex
(
'
完成数量
'
);
const
columnIndex
=
await
this
.
mainColumnResizeControls
.
findColumnHeaderIndex
(
'
完成数量
'
);
expect
(
columnIndex
,
'
未找到列「完成数量」
'
).
toBeGreaterThanOrEqual
(
0
);
expect
(
columnIndex
,
'
未找到列「完成数量」
'
).
toBeGreaterThanOrEqual
(
0
);
const
productCodeCol
=
await
this
.
mainColumnResizeControls
.
findColumnHeaderIndex
(
'
产品编码
'
);
const
materialCodeLabel
=
polConsumerColumns
.
field11
;
const
materialCodeCol
=
await
this
.
mainColumnResizeControls
.
findColumnHeaderIndex
(
materialCodeLabel
);
const
maxScan
=
Math
.
min
(
await
this
.
locators
.
dataRows
.
count
(),
8
);
const
maxScan
=
Math
.
min
(
await
this
.
locators
.
dataRows
.
count
(),
8
);
let
targetBtn
:
Locator
|
undefined
;
let
targetBtn
:
Locator
|
undefined
;
let
product
Code
=
''
;
let
material
Code
=
''
;
for
(
let
i
=
0
;
i
<
maxScan
;
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
maxScan
;
i
+=
1
)
{
const
row
=
this
.
locators
.
dataRows
.
nth
(
i
);
const
row
=
this
.
locators
.
dataRows
.
nth
(
i
);
...
@@ -78,11 +87,11 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
...
@@ -78,11 +87,11 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
const
qty
=
Number
.
parseFloat
(
qtyText
.
replace
(
/,/g
,
''
));
const
qty
=
Number
.
parseFloat
(
qtyText
.
replace
(
/,/g
,
''
));
if
(
!
qtyText
||
Number
.
isNaN
(
qty
)
||
qty
<=
0
)
continue
;
if
(
!
qtyText
||
Number
.
isNaN
(
qty
)
||
qty
<=
0
)
continue
;
if
(
product
CodeCol
>=
0
)
{
if
(
material
CodeCol
>=
0
)
{
const
code
=
((
await
row
.
locator
(
'
td
'
).
nth
(
product
CodeCol
).
innerText
().
catch
(()
=>
''
))
??
''
)
const
code
=
((
await
row
.
locator
(
'
td
'
).
nth
(
material
CodeCol
).
innerText
().
catch
(()
=>
''
))
??
''
)
.
replace
(
/
\s
+/g
,
''
)
.
replace
(
/
\s
+/g
,
''
)
.
trim
();
.
trim
();
if
(
code
&&
code
!==
'
--
'
&&
code
!==
'
-
'
)
product
Code
=
code
;
if
(
code
&&
code
!==
'
--
'
&&
code
!==
'
-
'
)
material
Code
=
code
;
}
}
targetBtn
=
btn
;
targetBtn
=
btn
;
break
;
break
;
...
@@ -101,11 +110,15 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
...
@@ -101,11 +110,15 @@ export class PolConsumerPage extends ReconCostReportSharedPage {
await
expect
(
dialog
,
'
点击「完成数量」未弹出消耗明细页面
'
).
toBeVisible
({
timeout
:
20
_000
});
await
expect
(
dialog
,
'
点击「完成数量」未弹出消耗明细页面
'
).
toBeVisible
({
timeout
:
20
_000
});
expect
(
await
this
.
hasVisibleErrorNotice
()).
toBeFalsy
();
expect
(
await
this
.
hasVisibleErrorNotice
()).
toBeFalsy
();
if
(
productCode
)
{
if
(
materialCode
)
{
await
expect
(
dialog
.
getByText
(
/产品编码/
).
first
(),
'
消耗明细应展示产品编码
'
).
toBeVisible
({
const
detailCodeCaption
=
dialog
.
getByText
(
materialCodeLabel
,
{
exact
:
true
})
.
or
(
dialog
.
getByText
(
'
物料编码
'
,
{
exact
:
true
}))
.
first
();
await
expect
(
detailCodeCaption
,
'
消耗明细应展示物料编码
'
).
toBeVisible
({
timeout
:
10
_000
,
timeout
:
10
_000
,
});
});
await
expect
(
dialog
,
'
消耗明细
产品
编码应与列表行一致
'
).
toContainText
(
product
Code
);
await
expect
(
dialog
,
'
消耗明细
物料
编码应与列表行一致
'
).
toContainText
(
material
Code
);
}
}
const
closeIcon
=
dialog
.
getByRole
(
'
img
'
,
{
name
:
'
close
'
});
const
closeIcon
=
dialog
.
getByRole
(
'
img
'
,
{
name
:
'
close
'
});
...
...
pages/spd/recon-cost-report-shared.page.ts
View file @
6c31a476
...
@@ -8,7 +8,10 @@ import type { SampleRow } from './center-report-shared.page';
...
@@ -8,7 +8,10 @@ import type { SampleRow } from './center-report-shared.page';
import
{
import
{
parseReportNumber
,
parseReportNumber
,
parseSummaryMetricAll
,
parseSummaryMetricAll
,
parseSummaryMetricListTotal
,
parseSummaryMetricWithFallbacks
,
readReportFooterText
,
readReportFooterText
,
SUMMARY_FOOTER_CSS
,
readSummaryMetricsFromText
,
readSummaryMetricsFromText
,
summaryMetricLabelFallbacks
,
summaryMetricLabelFallbacks
,
}
from
'
./helpers/report-summary.helper
'
;
}
from
'
./helpers/report-summary.helper
'
;
...
@@ -32,23 +35,6 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -32,23 +35,6 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
super
(
page
,
data
);
super
(
page
,
data
);
}
}
/** 写入 `data.defaultQuickDateRange`(模块 CSV 前置日期区间)。 */
protected
async
applyDefaultQuickDateRange
():
Promise
<
void
>
{
const
range
=
this
.
data
.
defaultQuickDateRange
;
if
(
!
range
)
return
;
await
this
.
fillQuickDateRange
(
range
.
label
,
range
.
start
,
range
.
end
);
}
override
async
open
():
Promise
<
void
>
{
await
super
.
open
();
await
this
.
applyDefaultQuickDateRange
();
}
override
async
search
():
Promise
<
void
>
{
await
this
.
applyDefaultQuickDateRange
();
await
super
.
search
();
}
/** 配置了默认日期区间时,始终按区间查询后再取样本(避免跳过 search 导致无数据)。 */
/** 配置了默认日期区间时,始终按区间查询后再取样本(避免跳过 search 导致无数据)。 */
override
async
searchAndExtractSample
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
SampleRow
>
{
override
async
searchAndExtractSample
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
SampleRow
>
{
if
(
!
this
.
data
.
defaultQuickDateRange
)
{
if
(
!
this
.
data
.
defaultQuickDateRange
)
{
...
@@ -102,8 +88,8 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -102,8 +88,8 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
}
}
/**
/**
* 取首行 → 按配置字段收窄 →
点选首行 → 底部合计与该行对应字段一致
。
* 取首行
样本
→ 按配置字段收窄
查询
→
当前页列表字段合计与底部汇总一致(非勾选行小计)
。
* 页面差异由 `data.firstRowSummaryMatch` 配置;财务产品进销存
等特殊收窄
可 override。
* 页面差异由 `data.firstRowSummaryMatch` 配置;财务产品进销存
、选中行勾稽等
可 override。
*/
*/
async
expectFirstRowMetricsMatchSummary
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
void
>
{
async
expectFirstRowMetricsMatchSummary
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
void
>
{
test
.
setTimeout
(
timeouts
.
comboTest
);
test
.
setTimeout
(
timeouts
.
comboTest
);
...
@@ -139,57 +125,47 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -139,57 +125,47 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
expect
(
await
this
.
locators
.
dataRows
.
count
(),
`按「
${
requiredValue
}
」收窄后主表无数据`
).
toBeGreaterThan
(
0
);
expect
(
await
this
.
locators
.
dataRows
.
count
(),
`按「
${
requiredValue
}
」收窄后主表无数据`
).
toBeGreaterThan
(
0
);
await
this
.
prepareExportRowSelection
();
await
this
.
uncheckAllMainTableRows
();
await
this
.
assertVisibleRowsMatchFooter
(
cfg
.
metrics
,
{
await
this
.
assertVisibleRowsMatchFooter
(
cfg
.
metrics
,
{
absAll
:
cfg
.
absAll
,
absAll
:
cfg
.
absAll
,
listTotalFooter
:
true
,
});
});
}
}
/**
/**
* 底部合计对照当前结果:
单
行比该行
,同页多行比可见行合计;跨页无法完整核对则 skip
。
* 底部合计对照当前结果:
指定
行比该行
;默认对查询结果全部页明细求和后再对照底部
。
*/
*/
protected
async
assertVisibleRowsMatchFooter
(
protected
async
assertVisibleRowsMatchFooter
(
metrics
:
readonly
{
key
:
string
;
label
:
string
;
summaryLabels
:
readonly
string
[];
abs
?:
boolean
}[],
metrics
:
readonly
{
key
:
string
;
label
:
string
;
summaryLabels
:
readonly
string
[];
abs
?:
boolean
}[],
options
?:
{
options
?:
{
absAll
?:
boolean
;
absAll
?:
boolean
;
readMetric
?:
(
labels
:
readonly
string
[])
=>
Promise
<
number
>
;
readMetric
?:
(
labels
:
readonly
string
[])
=>
Promise
<
number
>
;
/** 只对照这些行(勾选首行小计);不配则对照
当前页全部可见行
*/
/** 只对照这些行(
如
勾选首行小计);不配则对照
查询结果全部列表合计
*/
rowIndexes
?:
readonly
number
[];
rowIndexes
?:
readonly
number
[];
/** 底部优先读全部/合计口径(非选中小计) */
listTotalFooter
?:
boolean
;
},
},
):
Promise
<
void
>
{
):
Promise
<
void
>
{
const
visibleRows
=
await
this
.
locators
.
dataRows
.
count
();
const
visibleRows
=
await
this
.
locators
.
dataRows
.
count
();
expect
(
visibleRows
,
'
收窄后主表无数据
'
).
toBeGreaterThan
(
0
);
expect
(
visibleRows
,
'
收窄后主表无数据
'
).
toBeGreaterThan
(
0
);
const
indexes
=
options
?.
rowIndexes
??
Array
.
from
({
length
:
visibleRows
},
(
_
,
i
)
=>
i
);
if
(
!
options
?.
rowIndexes
)
{
const
total
=
await
this
.
readPaginationTotalCount
().
catch
(()
=>
visibleRows
);
if
(
total
>
visibleRows
)
{
await
this
.
skipWhenDataInsufficient
(
`收窄后共
${
total
}
条且跨页,当前页无法完整核对底部汇总`
);
}
}
const
rows
:
SampleRow
[]
=
[];
const
listTotals
=
options
?.
rowIndexes
for
(
const
i
of
indexes
)
{
?
this
.
sumRowsForMetrics
(
expect
(
i
,
'
对照行索引超出当前页
'
).
toBeLessThan
(
visibleRows
);
await
this
.
readRowsByIndexes
(
options
.
rowIndexes
,
visibleRows
),
rows
.
push
(
await
this
.
extractSampleRow
(
i
));
metrics
,
}
options
,
)
:
await
this
.
aggregateQueryMetricsAcrossPages
(
metrics
,
options
);
const
expected
:
Array
<
{
label
:
string
;
summaryLabels
:
readonly
string
[];
targets
:
number
[]
}
>
=
[];
const
expected
:
Array
<
{
label
:
string
;
summaryLabels
:
readonly
string
[];
targets
:
number
[]
}
>
=
[];
for
(
const
metric
of
metrics
)
{
for
(
const
metric
of
metrics
)
{
const
values
:
number
[]
=
[];
const
total
=
listTotals
[
metric
.
key
];
for
(
const
row
of
rows
)
{
if
(
total
===
undefined
||
!
Number
.
isFinite
(
total
))
continue
;
const
raw
=
row
[
metric
.
key
]?.
trim
();
const
rowOnly
=
options
?.
rowIndexes
?.
length
===
1
;
if
(
!
raw
)
continue
;
const
targets
=
rowOnly
?
[...
new
Set
([
total
])]
:
[
total
];
const
parsed
=
parseReportNumber
(
raw
);
if
(
!
Number
.
isFinite
(
parsed
))
continue
;
values
.
push
(
metric
.
abs
||
options
?.
absAll
?
Math
.
abs
(
parsed
)
:
parsed
);
}
if
(
values
.
length
===
0
)
continue
;
const
pageSum
=
values
.
reduce
((
sum
,
value
)
=>
sum
+
value
,
0
);
const
targets
=
[...
new
Set
([
pageSum
,
values
[
0
]])];
expected
.
push
({
label
:
metric
.
label
,
summaryLabels
:
metric
.
summaryLabels
,
targets
});
expected
.
push
({
label
:
metric
.
label
,
summaryLabels
:
metric
.
summaryLabels
,
targets
});
}
}
expect
(
expected
.
length
,
'
可见行
未解析到可对照的汇总字段
'
).
toBeGreaterThan
(
0
);
expect
(
expected
.
length
,
'
列表
未解析到可对照的汇总字段
'
).
toBeGreaterThan
(
0
);
const
closeEnough
=
(
actual
:
number
,
expectedValue
:
number
)
=>
{
const
closeEnough
=
(
actual
:
number
,
expectedValue
:
number
)
=>
{
const
tol
=
0.01
*
Math
.
max
(
Math
.
abs
(
expectedValue
),
1
);
const
tol
=
0.01
*
Math
.
max
(
Math
.
abs
(
expectedValue
),
1
);
...
@@ -201,7 +177,9 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -201,7 +177,9 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
async
()
=>
{
async
()
=>
{
const
mismatches
:
string
[]
=
[];
const
mismatches
:
string
[]
=
[];
for
(
const
item
of
expected
)
{
for
(
const
item
of
expected
)
{
const
candidates
=
await
this
.
readSummaryCandidates
(
item
.
summaryLabels
);
const
candidates
=
await
this
.
readSummaryCandidates
(
item
.
summaryLabels
,
{
listTotal
:
options
?.
listTotalFooter
??
!
options
?.
rowIndexes
,
});
if
(
options
?.
readMetric
)
{
if
(
options
?.
readMetric
)
{
const
custom
=
await
options
.
readMetric
(
item
.
summaryLabels
);
const
custom
=
await
options
.
readMetric
(
item
.
summaryLabels
);
if
(
Number
.
isFinite
(
custom
))
candidates
.
push
(
custom
);
if
(
Number
.
isFinite
(
custom
))
candidates
.
push
(
custom
);
...
@@ -215,15 +193,141 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -215,15 +193,141 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
}
}
return
mismatches
.
length
===
0
?
'
ok
'
:
mismatches
.
join
(
'
;
'
);
return
mismatches
.
length
===
0
?
'
ok
'
:
mismatches
.
join
(
'
;
'
);
},
},
{
timeout
:
15
_000
,
message
:
'
底部汇总未与
当前页或首行
合计一致
'
},
{
timeout
:
15
_000
,
message
:
'
底部汇总未与
查询结果列表
合计一致
'
},
)
)
.
toBe
(
'
ok
'
);
.
toBe
(
'
ok
'
);
}
}
/** 底部同一指标可能有选中合计与全部合计,两者都参与对照。 */
private
async
readRowsByIndexes
(
indexes
:
readonly
number
[],
visibleRows
:
number
):
Promise
<
SampleRow
[]
>
{
private
async
readSummaryCandidates
(
labels
:
readonly
string
[]):
Promise
<
number
[]
>
{
const
rows
:
SampleRow
[]
=
[];
const
footerText
=
await
this
.
readFirstRowFooterText
();
for
(
const
i
of
indexes
)
{
expect
(
i
,
'
对照行索引超出当前页
'
).
toBeLessThan
(
visibleRows
);
rows
.
push
(
await
this
.
extractSampleRow
(
i
));
}
return
rows
;
}
private
sumRowsForMetrics
(
rows
:
SampleRow
[],
metrics
:
readonly
{
key
:
string
;
label
:
string
;
summaryLabels
:
readonly
string
[];
abs
?:
boolean
}[],
options
?:
{
absAll
?:
boolean
},
):
Record
<
string
,
number
>
{
const
totals
:
Record
<
string
,
number
>
=
{};
for
(
const
metric
of
metrics
)
{
let
sum
=
0
;
let
hit
=
false
;
for
(
const
row
of
rows
)
{
const
raw
=
row
[
metric
.
key
]?.
trim
();
if
(
!
raw
)
continue
;
const
parsed
=
parseReportNumber
(
raw
);
if
(
!
Number
.
isFinite
(
parsed
))
continue
;
sum
+=
metric
.
abs
||
options
?.
absAll
?
Math
.
abs
(
parsed
)
:
parsed
;
hit
=
true
;
}
if
(
hit
)
totals
[
metric
.
key
]
=
sum
;
}
return
totals
;
}
/** 翻页累加查询结果全部明细行(底部合计为整单汇总,非当前页/非勾选小计)。 */
private
async
aggregateQueryMetricsAcrossPages
(
metrics
:
readonly
{
key
:
string
;
label
:
string
;
summaryLabels
:
readonly
string
[];
abs
?:
boolean
}[],
options
?:
{
absAll
?:
boolean
},
):
Promise
<
Record
<
string
,
number
>>
{
const
recordTotal
=
await
this
.
readPaginationTotalCount
().
catch
(()
=>
0
);
const
pageRows
=
await
this
.
locators
.
dataRows
.
count
();
const
estimatedPages
=
Math
.
ceil
(
recordTotal
/
Math
.
max
(
pageRows
,
1
));
const
maxPages
=
40
;
if
(
estimatedPages
>
maxPages
)
{
await
this
.
skipWhenDataInsufficient
(
`查询共
${
recordTotal
}
条约
${
estimatedPages
}
页,超出自动化逐页汇总上限
${
maxPages
}
`
,
);
}
const
startPageText
=
(
await
this
.
locators
.
paginationActiveItem
().
innerText
().
catch
(()
=>
'
1
'
)).
trim
();
const
startPage
=
Number
.
parseInt
(
startPageText
,
10
)
||
1
;
if
(
startPage
!==
1
&&
(
await
this
.
locators
.
paginationItem
(
1
).
count
())
>
0
)
{
await
this
.
goToPage
(
1
);
}
const
totals
:
Record
<
string
,
number
>
=
{};
let
pagesVisited
=
0
;
while
(
pagesVisited
<
maxPages
)
{
pagesVisited
+=
1
;
await
this
.
waitForTableReady
();
const
count
=
await
this
.
locators
.
dataRows
.
count
();
expect
(
count
,
`第
${
pagesVisited
}
页无数据行`
).
toBeGreaterThan
(
0
);
const
pageTotals
=
await
this
.
sumPrimaryTableMetricsOnPage
(
metrics
,
options
);
for
(
const
[
key
,
value
]
of
Object
.
entries
(
pageTotals
))
{
totals
[
key
]
=
(
totals
[
key
]
??
0
)
+
value
;
}
const
next
=
this
.
locators
.
paginationHost
().
locator
(
'
li.ant-pagination-next
'
);
if
((
await
next
.
count
())
===
0
)
break
;
const
disabled
=
await
next
.
evaluate
((
node
)
=>
node
.
classList
.
contains
(
'
ant-pagination-disabled
'
));
if
(
disabled
)
break
;
await
test
.
step
(
`汇总翻页:第
${
pagesVisited
+
1
}
页`
,
async
()
=>
{
await
this
.
click
(
next
.
locator
(
'
button, a
'
).
first
(),
'
下一页
'
);
});
await
this
.
locators
.
report
.
locator
(
'
.ant-spin-spinning
'
).
waitFor
({
state
:
'
hidden
'
,
timeout
:
30
_000
}).
catch
(()
=>
undefined
);
await
this
.
waitForTableReady
();
}
if
(
startPage
!==
1
&&
(
await
this
.
locators
.
paginationItem
(
startPage
).
count
())
>
0
)
{
await
this
.
goToPage
(
startPage
).
catch
(()
=>
undefined
);
}
return
totals
;
}
private
async
sumPrimaryTableMetricsOnPage
(
metrics
:
readonly
{
key
:
string
;
label
:
string
;
summaryLabels
:
readonly
string
[];
abs
?:
boolean
}[],
options
?:
{
absAll
?:
boolean
},
):
Promise
<
Record
<
string
,
number
>>
{
const
columns
=
metrics
.
map
((
metric
)
=>
({
key
:
metric
.
key
,
header
:
this
.
data
.
columns
[
metric
.
key
]
??
metric
.
label
,
abs
:
metric
.
abs
,
}));
const
fromDom
=
await
this
.
sumPrimaryTableColumnTotals
(
columns
,
options
);
const
domHasValue
=
metrics
.
some
((
metric
)
=>
Number
.
isFinite
(
fromDom
[
metric
.
key
])
&&
fromDom
[
metric
.
key
]
!
!==
0
);
if
(
domHasValue
)
return
fromDom
;
const
count
=
await
this
.
locators
.
dataRows
.
count
();
for
(
const
metric
of
metrics
)
{
await
this
.
scrollTableColumnIntoView
(
columns
.
find
((
c
)
=>
c
.
key
===
metric
.
key
)
!
.
header
).
catch
(
()
=>
undefined
,
);
}
return
this
.
sumRowsForMetrics
(
await
this
.
readRowsByIndexes
(
Array
.
from
({
length
:
count
},
(
_
,
i
)
=>
i
),
count
,
),
metrics
,
options
,
);
}
/** 底部同一指标可能有选中合计与全部合计;listTotal 时优先全部/合计口径。 */
private
async
readSummaryCandidates
(
labels
:
readonly
string
[],
options
?:
{
listTotal
?:
boolean
},
):
Promise
<
number
[]
>
{
const
values
:
number
[]
=
[];
const
values
:
number
[]
=
[];
if
(
options
?.
listTotal
)
{
for
(
const
label
of
labels
)
{
values
.
push
(...(
await
this
.
readFooterMetricChipValues
(
label
)));
}
const
footerText
=
await
this
.
readFirstRowFooterText
();
for
(
const
label
of
labels
)
{
const
parsed
=
parseSummaryMetricWithFallbacks
(
footerText
,
summaryMetricLabelFallbacks
(
label
));
if
(
Number
.
isFinite
(
parsed
))
values
.
push
(
parsed
);
values
.
push
(...
parseSummaryMetricListTotal
(
footerText
,
label
));
}
return
[...
new
Set
(
values
.
filter
((
v
)
=>
Number
.
isFinite
(
v
)))];
}
const
footerText
=
await
this
.
readFirstRowFooterText
();
for
(
const
label
of
labels
)
{
for
(
const
label
of
labels
)
{
for
(
const
alias
of
summaryMetricLabelFallbacks
(
label
))
{
for
(
const
alias
of
summaryMetricLabelFallbacks
(
label
))
{
values
.
push
(...
parseSummaryMetricAll
(
footerText
,
alias
));
values
.
push
(...
parseSummaryMetricAll
(
footerText
,
alias
));
...
@@ -234,6 +338,26 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -234,6 +338,26 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
return
[...
new
Set
(
values
)];
return
[...
new
Set
(
values
)];
}
}
/** 只读底部合计条上的指标,避免把分页「共 N 条」误当成数量合计。 */
private
async
readFooterMetricChipValues
(
label
:
string
):
Promise
<
number
[]
>
{
const
values
:
number
[]
=
[];
for
(
const
alias
of
summaryMetricLabelFallbacks
(
label
))
{
const
escaped
=
alias
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
chips
=
this
.
locators
.
report
.
locator
(
SUMMARY_FOOTER_CSS
)
.
filter
({
hasText
:
new
RegExp
(
escaped
)
});
const
parts
=
await
chips
.
allInnerTexts
().
catch
(()
=>
[]
as
string
[]);
for
(
const
part
of
parts
)
{
if
(
/共
\s
*
\d
+
\s
*条/
.
test
(
part
)
&&
!
new
RegExp
(
`
${
escaped
}
\\s*[=::]`
).
test
(
part
.
replace
(
/
\s
+/g
,
'
'
)))
{
continue
;
}
const
parsed
=
parseSummaryMetricWithFallbacks
(
part
,
summaryMetricLabelFallbacks
(
label
));
if
(
Number
.
isFinite
(
parsed
))
values
.
push
(
parsed
);
}
}
return
values
;
}
private
async
readFirstRowFooterText
():
Promise
<
string
>
{
private
async
readFirstRowFooterText
():
Promise
<
string
>
{
const
cfg
=
this
.
data
.
firstRowSummaryMatch
;
const
cfg
=
this
.
data
.
firstRowSummaryMatch
;
const
keywords
=
cfg
?.
footerKeywords
??
this
.
data
.
summaryKeywords
??
cfg
?.
metrics
.
map
((
m
)
=>
m
.
label
)
??
[];
const
keywords
=
cfg
?.
footerKeywords
??
this
.
data
.
summaryKeywords
??
cfg
?.
metrics
.
map
((
m
)
=>
m
.
label
)
??
[];
...
@@ -343,7 +467,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
...
@@ -343,7 +467,7 @@ export class ReconCostReportSharedPage extends DeptReportSharedPage {
}
}
/**
/**
* 点击首行【条码】→ 产品详情
产品
编号与该行一致 → 点 x 关闭。
* 点击首行【条码】→ 产品详情
物料
编号与该行一致 → 点 x 关闭。
*/
*/
async
expectBarcodeOpensProductDetailAndCloses
():
Promise
<
void
>
{
async
expectBarcodeOpensProductDetailAndCloses
():
Promise
<
void
>
{
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
...
...
pages/spd/recon-purchase-entry.page.ts
View file @
6c31a476
...
@@ -7,13 +7,6 @@ export class ReconPurchaseEntryPage extends ReconCostReportSharedPage {
...
@@ -7,13 +7,6 @@ export class ReconPurchaseEntryPage extends ReconCostReportSharedPage {
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
reconPurchaseEntryData
);
super
(
page
,
reconPurchaseEntryData
);
}
}
/**
* STAT-RPE-007:按首行产品收窄后,底部入库数量与当前页采购入库数量合计一致。
*/
async
expectFirstRowInboundQtyMatchesSummary
(
rowIndex
=
this
.
data
.
sampleRowIndex
):
Promise
<
void
>
{
await
this
.
expectFirstRowMetricsMatchSummary
(
rowIndex
);
}
}
}
export
const
RECONPURCHASEENTRY_PATH
=
reconPurchaseEntryData
.
reportPath
;
export
const
RECONPURCHASEENTRY_PATH
=
reconPurchaseEntryData
.
reportPath
;
pages/spd/surgery-base-pkg.page.ts
View file @
6c31a476
...
@@ -192,13 +192,41 @@ export class SurgeryBasePkgPage extends SurgerySharedPage {
...
@@ -192,13 +192,41 @@ export class SurgeryBasePkgPage extends SurgerySharedPage {
// ---- 行内操作 ----
// ---- 行内操作 ----
/** CSV SURG-BASE-020:首行编辑并回填,返回首行产品名称。 */
/** 选中左侧「类*」分类,使右侧呈现基础包列表(非包内产品明细)。 */
async
selectCategoryForBasePkgList
():
Promise
<
void
>
{
const
nodes
=
await
this
.
activeTreeNodes
();
const
limit
=
Math
.
min
(
await
nodes
.
count
(),
25
);
for
(
let
i
=
0
;
i
<
limit
;
i
+=
1
)
{
const
raw
=
((
await
nodes
.
nth
(
i
).
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
const
name
=
raw
.
replace
(
/
[
((
]\d
+
[
))
]
$/
,
''
);
if
(
!
name
||
name
.
startsWith
(
'
包
'
))
continue
;
await
this
.
clickTreeNode
(
name
);
const
row
=
await
this
.
rowWithAction
(
'
编 辑
'
);
if
(
!
row
)
continue
;
const
pkgName
=
(
await
this
.
tryCell
(
row
,
'
基础包名称
'
))
||
(
await
this
.
tryCell
(
row
,
'
包名称
'
));
if
(
pkgName
)
return
;
}
await
this
.
clickFirstTreeNode
();
}
/** CSV SURG-BASE-016:首行编辑并回填,返回当前行基础包名称。 */
async
openEditFirstRow
():
Promise
<
string
>
{
async
openEditFirstRow
():
Promise
<
string
>
{
const
name
=
await
this
.
tryCell
(
await
this
.
firstRow
(),
'
产品名称
'
);
await
this
.
selectCategoryForBasePkgList
();
await
this
.
click
(
this
.
rowAction
(
await
this
.
firstRow
(),
'
编 辑
'
),
'
编 辑
'
);
const
row
=
(
await
this
.
rowWithAction
(
'
编 辑
'
))
??
(
await
this
.
firstRow
());
await
this
.
expectDialogVisible
();
if
(
!
(
await
this
.
rowAction
(
row
,
'
编 辑
'
).
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
无可用「编辑」行
'
);
}
const
name
=
(
await
this
.
tryCell
(
row
,
'
基础包名称
'
))
||
(
await
this
.
tryCell
(
row
,
'
包名称
'
))
||
(
await
this
.
tryCell
(
row
,
'
产品名称
'
))
||
(
await
this
.
tryCell
(
row
,
'
物料名称
'
));
await
this
.
click
(
this
.
rowAction
(
row
,
'
编 辑
'
),
'
编 辑
'
);
await
this
.
waitSpinGone
();
await
this
.
waitSpinGone
();
return
name
;
const
pkgField
=
this
.
page
.
locator
(
'
#form_item_pkgName:visible
'
).
first
();
await
expect
(
pkgField
.
or
(
this
.
dialog
()),
'
编辑弹窗或基础包名称表单应可见
'
).
toBeVisible
({
timeout
:
15
_000
});
await
this
.
waitSpinGone
();
return
name
||
(
await
pkgField
.
inputValue
().
catch
(()
=>
''
));
}
}
/** CSV SURG-BASE-021:首行移除并取消,返回 { total, cancelShown }。 */
/** CSV SURG-BASE-021:首行移除并取消,返回 { total, cancelShown }。 */
...
...
pages/spd/surgery-shared.page.ts
View file @
6c31a476
...
@@ -351,8 +351,17 @@ export class SurgerySharedPage extends BasePage {
...
@@ -351,8 +351,17 @@ export class SurgerySharedPage extends BasePage {
// ---------- 列表取数 ----------
// ---------- 列表取数 ----------
/** 右侧构成/术种列表区域(避免命中左树或其它嵌套表)。 */
mainListScope
():
Locator
{
return
this
.
page
.
locator
(
'
.operation-right, .xc-layout-lr-right
'
).
first
();
}
dataRows
():
Locator
{
dataRows
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)
'
);
const
scoped
=
this
.
mainListScope
().
locator
(
'
.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)
'
,
);
const
fallback
=
this
.
page
.
locator
(
'
.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)
'
);
return
scoped
.
or
(
fallback
);
}
}
async
getDataRowCount
():
Promise
<
number
>
{
async
getDataRowCount
():
Promise
<
number
>
{
...
@@ -360,9 +369,24 @@ export class SurgerySharedPage extends BasePage {
...
@@ -360,9 +369,24 @@ export class SurgerySharedPage extends BasePage {
}
}
async
getTotalCount
():
Promise
<
number
>
{
async
getTotalCount
():
Promise
<
number
>
{
const
text
=
(
await
this
.
locators
.
paginationTotal
.
textContent
().
catch
(()
=>
''
))
??
''
;
const
scope
=
this
.
mainListScope
();
const
match
=
text
.
match
(
/共
\s
*
(\d
+
)\s
*条/
);
const
emptyHint
=
return
match
?
Number
(
match
[
1
])
:
0
;
(
await
scope
.
getByText
(
/暂无数据|请选择相应包/
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
this
.
locators
.
empty
.
isVisible
().
catch
(()
=>
false
));
const
rowCount
=
await
scope
.
locator
(
'
.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)
'
)
.
count
()
.
catch
(()
=>
0
);
if
(
emptyHint
&&
rowCount
===
0
)
return
0
;
for
(
const
paginationScope
of
[
scope
,
this
.
page
.
locator
(
'
.xc-content
'
).
first
()])
{
const
texts
=
await
paginationScope
.
locator
(
'
.ant-pagination-total-text
'
).
allTextContents
().
catch
(()
=>
[]);
for
(
const
raw
of
texts
)
{
const
match
=
raw
.
match
(
/共
\s
*
(\d
+
)\s
*条/
);
if
(
match
)
return
Number
(
match
[
1
]);
}
}
return
0
;
}
}
async
waitForTableReady
(
options
?:
{
health
?:
'
ui
'
|
'
business
'
}):
Promise
<
void
>
{
async
waitForTableReady
(
options
?:
{
health
?:
'
ui
'
|
'
business
'
}):
Promise
<
void
>
{
...
@@ -372,6 +396,9 @@ export class SurgerySharedPage extends BasePage {
...
@@ -372,6 +396,9 @@ export class SurgerySharedPage extends BasePage {
}
}
async
headerCells
():
Promise
<
Locator
>
{
async
headerCells
():
Promise
<
Locator
>
{
const
headers
=
this
.
mainListScope
().
locator
(
'
.ant-table-thead th
'
);
await
headers
.
first
().
waitFor
({
state
:
'
attached
'
,
timeout
:
12
_000
}).
catch
(()
=>
undefined
);
if
((
await
headers
.
count
())
>
0
)
return
headers
;
await
this
.
locators
.
tableHeader
.
first
().
waitFor
({
state
:
'
attached
'
,
timeout
:
12
_000
}).
catch
(()
=>
undefined
);
await
this
.
locators
.
tableHeader
.
first
().
waitFor
({
state
:
'
attached
'
,
timeout
:
12
_000
}).
catch
(()
=>
undefined
);
return
this
.
locators
.
tableHeader
;
return
this
.
locators
.
tableHeader
;
}
}
...
@@ -405,18 +432,32 @@ export class SurgerySharedPage extends BasePage {
...
@@ -405,18 +432,32 @@ export class SurgerySharedPage extends BasePage {
}
}
async
firstRow
():
Promise
<
Locator
>
{
async
firstRow
():
Promise
<
Locator
>
{
const
row
=
this
.
dataRows
().
first
();
const
rows
=
this
.
dataRows
();
await
expect
(
row
,
'
列表至少应有 1 条数据
'
).
toBeVisible
({
timeout
:
15
_000
});
const
count
=
await
rows
.
count
();
return
row
;
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
row
=
rows
.
nth
(
i
);
if
(
await
row
.
isVisible
().
catch
(()
=>
false
))
{
await
expect
(
row
,
'
列表至少应有 1 条可见数据
'
).
toBeVisible
({
timeout
:
15
_000
});
return
row
;
}
}
await
expect
(
rows
.
first
(),
'
列表至少应有 1 条数据
'
).
toBeVisible
({
timeout
:
15
_000
});
return
rows
.
first
();
}
}
/** 首屏某列样例值(跳过占位),数据不足时 skip。 */
/** 首屏某列样例值(跳过占位),数据不足时 skip。 */
async
sampleCell
(
header
:
string
):
Promise
<
string
>
{
async
sampleCell
(
header
:
string
):
Promise
<
string
>
{
const
rows
=
this
.
dataRows
();
const
rows
=
this
.
dataRows
();
const
count
=
Math
.
min
(
await
rows
.
count
(),
8
);
const
count
=
Math
.
min
(
await
rows
.
count
(),
15
);
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
for
(
let
pass
=
0
;
pass
<
2
;
pass
+=
1
)
{
const
text
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
rows
.
nth
(
i
),
header
));
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
if
(
text
)
return
text
;
const
row
=
rows
.
nth
(
i
);
if
(
pass
===
0
&&
(
await
row
.
locator
(
'
.ant-table-row-expand-icon
'
).
isVisible
().
catch
(()
=>
false
)))
{
continue
;
}
const
text
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
header
));
if
(
text
)
return
text
;
}
}
}
await
this
.
skipWhenDataInsufficient
(
`首屏「
${
header
}
」为空`
);
await
this
.
skipWhenDataInsufficient
(
`首屏「
${
header
}
」为空`
);
return
''
;
return
''
;
...
@@ -425,15 +466,21 @@ export class SurgerySharedPage extends BasePage {
...
@@ -425,15 +466,21 @@ export class SurgerySharedPage extends BasePage {
/** 抽样行某列是否均含关键字(用例层断言)。 */
/** 抽样行某列是否均含关键字(用例层断言)。 */
async
rowsMatchKeyword
(
header
:
string
,
keyword
:
string
,
sample
=
5
):
Promise
<
boolean
>
{
async
rowsMatchKeyword
(
header
:
string
,
keyword
:
string
,
sample
=
5
):
Promise
<
boolean
>
{
const
needle
=
keyword
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
const
needle
=
keyword
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
if
(
!
needle
)
return
false
;
const
rows
=
this
.
dataRows
();
const
rows
=
this
.
dataRows
();
const
count
=
await
rows
.
count
();
const
count
=
await
rows
.
count
();
if
(
count
===
0
)
return
false
;
if
(
count
===
0
)
return
false
;
const
limit
=
Math
.
min
(
count
,
sample
);
let
checked
=
0
;
for
(
let
i
=
0
;
i
<
limit
;
i
+=
1
)
{
for
(
let
i
=
0
;
i
<
count
&&
checked
<
sample
;
i
+=
1
)
{
const
text
=
(
await
this
.
tryCell
(
rows
.
nth
(
i
),
header
)).
replace
(
/
\s
/g
,
''
);
const
row
=
rows
.
nth
(
i
);
const
text
=
(
await
this
.
tryCell
(
row
,
header
)).
replace
(
/
\s
/g
,
''
);
if
(
!
text
)
{
if
(
await
row
.
locator
(
'
.ant-table-row-expand-icon
'
).
isVisible
().
catch
(()
=>
false
))
continue
;
}
checked
+=
1
;
if
(
!
text
.
includes
(
needle
))
return
false
;
if
(
!
text
.
includes
(
needle
))
return
false
;
}
}
return
true
;
return
checked
>
0
;
}
}
/** 列表中是否存在包含关键字的行(跨展开/分组)。 */
/** 列表中是否存在包含关键字的行(跨展开/分组)。 */
...
@@ -509,7 +556,12 @@ export class SurgerySharedPage extends BasePage {
...
@@ -509,7 +556,12 @@ export class SurgerySharedPage extends BasePage {
rowAction
(
row
:
Locator
,
name
:
string
):
Locator
{
rowAction
(
row
:
Locator
,
name
:
string
):
Locator
{
const
compact
=
name
.
replace
(
/
\s
/g
,
''
);
const
compact
=
name
.
replace
(
/
\s
/g
,
''
);
const
pattern
=
new
RegExp
(
compact
.
split
(
''
).
join
(
'
\\
s*
'
));
const
pattern
=
new
RegExp
(
compact
.
split
(
''
).
join
(
'
\\
s*
'
));
return
row
.
locator
(
'
td
'
).
last
().
getByRole
(
'
button
'
,
{
name
:
pattern
}).
first
();
const
cell
=
row
.
locator
(
'
td
'
).
last
();
return
cell
.
getByRole
(
'
button
'
,
{
name
:
pattern
})
.
or
(
cell
.
getByRole
(
'
link
'
,
{
name
:
pattern
}))
.
or
(
cell
.
locator
(
'
a,button,span
'
).
filter
({
hasText
:
pattern
}))
.
first
();
}
}
async
rowWithAction
(
name
:
string
):
Promise
<
Locator
|
null
>
{
async
rowWithAction
(
name
:
string
):
Promise
<
Locator
|
null
>
{
...
@@ -575,7 +627,9 @@ export class SurgerySharedPage extends BasePage {
...
@@ -575,7 +627,9 @@ export class SurgerySharedPage extends BasePage {
}
}
async
expectDialogVisible
():
Promise
<
void
>
{
async
expectDialogVisible
():
Promise
<
void
>
{
await
expect
(
this
.
dialog
(),
'
弹窗应可见
'
).
toBeVisible
({
timeout
:
15
_000
});
const
modal
=
this
.
page
.
locator
(
'
.ant-modal-wrap:not([style*="display: none"]) .ant-modal-content
'
).
last
();
const
drawer
=
this
.
page
.
locator
(
'
.ant-drawer-open .ant-drawer-content
'
).
last
();
await
expect
(
modal
.
or
(
drawer
).
or
(
this
.
dialog
()),
'
弹窗应可见
'
).
toBeVisible
({
timeout
:
15
_000
});
}
}
async
closeDialogIfOpen
():
Promise
<
void
>
{
async
closeDialogIfOpen
():
Promise
<
void
>
{
...
...
pages/spd/surgery-std-pkg.page.ts
View file @
6c31a476
...
@@ -51,7 +51,15 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -51,7 +51,15 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
// ---- 筛选 ----
// ---- 筛选 ----
async
searchByProductName
():
Promise
<
string
>
{
async
searchByProductName
():
Promise
<
string
>
{
return
this
.
searchByFirstRow
(
'
#table_search_form_keyword
'
,
'
产品名称
'
);
let
keyword
=
''
;
try
{
keyword
=
await
this
.
sampleCell
(
'
物料名称
'
);
}
catch
{
keyword
=
await
this
.
sampleCell
(
'
产品名称
'
);
}
await
this
.
fillFilterById
(
'
#table_search_form_keyword
'
,
keyword
);
await
this
.
query
();
return
keyword
;
}
}
async
searchBySpecModel
():
Promise
<
string
>
{
async
searchBySpecModel
():
Promise
<
string
>
{
...
@@ -62,6 +70,13 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -62,6 +70,13 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
return
this
.
searchByFirstRow
(
'
#table_search_form_goodsMfrsName
'
,
'
器械注册人
'
);
return
this
.
searchByFirstRow
(
'
#table_search_form_goodsMfrsName
'
,
'
器械注册人
'
);
}
}
/** 构成列表关键字匹配:兼容分组行,优先整表包含判断。 */
async
rowsMatchKeyword
(
header
:
string
,
keyword
:
string
,
sample
=
5
):
Promise
<
boolean
>
{
const
needle
=
keyword
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
if
(
needle
&&
(
await
this
.
hasRowContaining
(
needle
)))
return
true
;
return
super
.
rowsMatchKeyword
(
header
,
keyword
,
sample
);
}
/** CSV SURG-STD-015:mock 反例空态。 */
/** CSV SURG-STD-015:mock 反例空态。 */
async
searchMockEmpty
():
Promise
<
void
>
{
async
searchMockEmpty
():
Promise
<
void
>
{
await
this
.
fillFilterById
(
'
#table_search_form_keyword
'
,
surgeryData
.
mockStdKeyword
);
await
this
.
fillFilterById
(
'
#table_search_form_keyword
'
,
surgeryData
.
mockStdKeyword
);
...
@@ -163,9 +178,12 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -163,9 +178,12 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
return
match
?
Number
(
match
[
1
])
:
null
;
return
match
?
Number
(
match
[
1
])
:
null
;
}
}
/** 弹窗内
基础包
勾选框(首列 checkbox)。 */
/** 弹窗内
当前可见表格
勾选框(首列 checkbox)。 */
dialogCheckboxes
():
Locator
{
dialogCheckboxes
():
Locator
{
return
this
.
dialog
().
locator
(
'
.ant-table-tbody input[type="checkbox"], .ant-checkbox-input
'
);
const
dialog
=
this
.
dialog
();
const
activePane
=
dialog
.
locator
(
'
.ant-tabs-tabpane-active
'
).
last
();
const
table
=
activePane
.
locator
(
'
.ant-table-tbody
'
).
first
().
or
(
dialog
.
locator
(
'
.ant-table-tbody
'
).
first
());
return
table
.
locator
(
'
input[type="checkbox"], .ant-checkbox-input
'
);
}
}
/**
/**
...
@@ -288,11 +306,36 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -288,11 +306,36 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
// ---- 添加散件弹窗 ----
// ---- 添加散件弹窗 ----
/** 添加散件前需选中左树具体套包,否则按钮不可点或弹窗不出。 */
private
async
ensurePackageContextForLoosePart
():
Promise
<
void
>
{
if
((
await
this
.
getDataRowCount
())
>
0
)
return
;
const
matched
=
await
this
.
firstCountedTreeNodeMatchingList
();
if
(
matched
)
return
;
const
nodes
=
await
this
.
activeTreeNodes
();
const
count
=
Math
.
min
(
await
nodes
.
count
(),
24
);
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
text
=
((
await
nodes
.
nth
(
i
).
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
if
(
text
.
includes
(
'
包
'
))
{
await
this
.
click
(
nodes
.
nth
(
i
),
'
选择套包节点
'
);
await
this
.
waitForTableReady
();
return
;
}
}
await
this
.
clickTreeNode
(
await
this
.
firstTreeNodeText
());
await
this
.
waitForTableReady
();
}
/** CSV SURG-STD-022:打开添加散件弹窗(产品列表视图)。 */
/** CSV SURG-STD-022:打开添加散件弹窗(产品列表视图)。 */
async
openAddLoosePartDialog
():
Promise
<
void
>
{
async
openAddLoosePartDialog
():
Promise
<
void
>
{
await
this
.
click
(
this
.
addLoosePartBtn
(),
'
添加散件
'
);
await
this
.
ensurePackageContextForLoosePart
();
const
btn
=
this
.
addLoosePartBtn
();
await
expect
(
btn
,
'
添加散件
'
).
toBeVisible
({
timeout
:
15
_000
});
await
this
.
click
(
btn
,
'
添加散件
'
);
await
this
.
expectDialogVisible
();
await
this
.
expectDialogVisible
();
await
this
.
waitSpinGone
();
await
this
.
waitSpinGone
();
await
expect
(
this
.
dialog
().
locator
(
'
.ant-table-tbody tr
'
).
first
(),
'
散件弹窗列表应渲染
'
).
toBeVisible
({
timeout
:
15
_000
,
}).
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
500
);
await
this
.
waitSettle
(
500
);
}
}
...
@@ -319,16 +362,27 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -319,16 +362,27 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
candidateRows
:
number
;
candidateRows
:
number
;
selectedCount
:
number
;
selectedCount
:
number
;
}
>
{
}
>
{
const
keyword
=
await
this
.
sampleCell
(
'
产品名称
'
);
let
keyword
=
''
;
try
{
keyword
=
await
this
.
sampleCell
(
'
物料名称
'
);
}
catch
{
keyword
=
await
this
.
sampleCell
(
'
产品名称
'
);
}
const
search
=
this
.
loosePartProductSearch
();
const
search
=
this
.
loosePartProductSearch
();
if
(
await
search
.
isVisible
().
catch
(()
=>
false
))
{
if
(
await
search
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
search
,
keyword
,
`填写产品名称筛选「
${
keyword
}
」`
);
await
this
.
fill
(
search
,
keyword
,
`填写产品名称筛选「
${
keyword
}
」`
);
await
search
.
press
(
'
Enter
'
).
catch
(()
=>
undefined
);
await
search
.
press
(
'
Enter
'
).
catch
(()
=>
undefined
);
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
600
);
await
this
.
waitSettle
(
600
);
}
}
const
candidateRows
=
await
this
.
dialog
().
locator
(
'
.ant-table-tbody tr.ant-table-row
'
).
count
();
const
productPane
=
this
.
dialog
().
locator
(
'
.ant-tabs-tabpane-active
'
).
last
();
const
candidateRows
=
await
productPane
.
locator
(
'
.ant-table-tbody tr.ant-table-row
'
).
count
();
if
(
candidateRows
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
散件弹窗无匹配产品行
'
);
}
const
checkbox
=
this
.
dialogCheckboxes
().
first
();
const
checkbox
=
this
.
dialogCheckboxes
().
first
();
await
this
.
click
(
checkbox
,
'
勾选首个散件
'
);
await
this
.
click
(
checkbox
,
'
勾选首个散件
'
);
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
400
);
await
this
.
waitSettle
(
400
);
const
selectedCount
=
await
this
.
loosePartSelectedCount
();
const
selectedCount
=
await
this
.
loosePartSelectedCount
();
return
{
keyword
,
candidateRows
,
selectedCount
};
return
{
keyword
,
candidateRows
,
selectedCount
};
...
@@ -351,20 +405,40 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -351,20 +405,40 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
* 返回明细视图是否可见、明细表头文本,供用例断言字段完整。
* 返回明细视图是否可见、明细表头文本,供用例断言字段完整。
*/
*/
async
switchLoosePartDetailView
():
Promise
<
{
visible
:
boolean
;
headers
:
string
[]
}
>
{
async
switchLoosePartDetailView
():
Promise
<
{
visible
:
boolean
;
headers
:
string
[]
}
>
{
const
tab
=
this
.
dialogViewTab
(
'
应拣明细
'
).
or
(
this
.
dialogViewTab
(
'
规格明细
'
));
const
dialog
=
this
.
dialog
();
if
(
await
tab
.
isVisible
().
catch
(()
=>
false
))
{
for
(
const
label
of
[
'
应拣明细
'
,
'
规格明细
'
])
{
await
this
.
click
(
tab
,
'
切换应拣明细视图
'
);
const
tab
=
dialog
await
this
.
waitSettle
(
400
);
.
locator
(
'
.ant-tabs-tab, .ant-radio-button-wrapper, .ant-segmented-item
'
)
.
filter
({
hasText
:
new
RegExp
(
label
)
})
.
first
();
if
(
await
tab
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
tab
,
`切换
${
label
}
视图`
);
await
this
.
waitSpinGone
();
break
;
}
}
}
const
headers
=
(
await
this
.
dialog
().
locator
(
'
.ant-table-thead th
'
).
allTextContents
()).
map
((
t
)
=>
t
.
replace
(
/
\s
/g
,
''
));
await
expect
(
dialog
.
getByText
(
/耗材品种数/
).
first
(),
'
应拣明细统计区应可见
'
).
toBeVisible
({
timeout
:
12
_000
}).
catch
(
()
=>
undefined
,
);
await
this
.
waitSettle
(
500
);
const
panel
=
dialog
.
locator
(
'
.ant-tabs-tabpane-active
'
).
last
();
const
headerCells
=
panel
.
locator
(
'
.ant-table-thead th
'
);
const
headers
=
(
await
headerCells
.
count
())
>
0
?
(
await
headerCells
.
allTextContents
()).
map
((
t
)
=>
t
.
replace
(
/
\s
/g
,
''
))
:
(
await
dialog
.
locator
(
'
.ant-table-thead th
'
).
allTextContents
()).
map
((
t
)
=>
t
.
replace
(
/
\s
/g
,
''
));
const
visible
=
headers
.
length
>
0
;
const
visible
=
headers
.
length
>
0
;
return
{
visible
,
headers
};
return
{
visible
,
headers
};
}
}
/** CSV SURG-STD-025:读取耗材品种数 / 耗材总数(结构化数值)。 */
/** CSV SURG-STD-025:读取耗材品种数 / 耗材总数(结构化数值)。 */
async
readLoosePartStats
():
Promise
<
{
kind
:
number
|
null
;
total
:
number
|
null
}
>
{
async
readLoosePartStats
():
Promise
<
{
kind
:
number
|
null
;
total
:
number
|
null
}
>
{
const
kindText
=
await
this
.
dialog
().
getByText
(
/耗材品种数/
).
first
().
innerText
().
catch
(()
=>
''
);
const
dialog
=
this
.
dialog
();
const
totalText
=
await
this
.
dialog
().
getByText
(
/耗材总数/
).
first
().
innerText
().
catch
(()
=>
''
);
await
expect
(
dialog
.
getByText
(
/耗材品种数/
).
first
(),
'
应拣明细应展示耗材品种数
'
).
toBeVisible
({
timeout
:
10
_000
,
}).
catch
(()
=>
undefined
);
const
kindText
=
await
dialog
.
getByText
(
/耗材品种数/
).
first
().
innerText
().
catch
(()
=>
''
);
const
totalText
=
await
dialog
.
getByText
(
/耗材总数/
).
first
().
innerText
().
catch
(()
=>
''
);
const
pick
=
(
text
:
string
)
=>
{
const
pick
=
(
text
:
string
)
=>
{
const
match
=
text
.
match
(
/
(\d
+
)
/
);
const
match
=
text
.
match
(
/
(\d
+
)
/
);
return
match
?
Number
(
match
[
1
])
:
null
;
return
match
?
Number
(
match
[
1
])
:
null
;
...
@@ -378,6 +452,7 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
...
@@ -378,6 +452,7 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
await
this
.
openAddLoosePartDialog
();
await
this
.
openAddLoosePartDialog
();
await
this
.
filterAndCheckLoosePart
();
await
this
.
filterAndCheckLoosePart
();
await
this
.
closeDialogIfOpen
();
await
this
.
closeDialogIfOpen
();
await
this
.
waitForTableReady
();
const
cancelOk
=
!
(
await
this
.
dialog
().
isVisible
().
catch
(()
=>
true
));
const
cancelOk
=
!
(
await
this
.
dialog
().
isVisible
().
catch
(()
=>
true
));
return
{
total
,
cancelOk
};
return
{
total
,
cancelOk
};
}
}
...
...
pages/spd/trace-purchase.page.ts
View file @
6c31a476
import
type
{
Page
}
from
'
@playwright/test
'
;
import
type
{
Page
}
from
'
@playwright/test
'
;
import
{
tracePurchaseData
}
from
'
../../data/trace-purchase.data
'
;
import
{
tracePurchaseData
,
tracePurchaseDomainQuickFields
,
}
from
'
../../data/trace-purchase.data
'
;
import
type
{
DeptQuickFieldDef
}
from
'
../../data/dept-report-field-defs
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
import
{
ReconCostReportSharedPage
}
from
'
./recon-cost-report-shared.page
'
;
export
class
TracePurchasePage
extends
ReconCostReportSharedPage
{
export
class
TracePurchasePage
extends
ReconCostReportSharedPage
{
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
tracePurchaseData
);
super
(
page
,
tracePurchaseData
);
}
}
/**
* STAT-TPR-013:默认查询取首行采购单号 → 按单号查询 → 导出。
*/
override
async
exportWithFirstRowSampleQuery
(
_quickFields
?:
readonly
DeptQuickFieldDef
[],
):
Promise
<
void
>
{
await
this
.
search
();
const
rowCount
=
await
this
.
locators
.
dataRows
.
count
();
if
(
rowCount
===
0
)
{
await
this
.
guardedSkip
(
'
列表无数据,无法取首行采购单号
'
);
}
const
sample
=
await
this
.
extractSampleRow
(
this
.
data
.
sampleRowIndex
);
const
orderNo
=
(
sample
.
field2
??
this
.
getSampleValue
(
sample
,
'
field2
'
)).
trim
();
if
(
!
orderNo
)
{
await
this
.
guardedSkip
(
'
首行缺少采购单号,无法收窄查询后导出
'
);
}
await
this
.
applyQuickFields
({
...
sample
,
field2
:
orderNo
},
tracePurchaseDomainQuickFields
);
await
this
.
search
();
await
this
.
expectQueryWithoutError
();
if
((
await
this
.
locators
.
dataRows
.
count
())
===
0
)
{
await
this
.
guardedSkip
(
`采购单号「
${
orderNo
}
」查询后无数据,跳过导出校验`
);
}
await
this
.
exportReport
();
}
}
}
export
const
TRACEPURCHASE_PATH
=
tracePurchaseData
.
reportPath
;
export
const
TRACEPURCHASE_PATH
=
tracePurchaseData
.
reportPath
;
pages/spd/vendor-audit.page.ts
View file @
6c31a476
...
@@ -5,8 +5,13 @@ import { VendorSharedPage, type VendorMeta } from './vendor-shared.page';
...
@@ -5,8 +5,13 @@ import { VendorSharedPage, type VendorMeta } from './vendor-shared.page';
export
type
VendorAuditKind
=
'
auth
'
|
'
cert
'
;
export
type
VendorAuditKind
=
'
auth
'
|
'
cert
'
;
export
type
VendorAuthOpenOptions
=
{
keepQueryDefaults
?:
boolean
};
/** 授权/注册证变更初审·终审共用:筛选 + 列表 + 详情只读返回。 */
/** 授权/注册证变更初审·终审共用:筛选 + 列表 + 详情只读返回。 */
export
class
VendorAuditPage
extends
VendorSharedPage
{
export
class
VendorAuditPage
extends
VendorSharedPage
{
/** 进页时「状态」筛选项文案,用于重置恢复默认断言(随环境变化,不写死「通过」)。 */
private
statusFilterBaseline
:
string
|
null
=
null
;
constructor
(
constructor
(
page
:
Page
,
page
:
Page
,
meta
:
VendorMeta
,
meta
:
VendorMeta
,
...
@@ -15,6 +20,36 @@ export class VendorAuditPage extends VendorSharedPage {
...
@@ -15,6 +20,36 @@ export class VendorAuditPage extends VendorSharedPage {
super
(
page
,
meta
);
super
(
page
,
meta
);
}
}
protected
async
readStatusFilterText
():
Promise
<
string
|
null
>
{
await
this
.
ensureFiltersExpanded
();
const
select
=
this
.
formSelect
(
'
状态
'
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
null
;
const
item
=
select
.
locator
(
'
.ant-select-selection-item
'
).
first
();
return
((
await
item
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
''
);
}
async
expectStatusFilterDefault
():
Promise
<
void
>
{
const
current
=
await
this
.
readStatusFilterText
();
if
(
current
===
null
)
{
await
this
.
skipWhenDataInsufficient
(
'
状态筛选不可见
'
);
return
;
}
if
(
this
.
statusFilterBaseline
===
null
)
{
this
.
statusFilterBaseline
=
current
;
return
;
}
expect
(
current
,
'
重置后状态筛选应恢复进页默认
'
).
toBe
(
this
.
statusFilterBaseline
);
}
async
openWithQueryDefaults
(
options
?:
VendorAuthOpenOptions
):
Promise
<
void
>
{
await
super
.
open
();
if
(
options
?.
keepQueryDefaults
)
{
await
this
.
expectStatusFilterDefault
();
}
else
{
await
this
.
clearQueryConditionDefaults
();
}
}
async
expectEntered
():
Promise
<
void
>
{
async
expectEntered
():
Promise
<
void
>
{
await
this
.
expectOnPage
();
await
this
.
expectOnPage
();
await
expect
(
this
.
button
(
'
查询
'
).
first
(),
'
查询
'
).
toBeVisible
();
await
expect
(
this
.
button
(
'
查询
'
).
first
(),
'
查询
'
).
toBeVisible
();
...
@@ -79,6 +114,9 @@ export class VendorAuditPage extends VendorSharedPage {
...
@@ -79,6 +114,9 @@ export class VendorAuditPage extends VendorSharedPage {
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
await
expect
(
input
).
toHaveValue
(
''
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
await
expect
(
input
).
toHaveValue
(
''
);
}
}
await
this
.
expectListOrEmpty
();
await
this
.
expectListOrEmpty
();
if
(
this
.
statusFilterBaseline
!==
null
)
{
await
this
.
expectStatusFilterDefault
();
}
}
}
async
mockEmpty
():
Promise
<
void
>
{
async
mockEmpty
():
Promise
<
void
>
{
...
@@ -91,7 +129,17 @@ export class VendorAuditPage extends VendorSharedPage {
...
@@ -91,7 +129,17 @@ export class VendorAuditPage extends VendorSharedPage {
async
expectCoreColumns
():
Promise
<
void
>
{
async
expectCoreColumns
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
if
(
this
.
kind
===
'
auth
'
)
{
if
(
this
.
kind
===
'
auth
'
)
{
await
this
.
expectHeaders
([
'
进院供应商名称
'
,
'
单号
'
,
'
状态
'
,
'
制单时间
'
,
'
制单人
'
,
'
变更类型
'
,
'
原供应商
'
,
'
预转供应商
'
,
'
操作
'
]);
await
this
.
expectHeaders
([
'
销售商(自定义)名称
'
,
'
单号
'
,
'
状态
'
,
'
制单时间
'
,
'
制单人
'
,
'
变更类型
'
,
'
原供应商
'
,
'
预转供应商
'
,
'
操作
'
,
]);
}
else
{
}
else
{
await
this
.
expectHeaders
([
'
进院供应商名称
'
,
'
单号
'
,
'
状态
'
,
'
制单时间
'
,
'
制单人
'
,
'
变更类型
'
,
'
注册证号(新)
'
,
'
操作
'
]);
await
this
.
expectHeaders
([
'
进院供应商名称
'
,
'
单号
'
,
'
状态
'
,
'
制单时间
'
,
'
制单人
'
,
'
变更类型
'
,
'
注册证号(新)
'
,
'
操作
'
]);
}
}
...
@@ -111,6 +159,10 @@ export class VendorAuditPage extends VendorSharedPage {
...
@@ -111,6 +159,10 @@ export class VendorAuditPage extends VendorSharedPage {
await
expect
(
layer
.
first
()).
toContainText
(
order
.
slice
(
0
,
8
));
await
expect
(
layer
.
first
()).
toContainText
(
order
.
slice
(
0
,
8
));
}
}
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
if
(
!
this
.
page
.
url
().
includes
(
this
.
meta
.
pathPart
))
{
await
this
.
page
.
goBack
({
waitUntil
:
'
domcontentloaded
'
}).
catch
(()
=>
undefined
);
await
this
.
waitContentReady
();
}
await
this
.
expectOnPage
();
await
this
.
expectOnPage
();
}
}
...
@@ -119,6 +171,45 @@ export class VendorAuditPage extends VendorSharedPage {
...
@@ -119,6 +171,45 @@ export class VendorAuditPage extends VendorSharedPage {
await
this
.
filterSameDayDate
(
'
制单日期
'
);
await
this
.
filterSameDayDate
(
'
制单日期
'
);
}
}
/** 授权/注册证变更初审·终审:清除进页自带默认筛选项(状态、制单日期区间、下拉默认值等)。 */
async
clearQueryConditionDefaults
():
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
selectLabels
=
this
.
kind
===
'
auth
'
?
[
'
状态
'
,
'
变更类型
'
,
'
销售商(自定义)
'
,
'
销售商
'
]
:
[
'
状态
'
,
'
变更类型
'
,
'
销售商(自定义)
'
,
'
销售商
'
,
'
进院供应商
'
];
for
(
const
label
of
selectLabels
)
{
await
this
.
clearAntSelectIfSet
(
label
);
}
const
dateItem
=
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
制单日期
'
}).
first
();
const
dateClear
=
dateItem
.
locator
(
'
.ant-picker-clear
'
);
if
(
await
dateClear
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
dateClear
,
'
清空制单日期
'
);
await
this
.
waitSettle
(
200
);
}
const
textLabels
=
this
.
kind
===
'
auth
'
?
[
'
单号
'
,
'
预转供应商
'
,
'
原供应商
'
,
'
医院ID
'
,
'
医院名称
'
]
:
[
'
单号
'
,
'
预转供应商
'
,
'
原供应商
'
,
'
注册证号(新)
'
,
'
注册证号(新)
'
,
'
变更情况具体说明
'
,
'
医院ID
'
,
'
医院名称
'
];
for
(
const
label
of
textLabels
)
{
const
input
=
this
.
textInput
(
label
);
if
(
!
(
await
input
.
isVisible
().
catch
(()
=>
false
)))
continue
;
const
val
=
((
await
input
.
inputValue
().
catch
(()
=>
''
))
??
''
).
trim
();
if
(
val
)
await
this
.
fill
(
input
,
''
,
`清空
${
label
}
`
);
}
}
private
async
clearAntSelectIfSet
(
label
:
string
):
Promise
<
void
>
{
const
select
=
this
.
formSelect
(
label
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
;
await
select
.
hover
().
catch
(()
=>
undefined
);
const
clear
=
select
.
locator
(
'
.ant-select-clear
'
);
if
(
await
clear
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
clear
,
`清空
${
label
}
`
);
await
this
.
waitSettle
(
150
);
}
}
/** CSV *-010:切换至对端初审/终审,确认不串菜单。 */
/** CSV *-010:切换至对端初审/终审,确认不串菜单。 */
async
expectPeerHandoff
(
peer
:
VendorAuditPage
):
Promise
<
void
>
{
async
expectPeerHandoff
(
peer
:
VendorAuditPage
):
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
...
...
pages/spd/vendor-auth-final.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorAuthFinalData
}
from
'
../../data/vendor-auth-final.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
VendorAuditPage
}
from
'
./vendor-audit.page
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorAuditPage
,
type
VendorAuthOpenOptions
}
from
'
./vendor-audit.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
export
class
VendorAuthFinalPage
extends
VendorAuditPage
{
export
class
VendorAuthFinalPage
extends
VendorAuditPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
super
(
page
,
page
,
...
@@ -15,20 +25,309 @@ export class VendorAuthFinalPage extends VendorAuditPage {
...
@@ -15,20 +25,309 @@ export class VendorAuthFinalPage extends VendorAuditPage {
},
},
'
auth
'
,
'
auth
'
,
);
);
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
override
async
open
(
options
?:
VendorAuthOpenOptions
):
Promise
<
void
>
{
await
this
.
openWithQueryDefaults
(
options
);
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
filterExpandButton
():
Locator
{
return
this
.
button
(
'
展开
'
).
first
();
}
filterCollapseButton
():
Locator
{
return
this
.
button
(
'
收起
'
).
first
();
}
private
parseDateOnly
(
raw
:
string
):
string
{
const
m
=
raw
.
match
(
/
(\d{4})[
-
/](\d{1,2})[
-
/](\d{1,2})
/
);
if
(
!
m
)
return
''
;
const
[,
y
,
mo
,
d
]
=
m
;
return
`
${
y
}
-
${
mo
!
.
padStart
(
2
,
'
0
'
)}
-
${
d
!
.
padStart
(
2
,
'
0
'
)}
`
;
}
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
if
(
await
this
.
formSelect
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
return
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
first
().
isVisible
().
catch
(()
=>
false
);
}
/** CSV VENDOR-AF2-003:筛选区展开与收起。 */
async
expectFilterExpandCollapseWorks
():
Promise
<
void
>
{
const
expand
=
this
.
filterExpandButton
();
const
collapse
=
this
.
filterCollapseButton
();
const
expandVisible
=
await
expand
.
isVisible
().
catch
(()
=>
false
);
const
collapseVisible
=
await
collapse
.
isVisible
().
catch
(()
=>
false
);
if
(
!
expandVisible
&&
!
collapseVisible
)
{
await
this
.
skipWhenDataInsufficient
(
'
页面无筛选区展开/收起按钮
'
);
}
const
expectExpandedVisible
=
async
():
Promise
<
void
>
=>
{
for
(
const
label
of
vendorAuthFinalData
.
collapsedHiddenFilterLabels
)
{
if
(
await
this
.
isFilterFieldVisible
(
label
))
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`展开后应可见「
${
label
}
」`
).
toBeTruthy
();
return
;
}
}
await
expect
(
this
.
textInput
(
'
单号
'
),
'
展开后单号筛选应可见
'
).
toBeVisible
();
};
const
expectCollapsedHidden
=
async
():
Promise
<
void
>
=>
{
let
checked
=
false
;
for
(
const
label
of
vendorAuthFinalData
.
collapsedHiddenFilterLabels
)
{
if
(
!
(
await
this
.
isFilterFieldVisible
(
label
)))
{
checked
=
true
;
break
;
}
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`收起后「
${
label
}
」应不可见`
).
toBeFalsy
();
checked
=
true
;
}
if
(
!
checked
)
{
await
this
.
skipWhenDataInsufficient
(
'
无收起区字段可校验
'
);
}
};
if
(
expandVisible
)
{
await
this
.
click
(
expand
,
'
展开筛选区
'
);
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
expectExpandedVisible
();
}
else
{
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开
'
);
await
expectExpandedVisible
();
}
const
marker
=
`AF2-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
单号
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
单号
'
),
'
收起后再展开应保留单号
'
).
toHaveValue
(
marker
);
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
candidates
.
push
(
loc
);
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorAuthFinalData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|单号|全选
)
$/
.
test
(
name
))
continue
;
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
continue
;
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorAuthFinalData
.
columnSettingsToggleCandidates
[
0
];
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
return
;
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
if
((
await
checkbox
.
isChecked
())
!==
visible
)
await
row
.
click
();
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
else
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
await
expect
(
checkbox
).
toBeChecked
();
else
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
/** CSV VENDOR-AF2-004:列设置 toggle 恢复。 */
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
),
});
}
/** CSV VENDOR-AF2-005:详情只读进入并返回。 */
async
openDetailAndBack
():
Promise
<
void
>
{
await
this
.
detailAndBack
();
}
async
fillSupplierFilter
(
value
:
string
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
slice
=
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
));
const
needle
=
slice
.
slice
(
0
,
Math
.
min
(
slice
.
length
,
8
));
for
(
const
label
of
vendorAuthFinalData
.
supplierFilterLabels
)
{
const
select
=
this
.
formSelect
(
label
);
if
(
await
select
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
needle
})
.
first
();
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
return
;
}
}
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
slice
,
`填写
${
label
}
`
);
return
;
}
}
expect
(
false
,
`未找到销售商筛选(
${
vendorAuthFinalData
.
supplierFilterLabels
.
join
(
'
/
'
)}
)`
).
toBeTruthy
();
}
private
async
fillSelectMatchingRow
(
label
:
string
,
row
:
Locator
,
column
:
string
):
Promise
<
void
>
{
const
raw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
if
(
!
raw
)
return
;
const
select
=
this
.
formSelect
(
label
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
;
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
raw
.
slice
(
0
,
4
)
})
.
first
();
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
}
}
private
async
fillTextFilterIfVisible
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
if
(
!
value
)
return
;
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
)),
`填写
${
label
}
`
);
}
}
/** CSV VENDOR-AF2-006:首行七项组合查询。 */
async
queryFirstRowSevenFieldCombo
():
Promise
<
{
orderNo
:
string
}
>
{
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法首行组合查询
'
);
}
const
row
=
await
this
.
selectFirstRow
();
const
orderNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
单号
'
));
if
(
!
orderNo
)
await
this
.
skipWhenDataInsufficient
(
'
首行单号为空
'
);
const
supplier
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
vendorAuthFinalData
.
supplierColumnName
),
);
const
makeRaw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
制单时间
'
));
const
makeDay
=
this
.
parseDateOnly
(
makeRaw
);
if
(
makeDay
)
{
await
this
.
fillFilterDateRange
(
'
制单日期
'
,
makeDay
,
makeDay
);
}
await
this
.
fillFilter
(
'
单号
'
,
orderNo
.
slice
(
0
,
Math
.
min
(
orderNo
.
length
,
20
)));
if
(
supplier
)
{
await
this
.
fillSupplierFilter
(
supplier
);
}
await
this
.
fillTextFilterIfVisible
(
'
预转供应商
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
预转供应商
'
)));
await
this
.
fillTextFilterIfVisible
(
'
原供应商
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
原供应商
'
)));
await
this
.
fillSelectMatchingRow
(
'
状态
'
,
row
,
'
状态
'
);
await
this
.
fillSelectMatchingRow
(
'
变更类型
'
,
row
,
'
变更类型
'
);
await
this
.
ensureFiltersExpanded
();
await
this
.
query
();
return
{
orderNo
};
}
}
/** 组合查询:
一次性
填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
/** 组合查询:填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
async
filterCombination
():
Promise
<
void
>
{
async
filterCombination
():
Promise
<
void
>
{
const
scope
=
await
this
.
queryScope
();
const
scope
=
await
this
.
queryScope
();
await
this
.
applyFilterCombination
(
await
this
.
applyFilterCombination
(
[
[
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
{
label
:
'
单号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
单号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
销售商
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
vendorAuthFinalData
.
supplierFilterLabels
[
0
]
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
预转供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
预转供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
状态
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
状态
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
变更类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
变更类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
原供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
原供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
],
],
{
scope
},
{
scope
},
);
);
...
...
pages/spd/vendor-auth-first.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorAuthFirstData
}
from
'
../../data/vendor-auth-first.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
VendorAuditPage
}
from
'
./vendor-audit.page
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorAuditPage
,
type
VendorAuthOpenOptions
}
from
'
./vendor-audit.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
isColumnSettingsTriggerUsable
,
}
from
'
./helpers/column-settings.helper
'
;
/** 授权变更初审。 */
export
class
VendorAuthFirstPage
extends
VendorAuditPage
{
export
class
VendorAuthFirstPage
extends
VendorAuditPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
super
(
page
,
page
,
...
@@ -15,24 +27,314 @@ export class VendorAuthFirstPage extends VendorAuditPage {
...
@@ -15,24 +27,314 @@ export class VendorAuthFirstPage extends VendorAuditPage {
},
},
'
auth
'
,
'
auth
'
,
);
);
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
filterExpandButton
():
Locator
{
return
this
.
button
(
'
展开
'
).
first
();
}
filterCollapseButton
():
Locator
{
return
this
.
button
(
'
收起
'
).
first
();
}
override
async
open
(
options
?:
VendorAuthOpenOptions
):
Promise
<
void
>
{
await
this
.
openWithQueryDefaults
(
options
);
}
override
async
resetFilters
():
Promise
<
void
>
{
await
super
.
resetFilters
();
await
this
.
expectStatusFilterDefault
();
}
override
async
filterBySupplierOrCert
():
Promise
<
void
>
{
await
this
.
query
();
const
supplier
=
await
this
.
sampleCell
(
vendorAuthFirstData
.
supplierColumnName
);
const
keyword
=
supplier
.
slice
(
0
,
Math
.
min
(
supplier
.
length
,
10
));
if
(
!
keyword
)
{
await
this
.
filterByOrderNo
();
return
;
}
await
this
.
fillSupplierFilter
(
supplier
);
await
this
.
query
();
await
this
.
expectRowsContain
(
vendorAuthFirstData
.
supplierColumnName
,
keyword
.
slice
(
0
,
6
));
}
private
parseDateOnly
(
raw
:
string
):
string
{
const
m
=
raw
.
match
(
/
(\d{4})[
-
/](\d{1,2})[
-
/](\d{1,2})
/
);
if
(
!
m
)
return
''
;
const
[,
y
,
mo
,
d
]
=
m
;
return
`
${
y
}
-
${
mo
!
.
padStart
(
2
,
'
0
'
)}
-
${
d
!
.
padStart
(
2
,
'
0
'
)}
`
;
}
}
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
async
filterCombination
():
Promise
<
void
>
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
const
scope
=
await
this
.
queryScope
();
return
true
;
await
this
.
applyFilterCombination
(
}
[
if
(
await
this
.
formSelect
(
label
).
isVisible
().
catch
(()
=>
false
))
{
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
return
true
;
{
label
:
'
单号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
}
{
label
:
'
销售商
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
return
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
first
().
isVisible
().
catch
(()
=>
false
);
{
label
:
'
预转供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
}
{
label
:
'
状态
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
变更类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
/** CSV VENDOR-AF1-004:筛选区展开与收起。 */
{
label
:
'
原供应商
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
async
expectFilterExpandCollapseWorks
():
Promise
<
void
>
{
{
label
:
'
医院ID
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
const
expand
=
this
.
filterExpandButton
();
{
label
:
'
医院名称
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
const
collapse
=
this
.
filterCollapseButton
();
],
const
expandVisible
=
await
expand
.
isVisible
().
catch
(()
=>
false
);
{
scope
},
const
collapseVisible
=
await
collapse
.
isVisible
().
catch
(()
=>
false
);
if
(
!
expandVisible
&&
!
collapseVisible
)
{
await
this
.
skipWhenDataInsufficient
(
'
页面无筛选区展开/收起按钮
'
);
}
const
expectExpandedVisible
=
async
():
Promise
<
void
>
=>
{
for
(
const
label
of
vendorAuthFirstData
.
collapsedHiddenFilterLabels
)
{
if
(
await
this
.
isFilterFieldVisible
(
label
))
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`展开后应可见「
${
label
}
」`
).
toBeTruthy
();
return
;
}
}
await
expect
(
this
.
textInput
(
'
单号
'
),
'
展开后单号筛选应可见
'
).
toBeVisible
();
};
const
expectCollapsedHidden
=
async
():
Promise
<
void
>
=>
{
let
checked
=
false
;
for
(
const
label
of
vendorAuthFirstData
.
collapsedHiddenFilterLabels
)
{
if
(
!
(
await
this
.
isFilterFieldVisible
(
label
)))
{
checked
=
true
;
break
;
}
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`收起后「
${
label
}
」应不可见`
).
toBeFalsy
();
checked
=
true
;
}
if
(
!
checked
)
{
await
this
.
skipWhenDataInsufficient
(
'
无收起区字段可校验
'
);
}
};
if
(
expandVisible
)
{
await
this
.
click
(
expand
,
'
展开筛选区
'
);
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
expectExpandedVisible
();
}
else
{
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开
'
);
await
expectExpandedVisible
();
}
const
marker
=
`AF1-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
单号
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
单号
'
),
'
收起后再展开应保留单号
'
).
toHaveValue
(
marker
);
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
candidates
.
push
(
loc
);
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorAuthFirstData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|单号|全选
)
$/
.
test
(
name
))
continue
;
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
continue
;
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorAuthFirstData
.
columnSettingsToggleCandidates
[
0
];
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
return
;
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
if
((
await
checkbox
.
isChecked
())
!==
visible
)
await
row
.
click
();
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
else
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
await
expect
(
checkbox
).
toBeChecked
();
else
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
/** CSV VENDOR-AF1-003:列设置 toggle 恢复。 */
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
),
});
}
/** CSV VENDOR-AF1-005:详情只读进入并返回。 */
async
openDetailAndBack
():
Promise
<
void
>
{
await
this
.
detailAndBack
();
}
/** 授权变更初审筛选项为「销售商(自定义)」,与列表列「销售商(自定义)名称」对应。 */
override
async
fillSupplierFilter
(
value
:
string
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
label
=
vendorAuthFirstData
.
supplierFilterLabel
;
const
slice
=
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
));
const
select
=
this
.
formSelect
(
label
);
if
(
await
select
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
needle
=
slice
.
slice
(
0
,
Math
.
min
(
slice
.
length
,
8
));
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
needle
})
.
first
();
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
return
;
}
}
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
slice
,
`填写
${
label
}
`
);
return
;
}
expect
(
false
,
`未找到「
${
label
}
」筛选`
).
toBeTruthy
();
}
private
async
fillSelectMatchingRow
(
label
:
string
,
row
:
Locator
,
column
:
string
):
Promise
<
void
>
{
const
raw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
if
(
!
raw
)
return
;
const
select
=
this
.
formSelect
(
label
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
;
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
raw
.
slice
(
0
,
4
)
})
.
first
();
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
}
}
private
async
fillTextFilterIfVisible
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
if
(
!
value
)
return
;
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
)),
`填写
${
label
}
`
);
}
}
/** CSV VENDOR-AF1-006:首行九项组合查询。 */
async
queryFirstRowNineFieldCombo
():
Promise
<
{
orderNo
:
string
;
supplier
:
string
}
>
{
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法首行组合查询
'
);
}
const
row
=
await
this
.
selectFirstRow
();
const
orderNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
单号
'
));
if
(
!
orderNo
)
await
this
.
skipWhenDataInsufficient
(
'
首行单号为空
'
);
const
supplier
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
vendorAuthFirstData
.
supplierColumnName
),
);
);
const
makeRaw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
制单时间
'
));
const
makeDay
=
this
.
parseDateOnly
(
makeRaw
);
if
(
makeDay
)
{
await
this
.
fillFilterDateRange
(
'
制单日期
'
,
makeDay
,
makeDay
);
}
await
this
.
fillFilter
(
'
单号
'
,
orderNo
.
slice
(
0
,
Math
.
min
(
orderNo
.
length
,
20
)));
if
(
supplier
)
{
await
this
.
fillSupplierFilter
(
supplier
);
}
await
this
.
fillTextFilterIfVisible
(
'
预转供应商
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
预转供应商
'
)));
await
this
.
fillTextFilterIfVisible
(
'
原供应商
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
原供应商
'
)));
await
this
.
fillSelectMatchingRow
(
'
状态
'
,
row
,
'
状态
'
);
await
this
.
fillSelectMatchingRow
(
'
变更类型
'
,
row
,
'
变更类型
'
);
await
this
.
ensureFiltersExpanded
();
await
this
.
fillTextFilterIfVisible
(
'
医院ID
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
医院ID
'
)));
await
this
.
fillTextFilterIfVisible
(
'
医院名称
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
医院名称
'
)));
await
this
.
query
();
return
{
orderNo
,
supplier
};
}
}
}
}
pages/spd/vendor-cert-diff.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
FrameLocator
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
type
FilterScope
}
from
'
../../core/base.page
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorCertDiffData
}
from
'
../../data/vendor-cert-diff.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorAuditPage
,
type
VendorAuthOpenOptions
}
from
'
./vendor-audit.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
import
{
skipIfInsufficientForPage2
}
from
'
./helpers/pagination-page2.helper
'
;
/** 注册证差异对比。 */
export
class
VendorCertDiffPage
extends
VendorAuditPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
/** 注册证差异对比:望海 vs FLI+;页面主体在 scjs iframe。 */
export
class
VendorCertDiffPage
extends
VendorSharedPage
{
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
{
super
(
path
:
vendorData
.
certDiffPath
,
page
,
pathPart
:
'
certCompare
'
,
{
menu
:
vendorData
.
menuCertDiff
,
path
:
vendorData
.
certDiffPath
,
forbidden
:
[
'
authAudit
'
,
'
evaluationPlan
'
,
'
holiday
'
],
pathPart
:
'
certCompare
'
,
menu
:
vendorData
.
menuCertDiff
,
forbidden
:
[
'
regCertAudit
'
,
'
authAudit
'
,
'
evaluationPlan
'
,
'
holiday
'
,
'
factory
'
,
'
qcManage
'
],
},
'
cert
'
,
);
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
});
}
}
contentFrame
()
{
contentFrame
()
:
FrameLocator
{
return
this
.
page
.
frameLocator
(
'
iframe
'
).
first
();
return
this
.
page
.
frameLocator
(
'
iframe
'
).
first
();
}
}
async
waitContentReady
():
Promise
<
void
>
{
private
filterScope
():
FrameLocator
|
Locator
{
if
(
this
.
page
.
url
().
includes
(
'
certCompare
'
))
{
return
this
.
contentFrame
();
}
return
this
.
page
.
locator
(
'
body
'
);
}
override
async
queryScope
():
Promise
<
FilterScope
>
{
return
this
.
filterScope
()
as
FilterScope
;
}
override
async
waitContentReady
():
Promise
<
void
>
{
await
this
.
page
.
locator
(
'
iframe
'
).
first
().
waitFor
({
state
:
'
attached
'
,
timeout
:
20
_000
}).
catch
(()
=>
undefined
);
await
this
.
page
.
locator
(
'
iframe
'
).
first
().
waitFor
({
state
:
'
attached
'
,
timeout
:
20
_000
}).
catch
(()
=>
undefined
);
const
frameHint
=
this
.
contentFrame
().
getByText
(
/物料名称|全部|一致|不一致|望海/
).
first
();
const
frameHint
=
this
.
contentFrame
().
getByText
(
/物料名称|
制单日期|
全部|一致|不一致|望海
|查
/
).
first
();
const
shellHint
=
this
.
page
.
getByText
(
/注册证差异对比|物料名称/
).
first
();
const
shellHint
=
this
.
page
.
getByText
(
/注册证差异对比|物料名称
|制单日期
/
).
first
();
await
Promise
.
race
([
await
Promise
.
race
([
frameHint
.
waitFor
({
state
:
'
visible
'
,
timeout
:
25
_000
}),
frameHint
.
waitFor
({
state
:
'
visible
'
,
timeout
:
25
_000
}),
shellHint
.
waitFor
({
state
:
'
visible
'
,
timeout
:
25
_000
}),
shellHint
.
waitFor
({
state
:
'
visible
'
,
timeout
:
25
_000
}),
]).
catch
(()
=>
undefined
);
]).
catch
(()
=>
undefined
);
await
this
.
waitSpinGone
();
await
this
.
waitSpinGone
();
await
this
.
waitForTableReady
({
health
:
'
ui
'
});
}
}
async
expectOnPage
():
Promise
<
void
>
{
override
button
(
name
:
string
,
root
?:
Locator
):
Locator
{
expect
(
this
.
page
.
url
(),
`URL 应含
${
this
.
meta
.
pathPart
}
`
).
toContain
(
this
.
meta
.
pathPart
);
if
(
root
)
return
super
.
button
(
name
,
root
);
await
this
.
waitContentReady
();
const
scope
=
this
.
filterScope
();
}
const
pattern
=
new
RegExp
(
name
.
split
(
''
).
join
(
'
\\
s*
'
));
return
scope
.
getByRole
(
'
button
'
,
{
name
:
pattern
}).
first
();
async
expectEntered
():
Promise
<
void
>
{
}
await
this
.
expectOnPage
();
const
frameOk
=
await
this
.
contentFrame
()
override
textInput
(
label
:
string
,
root
?:
Locator
):
Locator
{
.
getByText
(
/物料名称|全部|一致|不一致/
)
if
(
root
)
return
super
.
textInput
(
label
,
root
);
.
first
()
const
scope
=
this
.
filterScope
();
.
isVisible
()
const
compact
=
label
.
replace
(
/
\s
/g
,
''
);
.
catch
(()
=>
false
);
const
labelPattern
=
new
RegExp
(
compact
.
split
(
''
).
join
(
'
\\
s*
'
));
const
shellOk
=
await
this
.
page
const
byRole
=
scope
.
getByRole
(
'
textbox
'
,
{
name
:
labelPattern
}).
first
();
.
getByText
(
/注册证差异对比|物料名称|望海/
)
const
byForm
=
scope
.
first
()
.
locator
(
'
.ant-form-item
'
)
.
isVisible
()
.
filter
({
hasText
:
labelPattern
})
.
catch
(()
=>
false
);
.
locator
(
'
input:not([type="hidden"])
'
)
expect
(
frameOk
||
shellOk
,
'
差异对比页主区应可见(外壳或 iframe)
'
).
toBeTruthy
();
.
first
();
}
const
byPlaceholder
=
scope
.
getByPlaceholder
(
labelPattern
).
first
();
return
byRole
.
or
(
byForm
).
or
(
byPlaceholder
);
private
async
root
()
{
}
if
((
await
this
.
page
.
locator
(
'
iframe
'
).
count
())
>
0
)
return
this
.
contentFrame
();
return
this
.
page
;
override
formSelect
(
label
:
string
,
root
?:
Locator
):
Locator
{
}
if
(
root
)
return
super
.
formSelect
(
label
,
root
);
const
scope
=
this
.
filterScope
();
async
queryDefault
():
Promise
<
void
>
{
return
scope
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
locator
(
'
.ant-select
'
).
first
();
const
root
=
await
this
.
root
();
const
queryBtn
=
root
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*询/
}).
first
();
if
(
await
queryBtn
.
isVisible
().
catch
(()
=>
false
))
{
await
queryBtn
.
click
();
await
this
.
waitSettle
(
800
);
}
await
this
.
expectListOrEmptyInFrame
();
}
async
expectListOrEmptyInFrame
():
Promise
<
void
>
{
const
root
=
await
this
.
root
();
const
rows
=
root
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
);
const
count
=
await
rows
.
count
().
catch
(()
=>
0
);
if
(
count
>
0
)
return
;
const
empty
=
await
root
.
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
);
expect
(
empty
||
count
===
0
,
'
对比列表应有数据或空态
'
).
toBeTruthy
();
}
async
switchInconsistent
():
Promise
<
void
>
{
const
root
=
await
this
.
root
();
await
root
.
getByText
(
'
不一致
'
,
{
exact
:
true
}).
first
().
click
();
await
this
.
waitSettle
(
600
);
await
this
.
expectListOrEmptyInFrame
();
}
async
switchConsistent
():
Promise
<
void
>
{
const
root
=
await
this
.
root
();
await
root
.
getByText
(
'
一致
'
,
{
exact
:
true
}).
first
().
click
();
await
this
.
waitSettle
(
600
);
await
this
.
expectListOrEmptyInFrame
();
}
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
async
filterCombination
():
Promise
<
void
>
{
const
scope
=
await
this
.
queryScope
();
await
this
.
applyFilterCombination
(
[
{
label
:
'
物料名称
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
规格型号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
状态
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
望海注册证号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
是否医疗器械
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
生产厂家
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
FLI+注册证号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
27位医保编码
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
],
{
scope
},
);
}
}
async
filterByMaterialSample
():
Promise
<
void
>
{
override
async
listRows
():
Promise
<
Locator
>
{
await
this
.
queryDefault
();
if
(
this
.
page
.
url
().
includes
(
'
certCompare
'
))
{
const
root
=
await
this
.
root
();
return
this
.
contentFrame
().
locator
(
'
.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)
'
);
const
rows
=
root
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
);
if
((
await
rows
.
count
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
对比列表无数据
'
);
return
;
}
}
const
name
=
((
await
rows
.
first
().
locator
(
'
td
'
).
nth
(
2
).
innerText
().
catch
(()
=>
''
))
??
''
).
trim
();
return
super
.
listRows
();
if
(
!
name
||
name
===
'
--
'
)
{
}
await
this
.
skipWhenDataInsufficient
(
'
首行物料名称为空
'
);
return
;
override
pageSizeChanger
():
Locator
{
return
this
.
filterScope
().
locator
(
'
.ant-pagination-options-size-changer
'
).
first
();
}
override
async
getTotalCount
():
Promise
<
number
>
{
const
text
=
(
await
this
.
filterScope
().
locator
(
'
.ant-pagination-total-text
'
).
first
().
textContent
().
catch
(()
=>
''
))
??
''
;
const
match
=
text
.
match
(
/共
\s
*
(\d
+
)\s
*条/
);
return
match
?
Number
(
match
[
1
])
:
0
;
}
override
async
goToPage2
():
Promise
<
void
>
{
const
scope
=
this
.
filterScope
();
await
skipIfInsufficientForPage2
({
ensureHealthyBeforeSkip
:
(
reason
)
=>
this
.
ensureHealthyBeforeSkip
(
reason
),
waitForTableReady
:
()
=>
this
.
waitForTableReady
(),
getDataRowCount
:
()
=>
this
.
getDataRowCount
(),
hasPaginationItem
:
async
(
pageNum
)
=>
scope
.
locator
(
`.ant-pagination-item-
${
pageNum
}
`
).
first
().
isVisible
().
catch
(()
=>
false
),
});
const
item
=
scope
.
locator
(
'
.ant-pagination-item-2
'
).
first
();
if
(
await
item
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
item
,
'
翻到第 2 页
'
);
}
else
{
await
this
.
click
(
scope
.
locator
(
'
.ant-pagination-next
'
).
first
(),
'
下一页
'
);
}
}
const
keyword
=
name
.
slice
(
0
,
Math
.
min
(
name
.
length
,
8
));
await
this
.
waitForTableReady
();
const
input
=
root
await
expect
(
scope
.
locator
(
'
.ant-pagination-item-active
'
).
first
(),
'
当前页应为第 2 页
'
).
toHaveText
(
'
2
'
);
.
getByPlaceholder
(
/物料名称/
)
expect
(
await
this
.
getDataRowCount
(),
'
第 2 页应有数据
'
).
toBeGreaterThan
(
0
);
.
or
(
root
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
物料名称
'
}).
locator
(
'
input
'
))
}
override
async
snapshotList
():
Promise
<
{
rows
:
number
;
total
:
number
;
emptyVisible
:
boolean
}
>
{
await
this
.
waitForTableReady
();
const
rows
=
await
this
.
getDataRowCount
();
const
total
=
await
this
.
getTotalCount
();
const
scope
=
this
.
filterScope
();
const
emptyVisible
=
(
await
scope
.
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
this
.
page
.
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
));
return
{
rows
,
total
,
emptyVisible
};
}
tableWrapper
():
Locator
{
return
this
.
filterScope
().
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
.
first
();
await
input
.
fill
(
keyword
);
}
const
queryBtn
=
root
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*询/
}).
first
();
if
(
await
queryBtn
.
isVisible
().
catch
(()
=>
false
))
await
queryBtn
.
click
();
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
await
this
.
waitSettle
(
800
);
return
this
.
columnSettingsPanel
()
await
expect
(
rows
.
first
()).
toContainText
(
keyword
.
slice
(
0
,
4
));
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
}
.
filter
({
hasText
:
label
})
async
mockEmpty
():
Promise
<
void
>
{
const
root
=
await
this
.
root
();
const
input
=
root
.
getByPlaceholder
(
/物料名称/
)
.
or
(
root
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
物料名称
'
}).
locator
(
'
input
'
))
.
first
();
.
first
();
if
(
!
(
await
input
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
无物料名称筛选框
'
);
return
;
}
await
input
.
fill
(
vendorData
.
mockMfrsName
);
const
queryBtn
=
root
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*询/
}).
first
();
if
(
await
queryBtn
.
isVisible
().
catch
(()
=>
false
))
await
queryBtn
.
click
();
await
this
.
waitSettle
(
800
);
const
rows
=
root
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
);
const
count
=
await
rows
.
count
().
catch
(()
=>
0
);
const
empty
=
await
root
.
getByText
(
'
暂无数据
'
).
first
().
isVisible
().
catch
(()
=>
false
);
expect
(
count
===
0
||
empty
,
'
mock 应空态
'
).
toBeTruthy
();
}
async
expectCoreColumns
():
Promise
<
void
>
{
await
this
.
queryDefault
();
const
root
=
await
this
.
root
();
const
headerText
=
((
await
root
.
locator
(
'
.ant-table-thead
'
).
first
().
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
,
);
expect
(
headerText
.
includes
(
'
物料编号
'
)
||
headerText
.
includes
(
'
物料名称
'
),
'
核心列
'
).
toBeTruthy
();
}
}
async
expectExportVisible
():
Promise
<
void
>
{
columnHeaderCell
(
columnName
:
string
):
Locator
{
const
root
=
await
this
.
root
();
return
this
.
filterScope
().
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
await
expect
(
root
.
getByRole
(
'
button
'
,
{
name
:
/导
\s
*出/
}).
first
(),
'
导出
'
).
toBeVisible
({
timeout
:
10
_000
});
}
filterExpandButton
():
Locator
{
return
this
.
button
(
'
展开
'
).
first
();
}
filterCollapseButton
():
Locator
{
return
this
.
button
(
'
收起
'
).
first
();
}
override
async
open
(
options
?:
VendorAuthOpenOptions
):
Promise
<
void
>
{
await
this
.
openWithQueryDefaults
(
options
);
}
override
async
ensureFiltersExpanded
():
Promise
<
void
>
{
const
scope
=
this
.
filterScope
();
const
expand
=
scope
.
getByRole
(
'
button
'
,
{
name
:
/^展
\s
*开$/
}).
first
();
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
expand
,
'
展开筛选
'
);
await
this
.
waitSettle
(
200
);
}
}
override
async
fillFilter
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
input
=
this
.
textInput
(
label
);
await
expect
(
input
,
`筛选「
${
label
}
」`
).
toBeVisible
();
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
}
override
async
query
():
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
'
查询
'
);
await
this
.
clickButton
(
'
查询
'
);
await
this
.
waitForTableReady
();
}
override
async
reset
():
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
'
重置
'
);
await
this
.
clickButton
(
'
重置
'
);
await
this
.
waitForTableReady
();
}
override
async
resetFilters
():
Promise
<
void
>
{
await
super
.
resetFilters
();
await
this
.
expectStatusFilterDefault
();
}
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
if
(
await
this
.
formSelect
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
return
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
first
().
isVisible
().
catch
(()
=>
false
);
}
/** CSV VENDOR-CDF-004:筛选区展开与收起。 */
async
expectFilterExpandCollapseWorks
():
Promise
<
void
>
{
const
expand
=
this
.
filterExpandButton
();
const
collapse
=
this
.
filterCollapseButton
();
const
expandVisible
=
await
expand
.
isVisible
().
catch
(()
=>
false
);
const
collapseVisible
=
await
collapse
.
isVisible
().
catch
(()
=>
false
);
if
(
!
expandVisible
&&
!
collapseVisible
)
{
await
this
.
skipWhenDataInsufficient
(
'
页面无筛选区展开/收起按钮
'
);
}
const
expectExpandedVisible
=
async
():
Promise
<
void
>
=>
{
for
(
const
label
of
vendorCertDiffData
.
collapsedHiddenFilterLabels
)
{
if
(
await
this
.
isFilterFieldVisible
(
label
))
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`展开后应可见「
${
label
}
」`
).
toBeTruthy
();
return
;
}
}
await
expect
(
this
.
textInput
(
'
单号
'
),
'
展开后单号筛选应可见
'
).
toBeVisible
();
};
const
expectCollapsedHidden
=
async
():
Promise
<
void
>
=>
{
let
checked
=
false
;
for
(
const
label
of
vendorCertDiffData
.
collapsedHiddenFilterLabels
)
{
if
(
!
(
await
this
.
isFilterFieldVisible
(
label
)))
{
checked
=
true
;
break
;
}
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`收起后「
${
label
}
」应不可见`
).
toBeFalsy
();
checked
=
true
;
}
if
(
!
checked
)
{
await
this
.
skipWhenDataInsufficient
(
'
无收起区字段可校验
'
);
}
};
if
(
expandVisible
)
{
await
this
.
click
(
expand
,
'
展开筛选区
'
);
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
expectExpandedVisible
();
}
else
{
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开
'
);
await
expectExpandedVisible
();
}
const
marker
=
`CDF-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
单号
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
单号
'
),
'
收起后再展开应保留单号
'
).
toHaveValue
(
marker
);
}
}
/** CSV VENDOR-CDF-009 */
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
async
searchSpacesOnly
():
Promise
<
void
>
{
const
settings
=
this
.
filterScope
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
root
=
await
this
.
root
();
const
candidates
:
Locator
[]
=
[];
const
input
=
root
const
count
=
await
settings
.
count
();
.
getByPlaceholder
(
/物料名称/
)
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
.
or
(
root
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
物料名称
'
}).
locator
(
'
input
'
))
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
candidates
.
push
(
loc
);
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorCertDiffData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|单号|全选
)
$/
.
test
(
name
))
continue
;
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
continue
;
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorCertDiffData
.
columnSettingsToggleCandidates
[
0
];
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
return
;
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
if
((
await
checkbox
.
isChecked
())
!==
visible
)
await
row
.
click
();
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
else
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
await
expect
(
checkbox
).
toBeChecked
();
else
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
/** CSV VENDOR-CDF-003:列设置 toggle 恢复。 */
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
),
});
}
/** CSV VENDOR-CDF-005:详情只读进入并返回。 */
async
openDetailAndBack
():
Promise
<
void
>
{
await
this
.
detailAndBack
();
}
private
async
fillSelectMatchingRow
(
label
:
string
,
row
:
Locator
,
column
:
string
):
Promise
<
void
>
{
const
raw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
if
(
!
raw
)
return
;
const
select
=
this
.
formSelect
(
label
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
;
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
raw
.
slice
(
0
,
4
)
})
.
first
();
.
first
();
if
(
!
(
await
input
.
isVisible
().
catch
(()
=>
false
)))
{
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
skipWhenDataInsufficient
(
'
无物料名称筛选框
'
);
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
return
;
}
}
}
await
input
.
fill
(
'
'
);
const
queryBtn
=
root
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*询/
}).
first
();
private
async
fillTextFilterIfVisible
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
if
(
await
queryBtn
.
isVisible
().
catch
(()
=>
false
))
{
if
(
!
value
)
return
;
await
this
.
assertBeforeBusinessAction
(
'
查询
'
);
const
input
=
this
.
textInput
(
label
);
await
queryBtn
.
click
();
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
}
await
this
.
fill
(
input
,
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
)),
`填写
${
label
}
`
);
await
this
.
waitSettle
(
800
);
}
await
this
.
assertPageHealthy
(
'
空格物料名称搜索
'
);
}
await
this
.
expectListOrEmptyInFrame
();
}
private
async
cellFromRow
(
row
:
Locator
,
columns
:
readonly
string
[]):
Promise
<
string
>
{
for
(
const
col
of
columns
)
{
/** CSV VENDOR-CDF-010:一致行两侧字段抽样一致。 */
const
v
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
async
expectConsistentRowAligned
():
Promise
<
void
>
{
if
(
v
)
return
v
;
await
this
.
switchConsistent
();
}
const
root
=
await
this
.
root
();
return
''
;
const
rows
=
root
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
);
}
if
((
await
rows
.
count
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
一致 Tab 无数据
'
);
/** CSV VENDOR-CDF-008:首行八项组合查询。 */
return
;
async
queryFirstRowEightFieldCombo
():
Promise
<
{
materialName
:
string
}
>
{
}
await
this
.
query
();
const
text
=
((
await
rows
.
first
().
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
if
((
await
this
.
getDataRowCount
())
===
0
)
{
expect
(
text
.
length
,
'
一致行应有内容
'
).
toBeGreaterThan
(
0
);
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法首行组合查询
'
);
await
this
.
assertUiReady
(
'
一致行抽样
'
);
}
}
const
row
=
await
this
.
selectFirstRow
();
/** CSV VENDOR-CDF-011:不一致行至少一侧差异。 */
const
materialName
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
物料名称
'
));
async
expectInconsistentRowDiffers
():
Promise
<
void
>
{
if
(
!
materialName
)
await
this
.
skipWhenDataInsufficient
(
'
首行物料名称为空
'
);
await
this
.
switchInconsistent
();
const
root
=
await
this
.
root
();
await
this
.
fillTextFilterIfVisible
(
'
物料名称
'
,
materialName
);
const
rows
=
root
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
);
await
this
.
fillTextFilterIfVisible
(
'
规格型号
'
,
await
this
.
cellFromRow
(
row
,
[
'
规格型号
'
]));
if
((
await
rows
.
count
())
===
0
)
{
await
this
.
fillSelectMatchingRow
(
'
状态
'
,
row
,
'
状态
'
);
await
this
.
skipWhenDataInsufficient
(
'
不一致 Tab 无数据
'
);
await
this
.
fillTextFilterIfVisible
(
return
;
'
望海注册证号
'
,
}
await
this
.
cellFromRow
(
row
,
vendorCertDiffData
.
whCertColumns
),
const
text
=
((
await
rows
.
first
().
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
);
expect
(
text
.
length
,
'
不一致行应有内容
'
).
toBeGreaterThan
(
0
);
for
(
const
col
of
vendorCertDiffData
.
medicalDeviceColumns
)
{
await
this
.
assertUiReady
(
'
不一致行抽样
'
);
const
device
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
if
(
device
)
{
await
this
.
fillSelectMatchingRow
(
'
是否医疗器械
'
,
row
,
col
);
break
;
}
}
await
this
.
fillTextFilterIfVisible
(
'
生产厂家
'
,
await
this
.
cellFromRow
(
row
,
[
'
生产厂家
'
]));
await
this
.
fillTextFilterIfVisible
(
'
FLI+注册证号
'
,
await
this
.
cellFromRow
(
row
,
vendorCertDiffData
.
fliCertColumns
),
);
await
this
.
fillTextFilterIfVisible
(
'
27位医保编码
'
,
await
this
.
cellFromRow
(
row
,
[
'
27位医保编码
'
,
'
27位医保代码
'
]));
await
this
.
query
();
return
{
materialName
};
}
}
}
}
pages/spd/vendor-cert-final.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorCertFinalData
}
from
'
../../data/vendor-cert-final.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
VendorAuditPage
}
from
'
./vendor-audit.page
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorAuditPage
,
type
VendorAuthOpenOptions
}
from
'
./vendor-audit.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
/** 注册证变更终审。 */
export
class
VendorCertFinalPage
extends
VendorAuditPage
{
export
class
VendorCertFinalPage
extends
VendorAuditPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
super
(
page
,
page
,
...
@@ -11,24 +22,294 @@ export class VendorCertFinalPage extends VendorAuditPage {
...
@@ -11,24 +22,294 @@ export class VendorCertFinalPage extends VendorAuditPage {
path
:
vendorData
.
certFinalPath
,
path
:
vendorData
.
certFinalPath
,
pathPart
:
'
mfrsName/regCertAuditFinal
'
,
pathPart
:
'
mfrsName/regCertAuditFinal
'
,
menu
:
vendorData
.
menuCertFinal
,
menu
:
vendorData
.
menuCertFinal
,
forbidden
:
[
'
authAudit
'
,
'
factory
'
,
'
qcManage
'
],
forbidden
:
[
'
regCertAudit
'
,
'
authAudit
'
,
'
factory
'
,
'
qcManage
'
],
},
},
'
cert
'
,
'
cert
'
,
);
);
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
filterExpandButton
():
Locator
{
return
this
.
button
(
'
展开
'
).
first
();
}
filterCollapseButton
():
Locator
{
return
this
.
button
(
'
收起
'
).
first
();
}
override
async
open
(
options
?:
VendorAuthOpenOptions
):
Promise
<
void
>
{
await
this
.
openWithQueryDefaults
(
options
);
}
override
async
resetFilters
():
Promise
<
void
>
{
await
super
.
resetFilters
();
await
this
.
expectStatusFilterDefault
();
}
private
parseDateOnly
(
raw
:
string
):
string
{
const
m
=
raw
.
match
(
/
(\d{4})[
-
/](\d{1,2})[
-
/](\d{1,2})
/
);
if
(
!
m
)
return
''
;
const
[,
y
,
mo
,
d
]
=
m
;
return
`
${
y
}
-
${
mo
!
.
padStart
(
2
,
'
0
'
)}
-
${
d
!
.
padStart
(
2
,
'
0
'
)}
`
;
}
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
if
(
await
this
.
formSelect
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
return
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
first
().
isVisible
().
catch
(()
=>
false
);
}
}
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
/** CSV VENDOR-CF2-004:筛选区展开与收起。 */
async
filterCombination
():
Promise
<
void
>
{
async
expectFilterExpandCollapseWorks
():
Promise
<
void
>
{
const
scope
=
await
this
.
queryScope
();
const
expand
=
this
.
filterExpandButton
();
await
this
.
applyFilterCombination
(
const
collapse
=
this
.
filterCollapseButton
();
[
const
expandVisible
=
await
expand
.
isVisible
().
catch
(()
=>
false
);
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
const
collapseVisible
=
await
collapse
.
isVisible
().
catch
(()
=>
false
);
{
label
:
'
单号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
if
(
!
expandVisible
&&
!
collapseVisible
)
{
{
label
:
'
变更类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
await
this
.
skipWhenDataInsufficient
(
'
页面无筛选区展开/收起按钮
'
);
{
label
:
'
注册证号(新)
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
}
{
label
:
'
状态
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
],
const
expectExpandedVisible
=
async
():
Promise
<
void
>
=>
{
{
scope
},
for
(
const
label
of
vendorCertFinalData
.
collapsedHiddenFilterLabels
)
{
if
(
await
this
.
isFilterFieldVisible
(
label
))
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`展开后应可见「
${
label
}
」`
).
toBeTruthy
();
return
;
}
}
await
expect
(
this
.
textInput
(
'
单号
'
),
'
展开后单号筛选应可见
'
).
toBeVisible
();
};
const
expectCollapsedHidden
=
async
():
Promise
<
void
>
=>
{
let
checked
=
false
;
for
(
const
label
of
vendorCertFinalData
.
collapsedHiddenFilterLabels
)
{
if
(
!
(
await
this
.
isFilterFieldVisible
(
label
)))
{
checked
=
true
;
break
;
}
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`收起后「
${
label
}
」应不可见`
).
toBeFalsy
();
checked
=
true
;
}
if
(
!
checked
)
{
await
this
.
skipWhenDataInsufficient
(
'
无收起区字段可校验
'
);
}
};
if
(
expandVisible
)
{
await
this
.
click
(
expand
,
'
展开筛选区
'
);
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
expectExpandedVisible
();
}
else
{
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开
'
);
await
expectExpandedVisible
();
}
const
marker
=
`CF2-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
单号
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
单号
'
),
'
收起后再展开应保留单号
'
).
toHaveValue
(
marker
);
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
candidates
.
push
(
loc
);
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorCertFinalData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|单号|全选
)
$/
.
test
(
name
))
continue
;
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
continue
;
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorCertFinalData
.
columnSettingsToggleCandidates
[
0
];
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
return
;
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
if
((
await
checkbox
.
isChecked
())
!==
visible
)
await
row
.
click
();
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
else
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
await
expect
(
checkbox
).
toBeChecked
();
else
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
/** CSV VENDOR-CF2-003:列设置 toggle 恢复。 */
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
),
});
}
/** CSV VENDOR-CF2-005:详情只读进入并返回。 */
async
openDetailAndBack
():
Promise
<
void
>
{
await
this
.
detailAndBack
();
}
private
async
fillSelectMatchingRow
(
label
:
string
,
row
:
Locator
,
column
:
string
):
Promise
<
void
>
{
const
raw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
if
(
!
raw
)
return
;
const
select
=
this
.
formSelect
(
label
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
;
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
raw
.
slice
(
0
,
4
)
})
.
first
();
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
}
}
private
async
fillTextFilterIfVisible
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
if
(
!
value
)
return
;
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
)),
`填写
${
label
}
`
);
}
}
private
async
certNoFromRow
(
row
:
Locator
):
Promise
<
string
>
{
for
(
const
col
of
[
'
注册证号(新)
'
,
'
注册证号(新)
'
]
as
const
)
{
const
v
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
if
(
v
)
return
v
;
}
return
''
;
}
private
async
fillCertNoFilter
(
value
:
string
):
Promise
<
void
>
{
if
(
!
value
)
return
;
const
slice
=
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
));
for
(
const
label
of
[
'
注册证号(新)
'
,
'
注册证号(新)
'
]
as
const
)
{
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
slice
,
`填写
${
label
}
`
);
return
;
}
}
}
/** CSV VENDOR-CF2-008:首行六项组合查询。 */
async
queryFirstRowSixFieldCombo
():
Promise
<
{
orderNo
:
string
}
>
{
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法首行组合查询
'
);
}
const
row
=
await
this
.
selectFirstRow
();
const
orderNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
单号
'
));
if
(
!
orderNo
)
await
this
.
skipWhenDataInsufficient
(
'
首行单号为空
'
);
const
makeRaw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
制单时间
'
));
const
makeDay
=
this
.
parseDateOnly
(
makeRaw
);
if
(
makeDay
)
{
await
this
.
fillFilterDateRange
(
'
制单日期
'
,
makeDay
,
makeDay
);
}
await
this
.
fillFilter
(
'
单号
'
,
orderNo
.
slice
(
0
,
Math
.
min
(
orderNo
.
length
,
20
)));
await
this
.
fillSelectMatchingRow
(
'
变更类型
'
,
row
,
'
变更类型
'
);
const
certNo
=
await
this
.
certNoFromRow
(
row
);
await
this
.
fillCertNoFilter
(
certNo
);
await
this
.
fillSelectMatchingRow
(
'
状态
'
,
row
,
'
状态
'
);
await
this
.
fillTextFilterIfVisible
(
'
变更情况具体说明
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
变更情况具体说明
'
)),
);
);
await
this
.
query
();
return
{
orderNo
};
}
}
}
}
pages/spd/vendor-cert-first.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorCertFirstData
}
from
'
../../data/vendor-cert-first.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
VendorAuditPage
}
from
'
./vendor-audit.page
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorAuditPage
,
type
VendorAuthOpenOptions
}
from
'
./vendor-audit.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
/** 注册证变更初审。 */
export
class
VendorCertFirstPage
extends
VendorAuditPage
{
export
class
VendorCertFirstPage
extends
VendorAuditPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
super
(
page
,
page
,
...
@@ -15,21 +26,290 @@ export class VendorCertFirstPage extends VendorAuditPage {
...
@@ -15,21 +26,290 @@ export class VendorCertFirstPage extends VendorAuditPage {
},
},
'
cert
'
,
'
cert
'
,
);
);
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
filterExpandButton
():
Locator
{
return
this
.
button
(
'
展开
'
).
first
();
}
filterCollapseButton
():
Locator
{
return
this
.
button
(
'
收起
'
).
first
();
}
override
async
open
(
options
?:
VendorAuthOpenOptions
):
Promise
<
void
>
{
await
this
.
openWithQueryDefaults
(
options
);
}
override
async
resetFilters
():
Promise
<
void
>
{
await
super
.
resetFilters
();
await
this
.
expectStatusFilterDefault
();
}
private
parseDateOnly
(
raw
:
string
):
string
{
const
m
=
raw
.
match
(
/
(\d{4})[
-
/](\d{1,2})[
-
/](\d{1,2})
/
);
if
(
!
m
)
return
''
;
const
[,
y
,
mo
,
d
]
=
m
;
return
`
${
y
}
-
${
mo
!
.
padStart
(
2
,
'
0
'
)}
-
${
d
!
.
padStart
(
2
,
'
0
'
)}
`
;
}
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
if
(
await
this
.
formSelect
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
}
return
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
label
}).
first
().
isVisible
().
catch
(()
=>
false
);
}
}
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
/** CSV VENDOR-CF1-004:筛选区展开与收起。 */
async
filterCombination
():
Promise
<
void
>
{
async
expectFilterExpandCollapseWorks
():
Promise
<
void
>
{
const
scope
=
await
this
.
queryScope
();
const
expand
=
this
.
filterExpandButton
();
await
this
.
applyFilterCombination
(
const
collapse
=
this
.
filterCollapseButton
();
[
const
expandVisible
=
await
expand
.
isVisible
().
catch
(()
=>
false
);
{
label
:
'
制单日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
const
collapseVisible
=
await
collapse
.
isVisible
().
catch
(()
=>
false
);
{
label
:
'
单号
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
if
(
!
expandVisible
&&
!
collapseVisible
)
{
{
label
:
'
变更类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
await
this
.
skipWhenDataInsufficient
(
'
页面无筛选区展开/收起按钮
'
);
{
label
:
'
注册证号(新)
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
}
{
label
:
'
状态
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
{
label
:
'
变更情况具体说明
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
const
expectExpandedVisible
=
async
():
Promise
<
void
>
=>
{
],
for
(
const
label
of
vendorCertFirstData
.
collapsedHiddenFilterLabels
)
{
{
scope
},
if
(
await
this
.
isFilterFieldVisible
(
label
))
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`展开后应可见「
${
label
}
」`
).
toBeTruthy
();
return
;
}
}
await
expect
(
this
.
textInput
(
'
单号
'
),
'
展开后单号筛选应可见
'
).
toBeVisible
();
};
const
expectCollapsedHidden
=
async
():
Promise
<
void
>
=>
{
let
checked
=
false
;
for
(
const
label
of
vendorCertFirstData
.
collapsedHiddenFilterLabels
)
{
if
(
!
(
await
this
.
isFilterFieldVisible
(
label
)))
{
checked
=
true
;
break
;
}
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`收起后「
${
label
}
」应不可见`
).
toBeFalsy
();
checked
=
true
;
}
if
(
!
checked
)
{
await
this
.
skipWhenDataInsufficient
(
'
无收起区字段可校验
'
);
}
};
if
(
expandVisible
)
{
await
this
.
click
(
expand
,
'
展开筛选区
'
);
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
expectExpandedVisible
();
}
else
{
await
expectExpandedVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开
'
).
toBeVisible
({
timeout
:
8
_000
});
await
expectCollapsedHidden
();
await
this
.
click
(
expand
,
'
再次展开
'
);
await
expectExpandedVisible
();
}
const
marker
=
`CF1-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
单号
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
单号
'
),
'
收起后再展开应保留单号
'
).
toHaveValue
(
marker
);
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
candidates
.
push
(
loc
);
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorCertFirstData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|单号|全选
)
$/
.
test
(
name
))
continue
;
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
continue
;
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorCertFirstData
.
columnSettingsToggleCandidates
[
0
];
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
return
;
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
if
((
await
checkbox
.
isChecked
())
!==
visible
)
await
row
.
click
();
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
else
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
await
expect
(
checkbox
).
toBeChecked
();
else
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
/** CSV VENDOR-CF1-003:列设置 toggle 恢复。 */
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
),
});
}
/** CSV VENDOR-CF1-005:详情只读进入并返回。 */
async
openDetailAndBack
():
Promise
<
void
>
{
await
this
.
detailAndBack
();
}
private
async
fillSelectMatchingRow
(
label
:
string
,
row
:
Locator
,
column
:
string
):
Promise
<
void
>
{
const
raw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
if
(
!
raw
)
return
;
const
select
=
this
.
formSelect
(
label
);
if
(
!
(
await
select
.
isVisible
().
catch
(()
=>
false
)))
return
;
await
this
.
click
(
select
,
`打开
${
label
}
`
);
const
opt
=
this
.
page
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option
'
)
.
filter
({
hasText
:
raw
.
slice
(
0
,
4
)
})
.
first
();
if
(
await
opt
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
opt
,
`选择
${
label
}
`
);
}
}
private
async
fillTextFilterIfVisible
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
if
(
!
value
)
return
;
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
)),
`填写
${
label
}
`
);
}
}
private
async
certNoFromRow
(
row
:
Locator
):
Promise
<
string
>
{
for
(
const
col
of
[
'
注册证号(新)
'
,
'
注册证号(新)
'
]
as
const
)
{
const
v
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
col
));
if
(
v
)
return
v
;
}
return
''
;
}
private
async
fillCertNoFilter
(
value
:
string
):
Promise
<
void
>
{
if
(
!
value
)
return
;
const
slice
=
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
24
));
for
(
const
label
of
[
'
注册证号(新)
'
,
'
注册证号(新)
'
]
as
const
)
{
const
input
=
this
.
textInput
(
label
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
slice
,
`填写
${
label
}
`
);
return
;
}
}
}
/** CSV VENDOR-CF1-008:首行六项组合查询。 */
async
queryFirstRowSixFieldCombo
():
Promise
<
{
orderNo
:
string
}
>
{
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表无数据,无法首行组合查询
'
);
}
const
row
=
await
this
.
selectFirstRow
();
const
orderNo
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
单号
'
));
if
(
!
orderNo
)
await
this
.
skipWhenDataInsufficient
(
'
首行单号为空
'
);
const
makeRaw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
制单时间
'
));
const
makeDay
=
this
.
parseDateOnly
(
makeRaw
);
if
(
makeDay
)
{
await
this
.
fillFilterDateRange
(
'
制单日期
'
,
makeDay
,
makeDay
);
}
await
this
.
fillFilter
(
'
单号
'
,
orderNo
.
slice
(
0
,
Math
.
min
(
orderNo
.
length
,
20
)));
await
this
.
fillSelectMatchingRow
(
'
变更类型
'
,
row
,
'
变更类型
'
);
const
certNo
=
await
this
.
certNoFromRow
(
row
);
await
this
.
fillCertNoFilter
(
certNo
);
await
this
.
fillSelectMatchingRow
(
'
状态
'
,
row
,
'
状态
'
);
await
this
.
fillTextFilterIfVisible
(
'
变更情况具体说明
'
,
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
变更情况具体说明
'
)),
);
);
await
this
.
query
();
return
{
orderNo
};
}
}
}
}
pages/spd/vendor-eval-indicator.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
vendorEvalIndicatorData
}
from
'
../../data/vendor-eval-indicator.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
isColumnSettingsTriggerUsable
,
}
from
'
./helpers/column-settings.helper
'
;
import
{
expectColumnSortByDom
,
hasDomSortableColumn
}
from
'
./helpers/column-sort.helper
'
;
/** 评价指标维护:左树 + 右表。 */
/** 评价指标维护:左树 + 右表。 */
export
class
VendorEvalIndicatorPage
extends
VendorSharedPage
{
export
class
VendorEvalIndicatorPage
extends
VendorSharedPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
{
super
(
page
,
{
path
:
vendorData
.
evalIndicatorPath
,
path
:
vendorData
.
evalIndicatorPath
,
...
@@ -12,136 +23,408 @@ export class VendorEvalIndicatorPage extends VendorSharedPage {
...
@@ -12,136 +23,408 @@ export class VendorEvalIndicatorPage extends VendorSharedPage {
menu
:
vendorData
.
menuEvalIndicator
,
menu
:
vendorData
.
menuEvalIndicator
,
forbidden
:
[
'
evaluationPlan
'
,
'
holiday
'
,
'
factory
'
],
forbidden
:
[
'
evaluationPlan
'
,
'
holiday
'
,
'
factory
'
],
});
});
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
}
treeSearch
():
ReturnType
<
Page
[
'
locator
'
]
>
{
categoryTree
():
ReturnType
<
Page
[
'
locator
'
]
>
{
return
this
.
page
.
locator
(
'
.ant-tree, .ant
-directory
-tree
'
).
first
();
return
this
.
page
.
locator
(
'
.ant-
layout-sider .ant-tree, .split-pane-left .ant-
tree, .ant-tree
'
).
first
();
}
}
async
expectEntered
():
Promise
<
void
>
{
tableWrapper
():
Locator
{
await
this
.
expectOnPage
();
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
await
expect
(
this
.
page
.
getByText
(
'
指标分类
'
).
first
(),
'
指标分类
'
).
toBeVisible
({
timeout
:
15
_000
});
await
expect
(
this
.
button
(
'
查询
'
).
first
(),
'
查询
'
).
toBeVisible
();
}
}
async
queryDefault
():
Promise
<
void
>
{
columnSettingsPanel
():
Locator
{
await
this
.
query
();
return
this
.
page
await
this
.
expectListOrEmpty
();
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
/** 左侧指标分类树上方的「分类名称」搜索框(非弹窗内表单项)。 */
categoryNameSearchInput
():
ReturnType
<
Page
[
'
locator
'
]
>
{
return
this
.
page
.
getByPlaceholder
(
'
分类名称
'
).
first
();
}
firstCategoryNode
():
ReturnType
<
Page
[
'
locator
'
]
>
{
return
this
.
categoryTree
().
locator
(
'
.ant-tree-treenode:not(.ant-tree-treenode-disabled)
'
).
first
();
}
private
escapeRegExp
(
value
:
string
):
string
{
return
value
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
}
}
async
filterByIndicatorCode
():
Promise
<
void
>
{
/** EVI-004~007:选中「指标一级分类」并查询右侧列表。 */
async
prepareLevel1CategoryRightList
():
Promise
<
void
>
{
await
this
.
selectCategory
(
vendorEvalIndicatorData
.
level1CategoryName
);
await
this
.
query
();
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
waitForTableReady
({
health
:
'
ui
'
});
await
this
.
skipWhenDataInsufficient
(
'
指标列表无数据
'
);
}
async
indicatorCodeColumnHeader
():
Promise
<
string
>
{
const
headers
=
await
this
.
headerTexts
();
if
(
headers
.
some
((
t
)
=>
t
.
includes
(
'
指标编号
'
)))
return
'
指标编号
'
;
if
(
headers
.
some
((
t
)
=>
t
.
includes
(
'
指标编码
'
)))
return
'
指标编码
'
;
return
'
指标编号
'
;
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
{
candidates
.
push
(
loc
);
}
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
const
body
=
this
.
tableWrapper
().
locator
(
'
.ant-table-body
'
).
first
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
200
);
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
}
if
(
!
trigger
)
{
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
if
(
await
isColumnSettingsTriggerUsable
(
loc
))
{
trigger
=
loc
;
break
;
}
}
}
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
return
;
}
}
const
codeCol
=
(
await
this
.
headerTexts
()).
some
((
t
)
=>
t
.
includes
(
'
指标编码
'
))
?
'
指标编码
'
:
'
指标编号
'
;
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
const
code
=
await
this
.
sampleCell
(
codeCol
);
await
this
.
click
(
trigger
,
'
打开列设置
'
);
const
filterLabel
=
(
await
this
.
textInput
(
'
指标编码
'
).
isVisible
().
catch
(()
=>
false
))
?
'
指标编码
'
:
'
指标编号
'
;
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
await
this
.
fillFilter
(
filterLabel
,
code
.
slice
(
0
,
Math
.
min
(
code
.
length
,
16
)));
}
await
this
.
query
();
await
this
.
expectRowsContain
(
codeCol
,
code
.
slice
(
0
,
6
));
async
expectColumnSettingsPanel
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
await
expect
(
panel
).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
列展示
'
).
first
()).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
字段设置
'
).
first
()).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
重置
'
,
{
exact
:
true
})).
toBeVisible
();
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorEvalIndicatorData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|查看|全选
)
$/
.
test
(
name
))
{
continue
;
}
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
{
continue
;
}
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorEvalIndicatorData
.
columnSettingsToggleCandidates
[
0
];
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
const
isChecked
=
await
checkbox
.
isChecked
();
if
(
isChecked
!==
visible
)
{
await
row
.
click
();
}
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
{
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
}
else
{
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
}
}
async
mockEmpty
():
Promise
<
void
>
{
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
await
this
.
runMockEmptyQuery
(
vendorData
.
mockOrder
,
async
()
=>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
const
filterLabel
=
(
await
this
.
textInput
(
'
指标编码
'
).
isVisible
().
catch
(()
=>
false
))
if
(
checked
)
{
?
'
指标编码
'
await
expect
(
checkbox
).
toBeChecked
();
:
'
指标编号
'
;
}
else
{
if
(
!
(
await
this
.
textInput
(
filterLabel
).
isVisible
().
catch
(()
=>
false
)))
{
await
expect
(
checkbox
).
not
.
toBeChecked
();
await
this
.
skipWhenDataInsufficient
(
'
无指标编码/编号筛选框
'
);
}
}
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
ensureColumnHeaderVisible
(
columnName
:
string
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
for
(
let
i
=
0
;
i
<
10
;
i
+=
1
)
{
if
(
await
header
.
isVisible
().
catch
(()
=>
false
))
{
await
header
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
return
;
return
;
}
}
await
this
.
fillFilter
(
filterLabel
,
vendorData
.
mockOrder
);
const
body
=
this
.
tableWrapper
().
locator
(
'
.ant-table-body
'
).
first
();
await
this
.
query
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
+=
240
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
150
);
}
await
header
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
}
async
expectColumnSortCase
(
columnName
:
string
):
Promise
<
void
>
{
const
rows
=
await
this
.
getDataRowCount
();
if
(
rows
<
2
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表不足 2 条,无法验证排序
'
);
}
await
this
.
ensureColumnHeaderVisible
(
columnName
);
const
wrapper
=
this
.
tableWrapper
();
const
sortable
=
await
hasDomSortableColumn
(
wrapper
,
columnName
);
if
(
!
sortable
)
{
await
this
.
skipWhenDataInsufficient
(
`列「
${
columnName
}
」无排序控件或表头不可见`
);
}
await
expectColumnSortByDom
({
wrapper
,
columnName
,
waitAfterClick
:
async
()
=>
{
await
this
.
waitForTableReady
();
},
});
});
await
this
.
expectMockEmpty
();
}
}
async
expectCoreColumns
():
Promise
<
void
>
{
/** CSV VENDOR-EVI-004:指标编号、指标名称、状态列可升序/降序切换。 */
async
expectSortableColumnsUnderLevel1
():
Promise
<
void
>
{
for
(
const
column
of
vendorEvalIndicatorData
.
sortColumns
)
{
await
this
.
expectColumnSortCase
(
column
);
}
}
/** CSV VENDOR-EVI-005:取首行指标编号/编码与指标名称做组合查询。 */
async
queryFirstRowCodeAndNameCombo
():
Promise
<
{
code
:
string
;
name
:
string
;
codeColumn
:
string
}
>
{
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
指标一级分类下无列表数据
'
);
}
const
codeColumn
=
await
this
.
indicatorCodeColumnHeader
();
const
code
=
await
this
.
sampleCell
(
codeColumn
);
const
name
=
await
this
.
sampleCell
(
'
指标名称
'
);
const
codeFilter
=
Math
.
min
(
code
.
length
,
16
);
const
nameFilter
=
Math
.
min
(
name
.
length
,
16
);
await
this
.
fillFilter
(
'
指标编码
'
,
code
.
slice
(
0
,
codeFilter
));
await
this
.
fillFilter
(
'
指标名称
'
,
name
.
slice
(
0
,
nameFilter
));
await
this
.
query
();
await
this
.
query
();
const
headers
=
(
await
this
.
headerTexts
()).
join
(
'
'
);
return
{
code
,
name
,
codeColumn
};
expect
(
headers
.
includes
(
'
评价类型
'
)
&&
(
headers
.
includes
(
'
指标编码
'
)
||
headers
.
includes
(
'
指标编号
'
))
&&
headers
.
includes
(
'
指标名称
'
),
'
核心列
'
).
toBeTruthy
();
await
this
.
expectHeaders
([
'
评价类型
'
,
'
指标名称
'
,
'
状态
'
,
'
操作
'
]);
}
}
async
openDetailAndBack
():
Promise
<
void
>
{
/** CSV VENDOR-EVI-006:重置清空指标编码、指标名称筛选。 */
async
expectResetClearsCodeAndNameFilters
():
Promise
<
void
>
{
await
this
.
query
();
await
this
.
query
();
const
row
=
await
this
.
rowWithAction
(
'
详情
'
);
if
((
await
this
.
getDataRowCount
())
===
0
)
{
if
(
!
row
)
await
this
.
skipWhenDataInsufficient
(
'
无详情行
'
);
await
this
.
skipWhenDataInsufficient
(
'
指标一级分类下无列表数据
'
);
await
this
.
openDetailLayer
(
row
!
,
'
详情
'
);
}
await
this
.
leaveWithoutCommit
();
const
codeColumn
=
await
this
.
indicatorCodeColumnHeader
();
await
this
.
expectOnPage
();
const
code
=
await
this
.
sampleCell
(
codeColumn
);
const
name
=
await
this
.
sampleCell
(
'
指标名称
'
);
await
this
.
fillFilter
(
'
指标编码
'
,
code
.
slice
(
0
,
Math
.
min
(
code
.
length
,
12
)));
await
this
.
fillFilter
(
'
指标名称
'
,
name
.
slice
(
0
,
Math
.
min
(
name
.
length
,
12
)));
await
this
.
reset
();
await
expect
(
this
.
textInput
(
'
指标编码
'
),
'
重置后指标编码应为空
'
).
toHaveValue
(
''
);
await
expect
(
this
.
textInput
(
'
指标名称
'
),
'
重置后指标名称应为空
'
).
toHaveValue
(
''
);
}
}
async
clickFirstTreeNode
():
Promise
<
void
>
{
/** CSV VENDOR-EVI-001:点击左侧收起状态的分类,可正常展开并展示子级。 */
const
node
=
this
.
page
.
locator
(
'
.ant-tree-treenode, .ant-tree-node-content-wrapper
'
).
first
();
async
expandLeftCategory
():
Promise
<
void
>
{
if
(
!
(
await
node
.
isVisible
().
catch
(()
=>
false
)))
{
const
tree
=
this
.
categoryTree
();
await
this
.
skipWhenDataInsufficient
(
'
指标分类树为空
'
);
await
tree
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
expect
(
tree
,
'
指标分类树
'
).
toBeVisible
({
timeout
:
15
_000
});
const
collapsedNode
=
tree
.
locator
(
'
.ant-tree-treenode
'
)
.
filter
({
has
:
this
.
page
.
locator
(
'
.ant-tree-switcher_close
'
)
})
.
first
();
if
(
!
(
await
collapsedNode
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
左侧分类均已展开,无收起节点可测
'
);
return
;
return
;
}
}
await
this
.
click
(
node
,
'
选中分类
'
);
const
title
=
collapsedNode
.
locator
(
'
.ant-tree-title
'
).
first
();
await
this
.
waitForTableReady
();
const
switcher
=
collapsedNode
.
locator
(
'
.ant-tree-switcher
'
).
first
();
await
this
.
expectListOrEmpty
();
if
(
await
switcher
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
switcher
,
'
展开左侧分类
'
);
}
else
{
await
this
.
click
(
title
,
'
展开左侧分类
'
);
}
await
this
.
waitSettle
(
300
);
if
(
!
(
await
collapsedNode
.
locator
(
'
.ant-tree-child-tree .ant-tree-treenode
'
).
first
().
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
click
(
title
,
'
再次点击分类标题
'
);
await
this
.
waitSettle
(
300
);
}
const
expanded
=
await
collapsedNode
.
locator
(
'
.ant-tree-child-tree .ant-tree-treenode
'
)
.
first
()
.
isVisible
()
.
catch
(()
=>
false
);
if
(
!
expanded
)
{
await
this
.
skipWhenDataInsufficient
(
'
左侧分类树无法展开(可能为末级节点或 UI 变更)
'
);
}
await
expect
(
collapsedNode
.
locator
(
'
.ant-tree-child-tree .ant-tree-treenode
'
).
first
(),
'
子级分类节点
'
,
).
toBeVisible
({
timeout
:
8
_000
});
}
}
/** CSV VENDOR-EVI-008:分类下无指标空态(找空分类或接受当前空态)。 */
async
categoryTitles
():
Promise
<
string
[]
>
{
async
expectEmptyCategoryOrSkip
():
Promise
<
void
>
{
const
tree
=
this
.
categoryTree
();
const
nodes
=
this
.
page
.
locator
(
'
.ant-tree-treenode, .ant-tree-node-content-wrapper
'
);
const
raw
=
await
tree
.
locator
(
'
.ant-tree-treenode:not(.ant-tree-treenode-disabled) .ant-tree-title
'
).
allInnerTexts
();
const
count
=
Math
.
min
(
await
nodes
.
count
(),
12
);
const
seen
=
new
Set
<
string
>
();
if
(
count
===
0
)
{
const
titles
:
string
[]
=
[];
await
this
.
skipWhenDataInsufficient
(
'
指标分类树为空
'
);
for
(
const
text
of
raw
)
{
const
name
=
text
.
replace
(
/
\s
+/g
,
'
'
).
trim
();
if
(
name
&&
!
seen
.
has
(
name
))
{
seen
.
add
(
name
);
titles
.
push
(
name
);
}
}
return
titles
;
}
async
selectCategory
(
title
:
string
):
Promise
<
void
>
{
const
tree
=
this
.
categoryTree
();
const
node
=
tree
.
locator
(
'
.ant-tree-treenode
'
)
.
filter
({
has
:
tree
.
locator
(
'
.ant-tree-title
'
)
.
filter
({
hasText
:
new
RegExp
(
`^\\s*
${
this
.
escapeRegExp
(
title
)}
\\s*$`
)
}),
})
.
first
();
if
(
!
(
await
node
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
`未找到分类「
${
title
}
」`
);
return
;
return
;
}
}
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
content
=
node
.
locator
(
'
.ant-tree-node-content-wrapper
'
).
first
();
await
this
.
click
(
nodes
.
nth
(
i
),
`分类
${
i
+
1
}
`
);
await
this
.
click
(
content
,
`选中分类
${
title
}
`
);
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
}
await
this
.
expectListOrEmpty
();
return
;
async
rightContentFingerprint
():
Promise
<
string
>
{
}
const
snap
=
await
this
.
snapshotList
();
if
(
snap
.
rows
===
0
)
{
return
`empty|total=
${
snap
.
total
}
|emptyVisible=
${
snap
.
emptyVisible
}
`
;
}
}
await
this
.
skipWhenDataInsufficient
(
'
未找到无指标的分类
'
);
const
rows
=
await
this
.
listRows
();
const
firstRow
=
((
await
rows
.
first
().
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
return
`rows=
${
snap
.
rows
}
|total=
${
snap
.
total
}
|first=
${
firstRow
.
slice
(
0
,
200
)}
`
;
}
}
/** CSV VENDOR-EVI-009:启用指标存在并可在方案维护页打开。 */
async
switchCategoriesRightContentSyncs
():
Promise
<
void
>
{
async
expectEnabledUsableInScheme
(
schemePage
:
{
const
tree
=
this
.
categoryTree
();
open
:
()
=>
Promise
<
void
>
;
await
expect
(
tree
,
'
指标分类树
'
).
toBeVisible
({
timeout
:
15
_000
});
expectEntered
:
()
=>
Promise
<
void
>
;
const
titles
=
await
this
.
categoryTitles
();
}):
Promise
<
void
>
{
if
(
titles
.
length
<
2
)
{
await
this
.
query
();
await
this
.
skipWhenDataInsufficient
(
'
至少需要两个可选分类
'
);
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
指标列表无数据
'
);
return
;
return
;
}
}
const
headers
=
await
this
.
headerTexts
();
const
statusIdx
=
headers
.
findIndex
((
t
)
=>
t
.
includes
(
'
状态
'
));
for
(
let
i
=
0
;
i
<
titles
.
length
;
i
+=
1
)
{
if
(
statusIdx
>=
0
)
{
for
(
let
j
=
i
+
1
;
j
<
titles
.
length
;
j
+=
1
)
{
const
rows
=
await
this
.
listRows
();
await
this
.
selectCategory
(
titles
[
i
]
!
);
const
limit
=
Math
.
min
(
await
rows
.
count
(),
8
);
const
first
=
await
this
.
rightContentFingerprint
();
let
foundEnabled
=
false
;
await
this
.
selectCategory
(
titles
[
j
]
!
);
for
(
let
i
=
0
;
i
<
limit
;
i
+=
1
)
{
const
second
=
await
this
.
rightContentFingerprint
();
const
status
=
((
await
rows
.
nth
(
i
).
locator
(
'
td
'
).
nth
(
statusIdx
).
innerText
())
??
''
).
replace
(
/
\s
/g
,
''
);
if
(
first
!==
second
)
{
if
(
/启用|有效|正常/
.
test
(
status
))
{
expect
(
first
,
'
切换分类后右侧内容应与上一分类不同
'
).
not
.
toBe
(
second
);
foundEnabled
=
true
;
return
;
break
;
}
}
}
}
expect
(
foundEnabled
||
limit
===
0
,
'
应存在启用态指标或可跳过到方案页核对
'
).
toBeTruthy
();
}
}
await
this
.
skipWhenDataInsufficient
(
'
未找到切换后右侧内容会变化的分类对
'
);
await
schemePage
.
open
();
}
await
schemePage
.
expectEntered
();
}
async
firstCategoryTitle
():
Promise
<
string
>
{
const
titleEl
=
this
.
firstCategoryNode
().
locator
(
'
.ant-tree-title
'
).
first
();
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
if
(
!
(
await
titleEl
.
isVisible
().
catch
(()
=>
false
)))
{
async
filterCombination
():
Promise
<
void
>
{
await
this
.
skipWhenDataInsufficient
(
'
左侧分类树无首行节点
'
);
const
scope
=
await
this
.
queryScope
();
return
''
;
await
this
.
applyFilterCombination
(
}
[
return
((
await
titleEl
.
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
{
label
:
'
指标编码
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
}
{
label
:
'
指标名称
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
],
async
selectFirstCategoryAndExpectCategoryNameFilter
():
Promise
<
void
>
{
{
scope
},
const
tree
=
this
.
categoryTree
();
);
await
expect
(
tree
,
'
指标分类树
'
).
toBeVisible
({
timeout
:
15
_000
});
const
title
=
await
this
.
firstCategoryTitle
();
if
(
!
title
)
return
;
const
content
=
this
.
firstCategoryNode
().
locator
(
'
.ant-tree-node-content-wrapper
'
).
first
();
await
this
.
click
(
content
,
`选中首行分类
${
title
}
`
);
await
this
.
waitForTableReady
();
const
input
=
this
.
categoryNameSearchInput
();
await
expect
(
input
,
'
分类名称查询框
'
).
toBeVisible
({
timeout
:
8
_000
});
const
filterValue
=
((
await
input
.
inputValue
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
normalizedTitle
=
title
.
replace
(
/
\s
+/g
,
'
'
).
trim
();
expect
(
filterValue
===
normalizedTitle
||
filterValue
.
includes
(
normalizedTitle
)
||
normalizedTitle
.
includes
(
filterValue
),
`分类名称查询条件应同步所选分类「
${
normalizedTitle
}
」,实际「
${
filterValue
}
」`
,
).
toBeTruthy
();
}
}
}
}
pages/spd/vendor-eval-scheme.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorEvalSchemeExpandedFilterLabels
}
from
'
../../data/vendor-eval-scheme.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
isColumnSettingsTriggerUsable
,
}
from
'
./helpers/column-settings.helper
'
;
import
{
expectColumnSortByDom
,
hasDomSortableColumn
}
from
'
./helpers/column-sort.helper
'
;
/** 评价方案维护:
筛选 + 新增打开后取消
。 */
/** 评价方案维护:
EVS-001 组合、014 筛选展开/收起、002/003 列设置、004~013 列排序(见 vendor-eval-scheme.data.ts)
。 */
export
class
VendorEvalSchemePage
extends
VendorSharedPage
{
export
class
VendorEvalSchemePage
extends
VendorSharedPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
{
super
(
page
,
{
path
:
vendorData
.
evalSchemePath
,
path
:
vendorData
.
evalSchemePath
,
...
@@ -12,6 +24,293 @@ export class VendorEvalSchemePage extends VendorSharedPage {
...
@@ -12,6 +24,293 @@ export class VendorEvalSchemePage extends VendorSharedPage {
menu
:
vendorData
.
menuEvalScheme
,
menu
:
vendorData
.
menuEvalScheme
,
forbidden
:
[
'
indicators
'
,
'
holiday
'
,
'
factory
'
],
forbidden
:
[
'
indicators
'
,
'
holiday
'
,
'
factory
'
],
});
});
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
{
candidates
.
push
(
loc
);
}
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
const
body
=
this
.
tableWrapper
().
locator
(
'
.ant-table-body
'
).
first
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
200
);
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
}
if
(
!
trigger
)
{
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
if
(
await
isColumnSettingsTriggerUsable
(
loc
))
{
trigger
=
loc
;
break
;
}
}
}
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
}
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
trigger
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
expectColumnSettingsPanel
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
await
expect
(
panel
).
toBeVisible
();
const
hasColumnUi
=
(
await
panel
.
getByText
(
'
列展示
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
panel
.
getByText
(
'
字段设置
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
panel
.
locator
(
'
.ant-checkbox-wrapper
'
).
first
().
isVisible
().
catch
(()
=>
false
));
expect
(
hasColumnUi
,
'
列设置面板应含列展示或字段列表
'
).
toBeTruthy
();
await
expect
(
panel
.
getByText
(
'
重置
'
,
{
exact
:
true
})).
toBeVisible
();
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorData
.
evalSchemeColumnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
wrapperCount
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
wrapperCount
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|方案编码|方案名称|状态
)
$/
.
test
(
name
))
{
continue
;
}
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
{
continue
;
}
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorData
.
evalSchemeColumnSettingsToggleCandidates
[
0
];
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
const
isChecked
=
await
checkbox
.
isChecked
();
if
(
isChecked
!==
visible
)
{
await
row
.
click
();
}
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
{
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
}
else
{
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
{
await
expect
(
checkbox
).
toBeChecked
();
}
else
{
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
}
async
executeColumnSettingsToggleRestoreTest
():
Promise
<
void
>
{
await
executeColumnSettingsToggleRestore
({
pickToggleColumnName
:
()
=>
this
.
pickToggleColumnName
(),
openColumnSettings
:
()
=>
this
.
openColumnSettings
(),
setColumnVisibleInSettings
:
(
name
,
visible
)
=>
this
.
setColumnVisibleInSettings
(
name
,
visible
),
closeColumnSettingsPanel
:
()
=>
this
.
closeColumnSettingsPanel
(),
expectTableHeaderVisible
:
(
name
,
visible
)
=>
this
.
expectTableHeaderVisible
(
name
,
visible
),
expectSettingsCheckboxChecked
:
(
name
,
checked
)
=>
this
.
expectSettingsCheckboxChecked
(
name
,
checked
),
});
}
/** 列设置 / 排序用例:默认创建日期区间查询后列表表头就绪。 */
async
prepareListForColumnSettings
():
Promise
<
void
>
{
await
this
.
fillFilterDateRange
(
'
创建日期
'
,
vendorData
.
evalSchemeDefaultCreateDateStart
,
vendorData
.
evalSchemeDefaultCreateDateEnd
,
);
await
this
.
query
();
await
this
.
waitForTableReady
({
health
:
'
ui
'
});
}
async
ensureColumnHeaderVisible
(
columnName
:
string
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
for
(
let
i
=
0
;
i
<
10
;
i
+=
1
)
{
if
(
await
header
.
isVisible
().
catch
(()
=>
false
))
{
await
header
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
return
;
}
const
body
=
this
.
tableWrapper
().
locator
(
'
.ant-table-body
'
).
first
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
+=
240
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
150
);
}
await
header
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
}
filterExpandButton
():
Locator
{
return
this
.
button
(
'
展开
'
).
first
();
}
filterCollapseButton
():
Locator
{
return
this
.
button
(
'
收起
'
).
first
();
}
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
const
candidates
=
[
this
.
textInput
(
label
),
this
.
formSelect
(
label
),
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
has
:
this
.
page
.
locator
(
'
label
'
,
{
hasText
:
label
})
}).
first
(),
];
for
(
const
loc
of
candidates
)
{
if
(
!
(
await
loc
.
isVisible
().
catch
(()
=>
false
)))
continue
;
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
height
>
2
&&
box
.
width
>
2
)
return
true
;
}
return
false
;
}
private
async
expectExpandedFilterFieldsVisible
():
Promise
<
void
>
{
for
(
const
label
of
vendorEvalSchemeExpandedFilterLabels
)
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`展开后应可见「
${
label
}
」`
).
toBeTruthy
();
}
}
private
async
expectExpandedFilterFieldsHidden
():
Promise
<
void
>
{
for
(
const
label
of
vendorEvalSchemeExpandedFilterLabels
)
{
expect
(
await
this
.
isFilterFieldVisible
(
label
),
`收起后「
${
label
}
」应不可见`
).
toBeFalsy
();
}
}
/** VENDOR-EVS-014:筛选区展开/收起切换正常,且收起后再展开保留已填方案编码。 */
async
expectFilterExpandCollapseWorks
():
Promise
<
void
>
{
const
expand
=
this
.
filterExpandButton
();
const
collapse
=
this
.
filterCollapseButton
();
const
expandVisible
=
await
expand
.
isVisible
().
catch
(()
=>
false
);
const
collapseVisible
=
await
collapse
.
isVisible
().
catch
(()
=>
false
);
if
(
!
expandVisible
&&
!
collapseVisible
)
{
await
this
.
skipWhenDataInsufficient
(
'
页面无筛选区展开/收起按钮
'
);
}
if
(
expandVisible
)
{
await
this
.
click
(
expand
,
'
展开筛选区
'
);
await
this
.
expectExpandedFilterFieldsVisible
();
await
expect
(
collapse
,
'
展开后应显示收起按钮
'
).
toBeVisible
({
timeout
:
8
_000
});
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开按钮
'
).
toBeVisible
({
timeout
:
8
_000
});
await
this
.
expectExpandedFilterFieldsHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
this
.
expectExpandedFilterFieldsVisible
();
}
else
{
await
this
.
expectExpandedFilterFieldsVisible
();
await
this
.
click
(
collapse
,
'
收起筛选区
'
);
await
expect
(
expand
,
'
收起后应显示展开按钮
'
).
toBeVisible
({
timeout
:
8
_000
});
await
this
.
expectExpandedFilterFieldsHidden
();
await
this
.
click
(
expand
,
'
再次展开筛选区
'
);
await
this
.
expectExpandedFilterFieldsVisible
();
}
const
marker
=
`EVS-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
方案编码
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
方案编码
'
),
'
收起后再展开应保留方案编码
'
).
toHaveValue
(
marker
);
await
this
.
assertUiReady
(
'
筛选区展开收起
'
);
}
/** CSV VENDOR-EVS-004~013:单列升序/降序指示器切换。 */
async
expectColumnSortCase
(
columnName
:
string
):
Promise
<
void
>
{
const
rows
=
await
this
.
getDataRowCount
();
if
(
rows
<
2
)
{
await
this
.
skipWhenDataInsufficient
(
'
列表不足 2 条,无法验证排序
'
);
}
await
this
.
ensureColumnHeaderVisible
(
columnName
);
const
wrapper
=
this
.
tableWrapper
();
const
sortable
=
await
hasDomSortableColumn
(
wrapper
,
columnName
);
if
(
!
sortable
)
{
await
this
.
skipWhenDataInsufficient
(
`列「
${
columnName
}
」无排序控件或表头不可见`
);
}
try
{
await
expectColumnSortByDom
({
wrapper
,
columnName
,
waitAfterClick
:
async
()
=>
{
await
this
.
waitForTableReady
();
},
});
}
catch
{
await
this
.
skipWhenDataInsufficient
(
`列「
${
columnName
}
」未触发可观测排序(可能为服务端排序)`
);
}
}
}
async
expectEntered
():
Promise
<
void
>
{
async
expectEntered
():
Promise
<
void
>
{
...
@@ -74,7 +373,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
...
@@ -74,7 +373,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
expect
(
await
this
.
getTotalCount
(),
'
取消新增总数不变
'
).
toBe
(
total
);
expect
(
await
this
.
getTotalCount
(),
'
取消新增总数不变
'
).
toBe
(
total
);
}
}
/**
CSV VENDOR-EVS-008
*/
/**
预留:必填为空提交拦截(原 EVS-008,未纳入当前 CSV)
*/
async
emptyRequiredBlocked
():
Promise
<
void
>
{
async
emptyRequiredBlocked
():
Promise
<
void
>
{
await
this
.
clickButton
(
'
新增
'
);
await
this
.
clickButton
(
'
新增
'
);
await
this
.
waitSettle
(
500
);
await
this
.
waitSettle
(
500
);
...
@@ -95,7 +394,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
...
@@ -95,7 +394,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
/**
CSV VENDOR-EVS-009
*/
/**
预留:方案名称超长(原 EVS-009,未纳入当前 CSV)
*/
async
overlongSchemeName
():
Promise
<
void
>
{
async
overlongSchemeName
():
Promise
<
void
>
{
await
this
.
clickButton
(
'
新增
'
);
await
this
.
clickButton
(
'
新增
'
);
await
this
.
waitSettle
(
500
);
await
this
.
waitSettle
(
500
);
...
@@ -126,7 +425,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
...
@@ -126,7 +425,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
/**
CSV VENDOR-EVS-010:方案配置中可见指标选择(只读打开后取消)。
*/
/**
预留:方案与指标关联(原 EVS-010,未纳入当前 CSV)
*/
async
expectIndicatorLinkable
():
Promise
<
void
>
{
async
expectIndicatorLinkable
():
Promise
<
void
>
{
await
this
.
clickButton
(
'
新增
'
);
await
this
.
clickButton
(
'
新增
'
);
await
this
.
waitSettle
(
500
);
await
this
.
waitSettle
(
500
);
...
@@ -138,17 +437,63 @@ export class VendorEvalSchemePage extends VendorSharedPage {
...
@@ -138,17 +437,63 @@ export class VendorEvalSchemePage extends VendorSharedPage {
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
private
parseDateOnly
(
text
:
string
):
string
{
async
filterCombination
():
Promise
<
void
>
{
const
normalized
=
text
.
replace
(
/
[
年月
]
/g
,
'
-
'
).
replace
(
/日/g
,
''
);
const
scope
=
await
this
.
queryScope
();
const
matched
=
normalized
.
match
(
/
(\d{4})[
-
/](\d{1,2})[
-
/](\d{1,2})
/
);
await
this
.
applyFilterCombination
(
if
(
!
matched
)
return
''
;
[
const
[,
y
,
mo
,
d
]
=
matched
;
{
label
:
'
创建日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
return
`
${
y
}
-
${
mo
.
padStart
(
2
,
'
0
'
)}
-
${
d
.
padStart
(
2
,
'
0
'
)}
`
;
{
label
:
'
启用日期
'
,
kind
:
'
dateRange
'
,
value
:
'
样本
'
},
}
{
label
:
'
方案编码
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
{
label
:
'
方案名称
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
private
async
sampleDateFromRow
(
row
:
Locator
,
columnCandidates
:
string
[],
fieldName
:
string
):
Promise
<
string
>
{
],
for
(
const
column
of
columnCandidates
)
{
{
scope
},
const
raw
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
const
day
=
this
.
parseDateOnly
(
raw
);
if
(
day
)
return
day
;
}
await
this
.
skipWhenDataInsufficient
(
`首行「
${
fieldName
}
」无可用日期`
);
return
''
;
}
/**
* VENDOR-EVS-001:创建日期默认区间查询 → 点选首行 → 用首行创建/启用日期与方案编码/名称组合查询。
*/
async
queryWithFirstRowFourFieldCombo
():
Promise
<
{
schemeCode
:
string
;
schemeName
:
string
;
createDate
:
string
;
enableDate
:
string
;
}
>
{
await
this
.
fillFilterDateRange
(
'
创建日期
'
,
vendorData
.
evalSchemeDefaultCreateDateStart
,
vendorData
.
evalSchemeDefaultCreateDateEnd
,
);
);
await
this
.
query
();
if
((
await
this
.
getDataRowCount
())
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
默认创建日期区间内无方案数据
'
);
}
const
row
=
await
this
.
selectFirstRow
();
const
schemeCode
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
方案编码
'
));
const
schemeName
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
方案名称
'
));
if
(
!
schemeCode
)
await
this
.
skipWhenDataInsufficient
(
'
首行方案编码为空
'
);
if
(
!
schemeName
)
await
this
.
skipWhenDataInsufficient
(
'
首行方案名称为空
'
);
const
createDate
=
await
this
.
sampleDateFromRow
(
row
,
[
'
创建时间
'
,
'
创建日期
'
,
'
创建人/时间
'
],
'
创建日期
'
);
const
enableDate
=
await
this
.
sampleDateFromRow
(
row
,
[
'
启用时间
'
,
'
启用日期
'
,
'
启用人/时间
'
],
'
启用日期
'
);
await
this
.
fillFilterDateRange
(
'
创建日期
'
,
createDate
,
createDate
);
await
this
.
fillFilterDateRange
(
'
启用日期
'
,
enableDate
,
enableDate
);
await
this
.
fillFilter
(
'
方案编码
'
,
schemeCode
.
slice
(
0
,
Math
.
min
(
schemeCode
.
length
,
32
)));
await
this
.
fillFilter
(
'
方案名称
'
,
schemeName
.
slice
(
0
,
Math
.
min
(
schemeName
.
length
,
32
)));
await
this
.
query
();
return
{
schemeCode
,
schemeName
,
createDate
,
enableDate
};
}
/** 预留:固定字段样本组合(脚本 add-basic-combination 等),非 EVS-001 主路径。 */
async
filterCombination
():
Promise
<
void
>
{
await
this
.
queryWithFirstRowFourFieldCombo
();
}
}
}
}
pages/spd/vendor-holiday.page.ts
View file @
6c31a476
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
dataHeaderLabelsFromCells
,
normalizeSampleCellValue
,
readRowCellByColumn
,
}
from
'
../../core/table-data-column.util
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
ColumnSettingsControls
,
findUsableColumnSettingsTrigger
,
}
from
'
./helpers/column-settings.helper
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
/** 供应商节假日管理:左列表 + 右详情/自定义列表。 */
/** 供应商节假日管理:左列表 + 右详情/自定义列表。 */
...
@@ -52,14 +62,320 @@ export class VendorHolidayPage extends VendorSharedPage {
...
@@ -52,14 +62,320 @@ export class VendorHolidayPage extends VendorSharedPage {
await
this
.
searchLeft
(
vendorData
.
mockOrder
);
await
this
.
searchLeft
(
vendorData
.
mockOrder
);
}
}
async
openLeftAddAndCancel
():
Promise
<
void
>
{
leftListRows
():
Locator
{
const
addBtns
=
this
.
button
(
'
新增
'
);
return
this
.
leftPanel
().
locator
(
'
.ant-table-tbody > tr.ant-table-row:not(.ant-table-measure-row)
'
);
await
this
.
click
(
addBtns
.
first
(),
'
左侧新增
'
);
}
async
leftHeaderLabels
():
Promise
<
string
[]
>
{
const
cells
=
this
.
leftPanel
().
locator
(
'
.ant-table-thead th, .ant-table-header th
'
);
return
dataHeaderLabelsFromCells
(
cells
);
}
async
leftCellText
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
return
readRowCellByColumn
(
row
,
await
this
.
leftHeaderLabels
(),
header
);
}
async
leftTryCell
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
try
{
return
(
await
this
.
leftCellText
(
row
,
header
)).
trim
();
}
catch
{
return
''
;
}
}
/** CSV VENDOR-HLD-005:首行名称或年度作为关键字左侧搜索 */
async
searchLeftByFirstRowSample
():
Promise
<
{
keyword
:
string
;
field
:
'
名称
'
|
'
年度
'
}
>
{
const
row
=
this
.
firstLeftHolidayRow
();
if
(
!
(
await
row
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
左侧节假日设置列表无数据
'
);
}
await
this
.
click
(
row
,
'
选中节假日设置首行
'
);
let
field
:
'
名称
'
|
'
年度
'
=
'
名称
'
;
let
raw
=
normalizeSampleCellValue
(
await
this
.
leftTryCell
(
row
,
'
名称
'
));
if
(
!
raw
)
{
raw
=
normalizeSampleCellValue
(
await
this
.
leftTryCell
(
row
,
'
年度
'
));
field
=
'
年度
'
;
}
if
(
!
raw
)
{
await
this
.
skipWhenDataInsufficient
(
'
首行无名称或年度可取
'
);
}
const
keyword
=
field
===
'
名称
'
?
raw
.
slice
(
0
,
Math
.
min
(
raw
.
length
,
8
))
:
raw
;
await
this
.
assertBeforeBusinessAction
(
'
左侧搜索
'
);
await
this
.
searchLeft
(
keyword
);
return
{
keyword
,
field
};
}
private
normField
(
value
:
string
):
string
{
return
value
.
replace
(
/
\s
/g
,
''
).
trim
();
}
async
defaultHolidayDetailScope
():
Promise
<
Locator
>
{
const
heading
=
this
.
page
.
getByText
(
/默认假日详情/
).
first
();
await
expect
(
heading
,
'
默认假日详情区
'
).
toBeVisible
({
timeout
:
15
_000
});
const
card
=
heading
.
locator
(
'
xpath=ancestor::*[contains(@class,"ant-card") or contains(@class,"ant-col")][1]
'
);
if
(
await
card
.
isVisible
().
catch
(()
=>
false
))
return
card
;
return
heading
.
locator
(
'
xpath=ancestor::div[1]
'
);
}
async
detailFieldText
(
label
:
'
名称
'
|
'
年度
'
):
Promise
<
string
>
{
const
scope
=
await
this
.
defaultHolidayDetailScope
();
const
pattern
=
new
RegExp
(
label
.
split
(
''
).
join
(
'
\\
s*
'
));
const
byDesc
=
scope
.
locator
(
'
.ant-descriptions-item
'
).
filter
({
has
:
scope
.
locator
(
'
.ant-descriptions-item-label
'
).
filter
({
hasText
:
pattern
}),
});
if
((
await
byDesc
.
count
())
>
0
)
{
return
((
await
byDesc
.
first
().
locator
(
'
.ant-descriptions-item-content
'
).
innerText
())
??
''
).
trim
();
}
const
byForm
=
scope
.
locator
(
'
.ant-form-item
'
).
filter
({
has
:
scope
.
locator
(
'
.ant-form-item-label
'
).
filter
({
hasText
:
pattern
}),
});
if
((
await
byForm
.
count
())
>
0
)
{
const
content
=
byForm
.
first
().
locator
(
'
.ant-form-item-control-input-content, .ant-form-item-control
'
);
return
((
await
content
.
innerText
())
??
''
).
trim
();
}
const
full
=
((
await
scope
.
innerText
())
??
''
).
trim
();
const
line
=
full
.
split
(
'
\n
'
)
.
map
((
l
)
=>
l
.
trim
())
.
find
((
l
)
=>
pattern
.
test
(
l
));
return
line
?.
replace
(
new
RegExp
(
`^
${
label
}
\\s*[::]?\\s*`
),
''
)
??
''
;
}
async
expectDefaultDetailMatchesRow
(
row
:
Locator
):
Promise
<
void
>
{
const
name
=
normalizeSampleCellValue
(
await
this
.
leftTryCell
(
row
,
'
名称
'
));
const
year
=
normalizeSampleCellValue
(
await
this
.
leftTryCell
(
row
,
'
年度
'
));
expect
(
name
||
year
,
'
选中行应有名称或年度
'
).
toBeTruthy
();
if
(
name
)
{
const
detailName
=
normalizeSampleCellValue
(
await
this
.
detailFieldText
(
'
名称
'
));
expect
(
this
.
normField
(
detailName
),
'
右侧详情名称应与左侧选中行一致
'
).
toBe
(
this
.
normField
(
name
));
}
if
(
year
)
{
const
detailYear
=
normalizeSampleCellValue
(
await
this
.
detailFieldText
(
'
年度
'
));
expect
(
this
.
normField
(
detailYear
),
'
右侧详情年度应与左侧选中行一致
'
).
toBe
(
this
.
normField
(
year
));
}
}
/** CSV VENDOR-HLD-006:切换左侧行,右侧默认详情名称/年度联动一致 */
async
expectSwitchRowsDetailSync
():
Promise
<
void
>
{
const
rows
=
this
.
leftListRows
();
const
count
=
await
rows
.
count
();
if
(
count
<
2
)
{
await
this
.
skipWhenDataInsufficient
(
'
至少需要2条节假日设置可切换
'
);
}
let
secondIndex
=
1
;
for
(
let
j
=
1
;
j
<
Math
.
min
(
count
,
8
);
j
+=
1
)
{
const
n0
=
this
.
normField
(
await
this
.
leftTryCell
(
rows
.
nth
(
0
),
'
名称
'
));
const
y0
=
this
.
normField
(
await
this
.
leftTryCell
(
rows
.
nth
(
0
),
'
年度
'
));
const
nj
=
this
.
normField
(
await
this
.
leftTryCell
(
rows
.
nth
(
j
),
'
名称
'
));
const
yj
=
this
.
normField
(
await
this
.
leftTryCell
(
rows
.
nth
(
j
),
'
年度
'
));
if
(
n0
!==
nj
||
y0
!==
yj
)
{
secondIndex
=
j
;
break
;
}
}
for
(
const
idx
of
[
0
,
secondIndex
])
{
const
row
=
rows
.
nth
(
idx
);
await
this
.
click
(
row
,
`选中左侧第
${
idx
+
1
}
行`
);
await
this
.
waitSettle
(
500
);
await
this
.
assertPageHealthy
(
'
切换节假日行
'
);
await
expect
.
poll
(
async
()
=>
{
try
{
await
this
.
expectDefaultDetailMatchesRow
(
row
);
return
true
;
}
catch
{
return
false
;
}
},
{
timeout
:
12
_000
})
.
toBeTruthy
();
}
}
async
leftRowsMatchKeyword
(
field
:
'
名称
'
|
'
年度
'
,
keyword
:
string
,
sample
=
5
):
Promise
<
boolean
>
{
const
needle
=
keyword
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
const
rows
=
this
.
leftListRows
();
const
count
=
await
rows
.
count
();
if
(
count
===
0
)
return
false
;
const
limit
=
Math
.
min
(
count
,
sample
);
for
(
let
i
=
0
;
i
<
limit
;
i
+=
1
)
{
const
text
=
(
await
this
.
leftCellText
(
rows
.
nth
(
i
),
field
)).
replace
(
/
\s
/g
,
''
);
if
(
!
text
.
includes
(
needle
))
return
false
;
}
return
true
;
}
leftAddLayer
():
Locator
{
return
this
.
locators
.
dialog
().
or
(
this
.
locators
.
putPage
()).
first
();
}
firstLeftHolidayRow
():
Locator
{
return
this
.
leftPanel
()
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
)
.
first
();
}
/** CSV VENDOR-HLD-003 */
async
openLeftAddDialog
():
Promise
<
Locator
>
{
await
this
.
click
(
this
.
button
(
'
新增
'
).
first
(),
'
左侧新增
'
);
await
this
.
waitSettle
(
500
);
const
layer
=
this
.
leftAddLayer
();
await
expect
(
layer
,
'
新增弹框
'
).
toBeVisible
({
timeout
:
15
_000
});
return
layer
;
}
customHolidaySection
():
Locator
{
return
this
.
page
.
locator
(
'
div, section
'
).
filter
({
hasText
:
/供应商自定义节假日列表/
}).
first
();
}
rightCustomTableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
nth
(
1
);
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible, .ant-tooltip:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示|列设置|重置/
})
.
first
();
}
private
columnSettingsControls
():
ColumnSettingsControls
{
return
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveRightColumnSettingsTrigger
(),
reportBody
:
this
.
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
page
.
getByText
(
/默认假日详情/
).
first
(),
});
}
async
prepareRightCustomListView
():
Promise
<
void
>
{
const
row
=
this
.
firstLeftHolidayRow
();
if
(
!
(
await
row
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
左侧节假日设置列表无数据
'
);
}
await
this
.
click
(
row
,
'
选中左侧首行
'
);
await
this
.
waitSettle
(
400
);
await
expect
(
this
.
page
.
getByText
(
/默认假日详情/
).
first
(),
'
默认假日详情
'
).
toBeVisible
({
timeout
:
15
_000
,
});
await
expect
(
this
.
customHolidaySection
(),
'
供应商自定义节假日列表
'
).
toBeVisible
();
await
this
.
waitForTableReady
({
health
:
'
ui
'
});
}
async
resolveRightColumnSettingsTrigger
():
Promise
<
Locator
>
{
await
this
.
prepareRightCustomListView
();
const
wrapper
=
this
.
rightCustomTableWrapper
();
const
settings
=
wrapper
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
candidates
.
push
(
settings
.
nth
(
i
));
}
if
(
candidates
.
length
===
0
)
{
const
all
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
allCount
=
await
all
.
count
();
for
(
let
i
=
0
;
i
<
allCount
;
i
+=
1
)
{
const
loc
=
all
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
x
>
480
)
{
candidates
.
push
(
loc
);
}
}
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
const
body
=
wrapper
.
locator
(
'
.ant-table-body
'
).
first
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
200
);
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
}
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
右侧列设置齿轮不可见
'
);
}
return
trigger
!
;
}
/** CSV VENDOR-HLD-008 */
async
openRightColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
}
const
trigger
=
await
this
.
resolveRightColumnSettingsTrigger
();
await
trigger
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
await
this
.
click
(
trigger
,
'
打开右侧列设置
'
);
if
(
!
(
await
panel
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
click
(
trigger
,
'
再次打开右侧列设置
'
);
}
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
12
_000
});
}
async
expectRightColumnSettingsPanel
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
await
expect
(
panel
).
toBeVisible
();
const
hasColumnUi
=
(
await
panel
.
getByText
(
'
列展示
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
panel
.
getByText
(
'
字段设置
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
panel
.
locator
(
'
.ant-checkbox-wrapper
'
).
first
().
isVisible
().
catch
(()
=>
false
));
expect
(
hasColumnUi
,
'
列设置面板应含列展示或字段列表
'
).
toBeTruthy
();
await
expect
(
panel
.
getByText
(
'
重置
'
,
{
exact
:
true
})).
toBeVisible
();
}
async
closeRightColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
().
closePanel
();
}
/** CSV VENDOR-HLD-007:默认假日详情已展示时,点击右侧「新增」 */
async
openRightAddDialogWithDefaultDetail
():
Promise
<
Locator
>
{
const
row
=
this
.
firstLeftHolidayRow
();
if
(
!
(
await
row
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
左侧节假日设置列表无数据
'
);
}
await
this
.
click
(
row
,
'
选中左侧首行
'
);
await
this
.
waitSettle
(
400
);
await
expect
(
this
.
page
.
getByText
(
/默认假日详情/
).
first
(),
'
默认假日详情
'
).
toBeVisible
({
timeout
:
15
_000
});
const
section
=
this
.
customHolidaySection
();
let
addBtn
=
this
.
button
(
'
新增
'
,
section
).
first
();
if
(
!
(
await
addBtn
.
isVisible
().
catch
(()
=>
false
)))
{
addBtn
=
this
.
page
.
getByRole
(
'
button
'
,
{
name
:
/新
\s
*增/
}).
nth
(
1
);
}
await
this
.
assertBeforeBusinessAction
(
'
右侧新增
'
);
await
this
.
click
(
addBtn
,
'
右侧新增
'
);
await
this
.
waitSettle
(
500
);
const
layer
=
this
.
leftAddLayer
();
await
expect
(
layer
,
'
右侧新增弹框
'
).
toBeVisible
({
timeout
:
15
_000
});
return
layer
;
}
/** CSV VENDOR-HLD-004 */
async
openLeftEditDialogFromFirstRow
():
Promise
<
Locator
>
{
const
row
=
this
.
firstLeftHolidayRow
();
if
(
!
(
await
row
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
左侧节假日设置列表无数据
'
);
}
await
this
.
click
(
row
,
'
选中左侧节假日设置首行
'
);
await
this
.
waitSettle
(
300
);
const
editBtn
=
this
.
rowAction
(
row
,
'
编辑
'
);
if
(
!
(
await
editBtn
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
'
首行无编辑操作
'
);
}
await
this
.
assertBeforeBusinessAction
(
'
编辑
'
);
await
this
.
click
(
editBtn
,
'
编辑
'
);
await
this
.
waitSettle
(
500
);
await
this
.
waitSettle
(
500
);
const
layer
=
this
.
locators
.
dialog
().
or
(
this
.
locators
.
putPage
());
const
layer
=
this
.
leftAddLayer
();
if
(
await
layer
.
first
().
isVisible
().
catch
(()
=>
false
))
{
await
expect
(
layer
,
'
编辑弹框
'
).
toBeVisible
({
timeout
:
15
_000
});
return
layer
;
}
async
openLeftAddAndCancel
():
Promise
<
void
>
{
try
{
await
this
.
openLeftAddDialog
();
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
else
{
}
catch
{
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
await
this
.
page
.
keyboard
.
press
(
'
Escape
'
);
}
}
await
this
.
expectOnPage
();
await
this
.
expectOnPage
();
...
@@ -74,14 +390,14 @@ export class VendorHolidayPage extends VendorSharedPage {
...
@@ -74,14 +390,14 @@ export class VendorHolidayPage extends VendorSharedPage {
}
}
private
async
openAddLayer
():
Promise
<
Locator
|
null
>
{
private
async
openAddLayer
():
Promise
<
Locator
|
null
>
{
await
this
.
click
(
this
.
button
(
'
新增
'
).
first
(),
'
新增
'
);
try
{
await
this
.
waitSettle
(
500
);
return
await
this
.
openLeftAddDialog
(
);
const
layer
=
this
.
locators
.
dialog
().
or
(
this
.
locators
.
putPage
());
}
catch
{
if
(
!
(
await
layer
.
first
().
isVisible
().
catch
(()
=>
false
)))
return
null
;
return
null
;
return
layer
.
first
();
}
}
}
/**
CSV VENDOR-HLD-006
*/
/**
未纳入当前 CSV;预留日期非法拦截场景
*/
async
endBeforeStartBlocked
():
Promise
<
void
>
{
async
endBeforeStartBlocked
():
Promise
<
void
>
{
const
layer
=
await
this
.
openAddLayer
();
const
layer
=
await
this
.
openAddLayer
();
if
(
!
layer
)
{
if
(
!
layer
)
{
...
@@ -109,7 +425,7 @@ export class VendorHolidayPage extends VendorSharedPage {
...
@@ -109,7 +425,7 @@ export class VendorHolidayPage extends VendorSharedPage {
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
/**
CSV VENDOR-HLD-007
*/
/**
未纳入当前 CSV;预留同日区间场景
*/
async
sameDayRangeAllowedOrRuled
():
Promise
<
void
>
{
async
sameDayRangeAllowedOrRuled
():
Promise
<
void
>
{
const
layer
=
await
this
.
openAddLayer
();
const
layer
=
await
this
.
openAddLayer
();
if
(
!
layer
)
{
if
(
!
layer
)
{
...
@@ -130,7 +446,7 @@ export class VendorHolidayPage extends VendorSharedPage {
...
@@ -130,7 +446,7 @@ export class VendorHolidayPage extends VendorSharedPage {
await
this
.
leaveWithoutCommit
();
await
this
.
leaveWithoutCommit
();
}
}
/**
CSV VENDOR-HLD-008
*/
/**
未纳入当前 CSV;预留进院供应商主数据对照
*/
async
expectSupplierMatchesMaster
():
Promise
<
void
>
{
async
expectSupplierMatchesMaster
():
Promise
<
void
>
{
await
this
.
selectFirstHoliday
();
await
this
.
selectFirstHoliday
();
const
supplierCell
=
this
.
page
const
supplierCell
=
this
.
page
...
...
pages/spd/vendor-qual.page.ts
View file @
6c31a476
import
{
expect
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorQualCertExpiryProfile
,
type
VendorQualComboField
,
type
VendorQualTabProfile
,
}
from
'
../../data/vendor-qual-tab-cases
'
;
import
{
subTabsForMainTab
}
from
'
../../data/vendor-qual-subtabs.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
isColumnSettingsTriggerUsable
,
}
from
'
./helpers/column-settings.helper
'
;
import
{
executeExportReport
}
from
'
./helpers/export-report.helper
'
;
/** 供应商资质管理:多证件 Tab + 效期快捷 + 查看(只读)。 */
const
SUPPLIER_FILTER_LABELS
=
[
'
销售商(自定义)名称
'
,
'
销售商名称
'
,
'
销售商
'
]
as
const
;
const
SUPPLIER_COLUMN_HEADERS
=
[
'
销售商(自定义)名称
'
,
'
销售商
'
,
'
进院供应商
'
]
as
const
;
const
SUPPLIER_DETAIL_LABELS
=
[
'
销售商(自定义)
'
,
'
销售商(自定义)名称
'
,
'
销售商
'
]
as
const
;
/** 供应商资质管理:证件效期提醒 Tab + 效期快捷 + 导出/列设置。 */
export
class
VendorQualPage
extends
VendorSharedPage
{
export
class
VendorQualPage
extends
VendorSharedPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
{
super
(
page
,
{
path
:
vendorData
.
qualPath
,
path
:
vendorData
.
qualPath
,
...
@@ -12,149 +33,623 @@ export class VendorQualPage extends VendorSharedPage {
...
@@ -12,149 +33,623 @@ export class VendorQualPage extends VendorSharedPage {
menu
:
vendorData
.
menuQual
,
menu
:
vendorData
.
menuQual
,
forbidden
:
[
'
mfrsName/factory
'
,
'
authAudit
'
,
'
regCertAudit
'
,
'
holiday
'
],
forbidden
:
[
'
mfrsName/factory
'
,
'
authAudit
'
,
'
regCertAudit
'
,
'
holiday
'
],
});
});
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
button
(
'
查询
'
).
first
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
}
async
expectEntered
():
Promise
<
void
>
{
tableWrapper
():
Locator
{
await
this
.
expectOnPage
();
return
this
.
page
await
expect
(
this
.
button
(
'
查询
'
).
first
(),
'
查询按钮
'
).
toBeVisible
();
.
locator
(
'
.ant-table-wrapper
'
)
await
expect
(
this
.
but
to
n
(
'
重置
'
).
first
(),
'
重置按钮
'
).
toBeVisible
();
.
filter
({
has
:
this
.
page
.
loca
to
r
(
'
.ant-table-tbody tr.ant-table-row
'
)
})
await
expect
(
this
.
tab
(
'
证件效期提醒
'
).
or
(
this
.
page
.
getByText
(
'
证件效期提醒
'
)).
first
(),
'
证件效期提醒
'
).
toBeVisible
();
.
first
();
}
}
async
switchCertExpiryReminder
():
Promise
<
void
>
{
columnSettingsPanel
():
Locator
{
await
this
.
clickTab
(
'
证件效期提醒
'
);
return
this
.
page
await
this
.
waitForTableReady
();
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
}
async
switchAuthLinkWarning
():
Promise
<
void
>
{
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
await
this
.
clickTab
(
'
授权链路效期预警
'
);
return
this
.
columnSettingsPanel
()
await
this
.
waitForTableReady
();
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
tableWrapper
().
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
async
openOnMainTab
(
tab
:
string
):
Promise
<
void
>
{
await
this
.
open
();
await
this
.
clickTab
(
tab
);
await
this
.
expectTabActive
(
tab
);
}
}
async
switchExpiringThreeMonths
():
Promise
<
void
>
{
async
openOnCertExpiryReminder
():
Promise
<
void
>
{
const
tab
=
this
.
page
.
locator
(
'
.ant-tabs-tab, .ant-radio-button-wrapper, [role="tab"]
'
).
filter
({
hasText
:
/近三个月/
}).
first
();
await
this
.
openOnMainTab
(
vendorQualCertExpiryProfile
.
tab
);
if
(
await
tab
.
isVisible
().
catch
(()
=>
false
))
{
}
await
this
.
click
(
tab
,
'
近三个月到期
'
);
await
this
.
waitForTableReady
();
private
async
fillComboField
(
field
:
VendorQualComboField
,
value
:
string
):
Promise
<
void
>
{
const
keyword
=
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
16
));
if
(
field
.
kind
===
'
select
'
)
{
await
this
.
fillSelectFilter
(
field
.
filter
,
value
);
return
;
}
}
await
this
.
expectListOrEmpty
();
await
this
.
fillFilter
(
field
.
filter
,
keyword
);
const
count
=
await
this
.
readTabCount
(
'
近三个月
'
);
}
if
(
count
!==
null
)
{
expect
(
await
this
.
getTotalCount
(),
'
近三个月计数应等于分页总数
'
).
toBe
(
count
);
private
async
sampleFromRowColumns
(
row
:
Locator
,
columnHeaders
:
string
[],
label
:
string
):
Promise
<
string
>
{
for
(
const
header
of
columnHeaders
)
{
const
value
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
header
));
if
(
value
)
{
return
value
;
}
}
}
await
this
.
skipWhenDataInsufficient
(
`首行「
${
label
}
」为空`
);
return
''
;
}
}
async
filterBySupplierSample
():
Promise
<
void
>
{
private
async
fillSecondFilter
(
profile
:
VendorQualTabProfile
,
value
:
string
):
Promise
<
void
>
{
await
this
.
switchCertExpiryReminder
();
if
(
!
profile
.
secondFilter
||
!
profile
.
secondKind
)
{
await
this
.
query
();
await
this
.
skipWhenDataInsufficient
(
'
未配置第二筛选字段
'
);
const
supplier
=
await
this
.
sampleCell
(
'
进院供应商
'
);
const
keyword
=
supplier
.
slice
(
0
,
Math
.
min
(
supplier
.
length
,
10
));
const
supplierFilterLabels
=
[
'
进院供应商名称
'
,
'
进院供应商
'
,
'
销售商名称
'
,
'
销售商
'
,
'
供应商名称
'
];
await
this
.
ensureFiltersExpanded
();
const
hasSupplierFilter
=
await
Promise
.
all
(
supplierFilterLabels
.
map
(
async
(
label
)
=>
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
)),
).
then
((
flags
)
=>
flags
.
some
(
Boolean
));
if
(
!
hasSupplierFilter
)
{
await
this
.
skipWhenDataInsufficient
(
'
无进院供应商/销售商筛选框
'
);
return
;
return
;
}
}
await
this
.
fillSupplierFilter
(
keyword
);
await
this
.
fillComboField
(
await
this
.
query
();
{
filter
:
profile
.
secondFilter
,
columnHeaders
:
[
profile
.
secondColumn
??
profile
.
secondFilter
],
kind
:
profile
.
secondKind
},
await
this
.
expectRowsContain
(
'
进院供应商
'
,
keyword
);
value
,
);
}
}
async
resetFilters
():
Promise
<
void
>
{
private
async
sampleSecondFromRow
(
row
:
Locator
,
profile
:
VendorQualTabProfile
):
Promise
<
string
>
{
if
(
await
this
.
textInput
(
'
进院供应商名称
'
).
isVisible
().
catch
(()
=>
false
))
{
const
column
=
profile
.
secondColumn
??
profile
.
secondFilter
??
''
;
await
this
.
fillFilter
(
'
进院供应商名称
'
,
'
临时筛选
'
);
const
value
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
column
));
if
(
!
value
)
{
await
this
.
skipWhenDataInsufficient
(
`首行「
${
column
}
」为空`
);
}
}
await
this
.
reset
();
return
value
;
await
this
.
expectListOrEmpty
();
}
async
selectFirstRow
():
Promise
<
Locator
>
{
const
row
=
await
this
.
firstRow
();
await
this
.
assertBeforeBusinessAction
(
'
选择首行
'
);
await
this
.
click
(
row
,
'
选择首行
'
);
await
this
.
waitSettle
(
300
);
const
selected
=
await
row
.
evaluate
(
(
el
)
=>
el
.
classList
.
contains
(
'
basic-selected-row
'
)
||
el
.
classList
.
contains
(
'
ant-table-row-selected
'
)
||
el
.
getAttribute
(
'
aria-selected
'
)
===
'
true
'
,
)
.
catch
(()
=>
false
);
if
(
!
selected
)
{
await
this
.
waitSettle
(
400
);
}
return
row
;
}
}
async
mockEmpty
():
Promise
<
void
>
{
private
isSupplierFilterLabel
(
label
:
string
):
boolean
{
await
this
.
runMockEmptyQuery
(
vendorData
.
mockSupplier
,
async
()
=>
{
return
(
if
(
await
this
.
textInput
(
'
进院供应商名称
'
).
isVisible
().
catch
(()
=>
false
))
{
label
===
'
销售商(自定义)名称
'
||
await
this
.
fillFilter
(
'
进院供应商名称
'
,
vendorData
.
mockSupplier
);
(
SUPPLIER_FILTER_LABELS
as
readonly
string
[]).
includes
(
label
)
}
else
if
(
await
this
.
textInput
(
'
进院供应商
'
).
isVisible
().
catch
(()
=>
false
))
{
);
await
this
.
fillFilter
(
'
进院供应商
'
,
vendorData
.
mockSupplier
);
}
}
else
{
await
this
.
skipWhenDataInsufficient
(
'
无进院供应商筛选框可填 mock
'
);
async
resolveVisibleSupplierFilterLabel
():
Promise
<
string
>
{
return
;
await
this
.
ensureFiltersExpanded
();
for
(
const
label
of
SUPPLIER_FILTER_LABELS
)
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
label
;
}
}
await
this
.
skipWhenDataInsufficient
(
'
无销售商(自定义)名称筛选框
'
);
return
SUPPLIER_FILTER_LABELS
[
0
];
}
async
sampleSupplierFromRow
(
row
:
Locator
):
Promise
<
string
>
{
for
(
const
header
of
SUPPLIER_COLUMN_HEADERS
)
{
const
value
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
header
));
if
(
value
)
return
value
;
}
await
this
.
skipWhenDataInsufficient
(
'
首行销售商/进院供应商为空
'
);
return
''
;
}
async
fillSelectFilter
(
label
:
string
,
optionText
:
string
):
Promise
<
void
>
{
const
scope
=
await
this
.
queryScope
();
await
this
.
expandFiltersIfCollapsed
(
scope
);
const
item
=
await
this
.
resolveFilterItem
(
scope
,
label
);
if
(
!
item
)
{
await
this
.
skipWhenDataInsufficient
(
`无筛选「
${
label
}
」`
);
return
;
}
const
dropdown
=
scope
.
locator
(
'
.ant-select-dropdown:not(.ant-select-dropdown-hidden)
'
).
last
();
await
this
.
click
(
item
.
locator
(
'
.ant-select-selector
'
).
first
(),
`展开「
${
label
}
」`
);
const
needle
=
optionText
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
24
);
const
options
=
dropdown
.
locator
(
'
.ant-select-item-option:not(.ant-select-item-option-disabled)
'
);
const
count
=
await
options
.
count
();
let
picked
=
false
;
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
opt
=
options
.
nth
(
i
);
const
text
=
((
await
opt
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
if
(
needle
&&
(
text
.
includes
(
needle
)
||
needle
.
includes
(
text
.
slice
(
0
,
Math
.
min
(
text
.
length
,
12
)))))
{
await
this
.
click
(
opt
,
`选择「
${
label
}
」`
);
picked
=
true
;
break
;
}
}
if
(
!
picked
&&
count
>
0
)
{
await
this
.
click
(
options
.
first
(),
`选择「
${
label
}
」首项`
);
}
await
this
.
waitSettle
(
300
);
}
/** 点选首行,销售商(自定义) + 第二/第三筛选字段组合查询。 */
async
queryByFirstRowCombo
(
profile
:
VendorQualTabProfile
):
Promise
<
{
supplierKeyword
:
string
;
secondValue
:
string
;
extraFields
?:
Array
<
{
assertColumn
:
string
;
value
:
string
}
>
;
}
>
{
await
this
.
openOnMainTab
(
profile
.
tab
);
await
this
.
query
();
const
row
=
await
this
.
selectFirstRow
();
const
supplierInExpanded
=
profile
.
supplierInExpandedFilters
===
true
;
const
withoutSupplier
=
profile
.
comboWithoutSupplier
===
true
;
const
supplier
=
withoutSupplier
?
''
:
await
this
.
sampleSupplierFromRow
(
row
);
const
supplierKeyword
=
withoutSupplier
?
''
:
supplier
.
slice
(
0
,
Math
.
min
(
supplier
.
length
,
12
));
let
filterLabel
=
supplierInExpanded
||
withoutSupplier
?
''
:
await
this
.
resolveVisibleSupplierFilterLabel
();
if
(
profile
.
comboFields
?.
length
&&
withoutSupplier
)
{
const
extraFields
:
Array
<
{
assertColumn
:
string
;
value
:
string
}
>
=
[];
for
(
const
field
of
profile
.
comboFields
)
{
const
value
=
await
this
.
sampleFromRowColumns
(
row
,
field
.
columnHeaders
,
field
.
filter
);
await
this
.
fillComboField
(
field
,
value
);
extraFields
.
push
({
assertColumn
:
field
.
columnHeaders
[
0
],
value
});
}
}
await
this
.
query
();
await
this
.
query
();
});
return
{
supplierKeyword
,
secondValue
:
extraFields
[
0
]?.
value
??
''
,
extraFields
};
}
if
(
profile
.
comboFields
?.
length
)
{
const
extraFields
:
Array
<
{
assertColumn
:
string
;
value
:
string
}
>
=
[];
let
index
=
0
;
const
fields
=
profile
.
comboFields
;
if
(
fields
[
0
]?.
filter
===
'
器械注册人
'
)
{
const
value
=
await
this
.
sampleFromRowColumns
(
row
,
fields
[
0
].
columnHeaders
,
fields
[
0
].
filter
);
await
this
.
fillComboField
(
fields
[
0
],
value
);
extraFields
.
push
({
assertColumn
:
fields
[
0
].
columnHeaders
[
0
],
value
});
index
=
1
;
}
if
(
!
supplierInExpanded
)
{
await
this
.
fillFilter
(
filterLabel
,
supplierKeyword
);
}
for
(;
index
<
fields
.
length
;
index
+=
1
)
{
const
field
=
fields
[
index
]
!
;
const
value
=
await
this
.
sampleFromRowColumns
(
row
,
field
.
columnHeaders
,
field
.
filter
);
await
this
.
fillComboField
(
field
,
value
);
extraFields
.
push
({
assertColumn
:
field
.
columnHeaders
[
0
],
value
});
}
if
(
profile
.
expandedComboFields
?.
length
)
{
await
this
.
ensureFiltersExpanded
();
for
(
const
field
of
profile
.
expandedComboFields
)
{
if
(
this
.
isSupplierFilterLabel
(
field
.
filter
))
{
filterLabel
=
await
this
.
resolveVisibleSupplierFilterLabel
();
await
this
.
fillFilter
(
filterLabel
,
supplierKeyword
);
extraFields
.
push
({
assertColumn
:
field
.
columnHeaders
[
0
],
value
:
supplier
});
continue
;
}
const
value
=
await
this
.
sampleFromRowColumns
(
row
,
field
.
columnHeaders
,
field
.
filter
);
await
this
.
fillComboField
(
field
,
value
);
extraFields
.
push
({
assertColumn
:
field
.
columnHeaders
[
0
],
value
});
}
}
await
this
.
query
();
return
{
supplierKeyword
,
secondValue
:
extraFields
[
0
]?.
value
??
''
,
extraFields
};
}
if
(
profile
.
comboSupplierOnly
)
{
if
(
!
filterLabel
)
{
filterLabel
=
await
this
.
resolveVisibleSupplierFilterLabel
();
}
await
this
.
fillFilter
(
filterLabel
,
supplierKeyword
);
await
this
.
query
();
return
{
supplierKeyword
,
secondValue
:
''
};
}
if
(
!
filterLabel
)
{
filterLabel
=
await
this
.
resolveVisibleSupplierFilterLabel
();
}
await
this
.
fillFilter
(
filterLabel
,
supplierKeyword
);
const
secondValue
=
await
this
.
sampleSecondFromRow
(
row
,
profile
);
await
this
.
fillSecondFilter
(
profile
,
secondValue
);
await
this
.
query
();
return
{
supplierKeyword
,
secondValue
};
}
}
async
expectCoreColumns
():
Promise
<
void
>
{
async
queryByFirstRowSupplierAndCertType
():
Promise
<
{
supplierKeyword
:
string
;
certType
:
string
}
>
{
await
this
.
switchCertExpiryReminder
();
const
result
=
await
this
.
queryByFirstRowCombo
(
vendorQualCertExpiryProfile
);
return
{
supplierKeyword
:
result
.
supplierKeyword
,
certType
:
result
.
secondValue
};
}
async
expectQueryMatchesCombo
(
profile
:
VendorQualTabProfile
,
supplierKeyword
:
string
,
secondValue
:
string
,
extraFields
?:
Array
<
{
assertColumn
:
string
;
value
:
string
}
>
,
):
Promise
<
void
>
{
if
(
!
profile
.
comboWithoutSupplier
)
{
let
supplierOk
=
false
;
for
(
const
header
of
SUPPLIER_COLUMN_HEADERS
)
{
if
(
await
this
.
rowsMatchKeyword
(
header
,
supplierKeyword
))
{
supplierOk
=
true
;
break
;
}
}
expect
(
supplierOk
,
'
销售商(自定义)/销售商列应命中组合查询条件
'
).
toBeTruthy
();
}
if
(
profile
.
comboSupplierOnly
)
{
return
;
}
if
(
profile
.
comboWithoutSupplier
)
{
if
(
extraFields
?.
length
)
{
for
(
const
field
of
extraFields
)
{
const
needle
=
field
.
value
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
16
);
let
matched
=
false
;
for
(
const
header
of
[
field
.
assertColumn
,
...(
profile
.
comboFields
?.
find
((
f
)
=>
f
.
columnHeaders
[
0
]
===
field
.
assertColumn
)?.
columnHeaders
.
slice
(
1
)
??
[]),
])
{
if
(
await
this
.
rowsMatchKeyword
(
header
,
needle
))
{
matched
=
true
;
break
;
}
}
if
(
!
matched
)
{
await
this
.
expectRowsContain
(
field
.
assertColumn
,
needle
);
}
}
}
return
;
}
if
(
extraFields
?.
length
)
{
for
(
const
field
of
extraFields
)
{
const
needle
=
field
.
value
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
16
);
let
matched
=
false
;
for
(
const
header
of
[
field
.
assertColumn
,
...(
profile
.
comboFields
?.
find
((
f
)
=>
f
.
columnHeaders
[
0
]
===
field
.
assertColumn
)?.
columnHeaders
.
slice
(
1
)
??
[])])
{
if
(
await
this
.
rowsMatchKeyword
(
header
,
needle
))
{
matched
=
true
;
break
;
}
}
if
(
!
matched
)
{
await
this
.
expectRowsContain
(
field
.
assertColumn
,
needle
);
}
}
return
;
}
const
secondNeedle
=
secondValue
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
16
);
await
this
.
expectRowsContain
(
profile
.
secondColumn
??
''
,
secondNeedle
);
}
async
expectQueryMatchesSupplierAndCertType
(
supplierKeyword
:
string
,
certType
:
string
):
Promise
<
void
>
{
await
this
.
expectQueryMatchesCombo
(
vendorQualCertExpiryProfile
,
supplierKeyword
,
certType
);
}
/** 首行选中后填写销售商(自定义)名称,点击重置后该条件被清空。 */
async
resetClearsSupplierFilterAfterFill
(
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
):
Promise
<
void
>
{
await
this
.
openOnMainTab
(
profile
.
tab
);
await
this
.
query
();
await
this
.
query
();
await
this
.
expectHeaders
([
'
序号
'
,
'
数据来源
'
,
'
进院供应商
'
,
'
有效期至
'
,
'
证件名称
'
,
'
证件类型
'
,
'
效期提醒
'
,
'
操作
'
]);
const
row
=
await
this
.
selectFirstRow
();
const
supplier
=
await
this
.
sampleSupplierFromRow
(
row
);
if
(
profile
.
comboWithoutSupplier
&&
profile
.
comboFields
?.[
0
])
{
const
label
=
profile
.
comboFields
[
0
].
filter
;
const
value
=
await
this
.
sampleFromRowColumns
(
row
,
profile
.
comboFields
[
0
].
columnHeaders
,
label
);
const
keyword
=
value
.
slice
(
0
,
Math
.
min
(
value
.
length
,
16
));
await
this
.
fillFilter
(
label
,
keyword
);
await
this
.
reset
();
await
expect
(
this
.
textInput
(
label
),
`重置后「
${
label
}
」应清空`
).
toHaveValue
(
''
);
return
;
}
if
(
profile
.
supplierInExpandedFilters
)
{
await
this
.
ensureFiltersExpanded
();
}
const
filterLabel
=
await
this
.
resolveVisibleSupplierFilterLabel
();
await
this
.
fillFilter
(
filterLabel
,
supplier
.
slice
(
0
,
Math
.
min
(
supplier
.
length
,
12
)));
await
this
.
reset
();
const
input
=
this
.
textInput
(
filterLabel
);
await
expect
(
input
,
'
重置后销售商(自定义)名称应清空
'
).
toHaveValue
(
''
);
}
detailLayerRoot
():
Locator
{
return
this
.
locators
.
putPage
()
.
or
(
this
.
locators
.
dialog
())
.
or
(
this
.
page
.
locator
(
'
.ant-drawer-open, .ant-modal:visible
'
));
}
}
async
openViewAndClose
():
Promise
<
void
>
{
private
async
readLabeledValueInRoot
(
root
:
Locator
,
label
:
string
):
Promise
<
string
>
{
const
escaped
=
label
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
pattern
=
new
RegExp
(
escaped
);
const
item
=
root
.
locator
(
'
.ant-form-item, .ant-descriptions-item, .ant-descriptions-row, tr
'
)
.
filter
({
hasText
:
pattern
})
.
first
();
if
(
!
(
await
item
.
isVisible
().
catch
(()
=>
false
)))
{
return
''
;
}
const
valueLoc
=
item
.
locator
(
'
.ant-form-item-control-input-content, .ant-descriptions-item-content, td
'
)
.
last
();
const
text
=
normalizeSampleCellValue
(((
await
valueLoc
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
());
if
(
text
&&
!
text
.
includes
(
label
.
replace
(
/
\s
/g
,
''
)))
{
return
text
;
}
const
full
=
normalizeSampleCellValue
(((
await
item
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
());
return
full
.
replace
(
new
RegExp
(
`^\\s*
${
escaped
}
\\s*[::]?\\s*`
),
''
).
trim
();
}
async
readSupplierCustomInDetail
():
Promise
<
string
>
{
const
layer
=
this
.
detailLayerRoot
().
first
();
await
expect
(
layer
,
'
查看详情层
'
).
toBeVisible
({
timeout
:
15
_000
});
for
(
const
label
of
SUPPLIER_DETAIL_LABELS
)
{
const
value
=
await
this
.
readLabeledValueInRoot
(
layer
,
label
);
if
(
value
)
{
return
value
;
}
}
return
''
;
}
/**
* 证件效期提醒:选首行 → 查看 → 读取详情销售商(自定义);关闭详情后返回列表值供断言。
*/
async
viewFirstRowAndReadSuppliers
(
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
,
):
Promise
<
{
listSupplier
:
string
;
detailSupplier
:
string
}
>
{
await
this
.
openOnMainTab
(
profile
.
tab
);
await
this
.
query
();
await
this
.
query
();
const
row
=
await
this
.
firstRow
();
const
row
=
await
this
.
selectFirstRow
();
const
view
=
row
.
getByRole
(
'
button
'
,
{
name
:
/查
\s
*看/
}).
or
(
row
.
locator
(
'
a,button,span
'
).
filter
({
hasText
:
/查
\s
*看/
})).
first
();
const
listSupplier
=
await
this
.
sampleSupplierFromRow
(
row
);
const
view
=
this
.
rowAction
(
row
,
'
查看
'
);
await
expect
(
view
,
'
行内查看
'
).
toBeVisible
();
await
expect
(
view
,
'
行内查看
'
).
toBeVisible
();
await
this
.
assertBeforeBusinessAction
(
'
查看
'
);
await
this
.
click
(
view
,
'
查看
'
);
await
this
.
click
(
view
,
'
查看
'
);
await
this
.
waitSettle
(
500
);
await
this
.
waitSpinGone
();
const
layer
=
let
detailSupplier
=
await
this
.
readSupplierCustomInDetail
();
this
.
locators
.
dialog
().
or
(
this
.
locators
.
putPage
()).
or
(
this
.
page
.
locator
(
'
.ant-drawer-open, .ant-modal
'
));
if
(
!
detailSupplier
)
{
await
expect
(
layer
.
first
(),
'
查看详情层
'
).
toBeVisible
({
timeout
:
10
_000
});
const
snippet
=
listSupplier
.
slice
(
0
,
Math
.
min
(
listSupplier
.
length
,
8
));
await
this
.
closeOverlay
();
await
expect
(
this
.
detailLayerRoot
().
first
(),
'
详情应展示列表销售商(自定义)
'
).
toContainText
(
snippet
);
detailSupplier
=
listSupplier
;
}
await
this
.
leaveWithoutCommit
();
await
this
.
expectOnPage
();
await
this
.
expectOnPage
();
return
{
listSupplier
,
detailSupplier
};
}
}
async
expectExportVisible
():
Promise
<
void
>
{
/** 查询并选中首行后触发导出(不校验文件内容)。 */
async
exportAfterQueryWithFirstRow
(
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
):
Promise
<
void
>
{
await
this
.
openOnMainTab
(
profile
.
tab
);
await
this
.
query
();
await
this
.
selectFirstRow
();
const
exportBtn
=
this
.
button
(
'
导出
'
).
or
(
this
.
button
(
'
导出明细
'
)).
first
();
const
exportBtn
=
this
.
button
(
'
导出
'
).
or
(
this
.
button
(
'
导出明细
'
)).
first
();
await
expect
(
exportBtn
,
'
导出按钮
'
).
toBeVisible
();
await
expect
(
exportBtn
,
'
导出按钮
'
).
toBeVisible
();
await
executeExportReport
({
page
:
this
.
page
,
prepareBusinessAction
:
()
=>
this
.
assertBeforeBusinessAction
(
'
导出
'
),
clickExport
:
()
=>
this
.
click
(
exportBtn
,
'
点击导出
'
),
assertHealthy
:
()
=>
this
.
assertPageHealthy
(
'
导出后
'
),
});
}
}
/** CSV VENDOR-QUL-011 */
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
async
filterSameDayExpiryOrMakeDate
():
Promise
<
void
>
{
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
expand
=
this
.
button
(
'
展开
'
).
first
();
const
candidates
:
Locator
[]
=
[];
if
(
await
expand
.
isVisible
().
catch
(()
=>
false
))
{
const
count
=
await
settings
.
count
();
await
this
.
click
(
expand
,
'
展开筛选
'
);
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
await
this
.
waitSettle
(
300
);
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
{
candidates
.
push
(
loc
);
}
}
}
if
(
await
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
/有效期限|有效期至|有效期/
}).
first
().
isVisible
().
catch
(()
=>
false
))
{
if
(
candidates
.
length
===
0
)
{
await
this
.
filterSameDayDate
(
'
有效期
'
);
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
return
;
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
const
body
=
this
.
tableWrapper
().
locator
(
'
.ant-table-body
'
).
first
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
200
);
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
}
}
if
(
await
this
.
page
.
locator
(
'
.ant-form-item
'
).
filter
({
hasText
:
'
制单日期
'
}).
first
().
isVisible
().
catch
(()
=>
false
))
{
if
(
!
trigger
)
{
await
this
.
filterSameDayDate
(
'
制单日期
'
);
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
if
(
await
isColumnSettingsTriggerUsable
(
loc
))
{
trigger
=
loc
;
break
;
}
}
}
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
return
;
}
}
await
this
.
skipWhenDataInsufficient
(
'
无制单/效期日期区间筛选
'
);
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
expectColumnSettingsPanel
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
await
expect
(
panel
).
toBeVisible
();
const
hasColumnUi
=
(
await
panel
.
getByText
(
'
列展示
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
panel
.
getByText
(
'
字段设置
'
).
first
().
isVisible
().
catch
(()
=>
false
))
||
(
await
panel
.
locator
(
'
.ant-checkbox-wrapper
'
).
first
().
isVisible
().
catch
(()
=>
false
));
expect
(
hasColumnUi
,
'
列设置面板应含字段设置或列勾选列表
'
).
toBeTruthy
();
await
expect
(
panel
.
getByText
(
/重
\s
*置/
).
first
()).
toBeVisible
();
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorData
.
qualColumnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
await
this
.
openColumnSettings
();
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
'
'
).
trim
();
const
name
=
raw
.
split
(
'
'
)[
0
]
??
raw
;
if
(
!
name
||
/^
(
序号|操作|查看|全选
)
$/
.
test
(
name
))
{
continue
;
}
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
!
(
await
checkbox
.
isChecked
().
catch
(()
=>
false
)))
{
continue
;
}
await
this
.
closeColumnSettingsPanel
();
return
name
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
return
vendorData
.
qualColumnSettingsToggleCandidates
[
0
];
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
const
isChecked
=
await
checkbox
.
isChecked
();
if
(
isChecked
!==
visible
)
{
await
row
.
click
();
}
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
{
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
}
else
{
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
{
await
expect
(
checkbox
).
toBeChecked
();
}
else
{
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
}
async
executeColumnSettingsToggleRestoreTest
():
Promise
<
void
>
{
await
executeColumnSettingsToggleRestore
({
pickToggleColumnName
:
()
=>
this
.
pickToggleColumnName
(),
openColumnSettings
:
()
=>
this
.
openColumnSettings
(),
setColumnVisibleInSettings
:
(
name
,
visible
)
=>
this
.
setColumnVisibleInSettings
(
name
,
visible
),
closeColumnSettingsPanel
:
()
=>
this
.
closeColumnSettingsPanel
(),
expectTableHeaderVisible
:
(
name
,
visible
)
=>
this
.
expectTableHeaderVisible
(
name
,
visible
),
expectSettingsCheckboxChecked
:
(
name
,
checked
)
=>
this
.
expectSettingsCheckboxChecked
(
name
,
checked
),
});
}
}
/** CSV VENDOR-QUL-012:效期提醒与有效期至抽样一致。 */
/** 当前主 Tab 内容区(含列表表格),避免误点其它 Tab 下仍留在 DOM 中的子页签。 */
async
expectReminderMatchesExpireDate
():
Promise
<
void
>
{
listSubTabScope
():
Locator
{
await
this
.
switchExpiringThreeMonths
();
return
this
.
page
if
((
await
this
.
getDataRowCount
())
===
0
)
{
.
locator
(
'
.ant-tabs-tabpane-active
'
)
await
this
.
skipWhenDataInsufficient
(
'
近三个月到期无数据可抽样
'
);
.
filter
({
has
:
this
.
page
.
locator
(
'
.ant-table-wrapper
'
)
})
.
first
();
}
expirySubTabLocator
(
pattern
:
RegExp
):
Locator
{
return
this
.
listSubTabScope
()
.
locator
(
'
.ant-tabs-tab, [role="tab"], .ant-radio-button-wrapper, .ant-segmented-item
'
)
.
filter
({
hasText
:
pattern
})
.
first
();
}
async
switchListSubTab
(
pattern
:
RegExp
,
stepName
:
string
):
Promise
<
void
>
{
const
tab
=
this
.
expirySubTabLocator
(
pattern
);
if
(
!
(
await
tab
.
isVisible
().
catch
(()
=>
false
)))
{
await
this
.
skipWhenDataInsufficient
(
`列表子 Tab「
${
stepName
}
」不可见`
);
return
;
return
;
}
}
const
row
=
await
this
.
firstRow
();
await
this
.
assertBeforeBusinessAction
(
`切换
${
stepName
}
`
);
const
expire
=
(
await
this
.
tryCell
(
row
,
'
有效期至
'
)).
replace
(
/
\s
/g
,
''
);
await
this
.
click
(
tab
,
`切换
${
stepName
}
`
);
const
remind
=
(
await
this
.
tryCell
(
row
,
'
效期提醒
'
)).
replace
(
/
\s
/g
,
''
);
await
this
.
waitForTableReady
();
expect
(
expire
.
length
>
0
||
remind
.
length
>
0
,
'
有效期至或效期提醒应有值
'
).
toBeTruthy
();
}
if
(
expire
&&
remind
)
{
// 提醒文案通常含「天」或日期片段;至少不应互相矛盾为空
/** @deprecated 使用 switchListSubTab */
expect
(
remind
.
length
,
'
效期提醒文案非空
'
).
toBeGreaterThan
(
0
);
async
switchExpirySubTab
(
pattern
:
RegExp
,
stepName
:
string
):
Promise
<
void
>
{
return
this
.
switchListSubTab
(
pattern
,
stepName
);
}
async
readListSubTabCount
(
pattern
:
RegExp
):
Promise
<
number
|
null
>
{
const
text
=
((
await
this
.
expirySubTabLocator
(
pattern
).
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
const
matched
=
text
.
match
(
/
\((\d
+
)\)
/
)
??
text
.
match
(
/
(\d
+
)
$/
)
??
text
.
match
(
/
[
::
](\d
+
)
/
);
return
matched
?
Number
(
matched
[
1
])
:
null
;
}
/** 当前主 Tab 下列表区子页签均可切换且列表可加载(配置见 vendor-qual-subtabs.data.ts)。 */
async
switchAllExpirySubTabs
(
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
):
Promise
<
void
>
{
const
{
subTabs
}
=
subTabsForMainTab
(
profile
.
tab
);
await
this
.
openOnMainTab
(
profile
.
tab
);
for
(
const
sub
of
subTabs
)
{
await
this
.
switchListSubTab
(
sub
.
pattern
,
sub
.
title
);
await
this
.
expectListOrEmpty
();
}
}
}
}
/** 组合查询:一次性填满筛选区全部字段后查询(字段以现网搜索条为准)。 */
/** 列表子 Tab 标签计数与分页「共 N 条」一致(可读且配置开启时断言)。 */
async
filterCombination
():
Promise
<
void
>
{
async
expectExpirySubTabCountsMatchList
(
const
scope
=
await
this
.
queryScope
();
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
,
// 实测该页仅「销售商名称」「证件类型」可见;授权书名称/授权单位/被授权单位在页面上是隐藏死字段,不纳入组合查询
):
Promise
<
void
>
{
await
this
.
applyFilterCombination
(
const
{
subTabs
,
matchCountWithPagination
}
=
subTabsForMainTab
(
profile
.
tab
);
[
if
(
!
matchCountWithPagination
)
{
{
label
:
'
销售商名称
'
,
kind
:
'
text
'
,
value
:
'
样本
'
},
return
;
{
label
:
'
证件类型
'
,
kind
:
'
select
'
,
value
:
'
样本
'
},
}
],
await
this
.
openOnMainTab
(
profile
.
tab
);
{
scope
},
for
(
const
sub
of
subTabs
)
{
);
await
this
.
switchListSubTab
(
sub
.
pattern
,
sub
.
title
);
const
count
=
await
this
.
readListSubTabCount
(
sub
.
pattern
);
if
(
count
===
null
)
{
continue
;
}
expect
(
await
this
.
getTotalCount
(),
`
${
sub
.
title
}
Tab 计数应等于分页总数`
).
toBe
(
count
);
}
}
}
}
}
pages/spd/vendor-query.page.ts
View file @
6c31a476
import
type
{
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
type
{
VendorQualPage
}
from
'
./vendor-qual.page
'
;
import
type
{
VendorQualPage
}
from
'
./vendor-qual.page
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
VendorSharedPage
}
from
'
./vendor-shared.page
'
;
import
{
ColumnSettingsControls
,
executeColumnSettingsToggleRestore
,
findUsableColumnSettingsTrigger
,
isColumnSettingsTriggerUsable
,
}
from
'
./helpers/column-settings.helper
'
;
/** 厂商查询:动作与取数;断言在 tests/spd/vendor-query.spec.ts。 */
/** 厂商查询:动作与取数;断言在 tests/spd/vendor-query.spec.ts。 */
export
class
VendorQueryPage
extends
VendorSharedPage
{
export
class
VendorQueryPage
extends
VendorSharedPage
{
private
readonly
columnSettingsControls
:
ColumnSettingsControls
;
constructor
(
page
:
Page
)
{
constructor
(
page
:
Page
)
{
super
(
page
,
{
super
(
page
,
{
path
:
vendorData
.
queryPath
,
path
:
vendorData
.
queryPath
,
...
@@ -13,6 +23,146 @@ export class VendorQueryPage extends VendorSharedPage {
...
@@ -13,6 +23,146 @@ export class VendorQueryPage extends VendorSharedPage {
menu
:
vendorData
.
menuQuery
,
menu
:
vendorData
.
menuQuery
,
forbidden
:
[
'
qcManage
'
,
'
authAudit
'
,
'
regCertAudit
'
,
'
evaluationPlan
'
,
'
holiday
'
],
forbidden
:
[
'
qcManage
'
,
'
authAudit
'
,
'
regCertAudit
'
,
'
evaluationPlan
'
,
'
holiday
'
],
});
});
this
.
columnSettingsControls
=
new
ColumnSettingsControls
({
columnSettingsPanel
:
()
=>
this
.
columnSettingsPanel
(),
resolveTrigger
:
()
=>
this
.
resolveColumnSettingsTrigger
(),
reportBody
:
page
.
locator
(
'
body
'
),
clickOutsideTarget
:
this
.
nameSearch
(),
tableClickTarget
:
this
.
tableWrapper
(),
});
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-popover:visible, .ant-dropdown:visible
'
)
.
filter
({
hasText
:
/字段设置|列展示/
})
.
first
();
}
columnSettingsCheckboxRow
(
label
:
string
):
Locator
{
return
this
.
columnSettingsPanel
()
.
locator
(
'
.ant-checkbox-wrapper, label
'
)
.
filter
({
hasText
:
label
})
.
first
();
}
columnHeaderCell
(
columnName
:
string
):
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
// column-height 为表格密度(分散/中等/紧凑),非列设置;列设置为 anticon-setting(列表工具栏)。
const
settings
=
this
.
page
.
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
);
const
candidates
:
Locator
[]
=
[];
const
count
=
await
settings
.
count
();
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
const
box
=
await
loc
.
boundingBox
().
catch
(()
=>
null
);
if
(
box
&&
box
.
y
>
120
)
{
candidates
.
push
(
loc
);
}
}
if
(
candidates
.
length
===
0
)
{
candidates
.
push
(
this
.
tableWrapper
().
locator
(
COLUMN_SETTINGS_TRIGGER_CSS
).
first
());
}
let
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
if
(
!
trigger
)
{
const
body
=
this
.
tableWrapper
().
locator
(
'
.ant-table-body
'
).
first
();
await
body
.
evaluate
((
el
)
=>
{
(
el
as
HTMLElement
).
scrollLeft
=
(
el
as
HTMLElement
).
scrollWidth
;
})
.
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
200
);
trigger
=
await
findUsableColumnSettingsTrigger
(
candidates
);
}
if
(
!
trigger
)
{
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
loc
=
settings
.
nth
(
i
);
if
(
await
isColumnSettingsTriggerUsable
(
loc
))
{
trigger
=
loc
;
break
;
}
}
}
if
(
!
trigger
)
{
await
this
.
skipWhenDataInsufficient
(
'
列设置齿轮不可见
'
);
}
return
trigger
!
;
}
async
openColumnSettings
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
if
(
await
panel
.
isVisible
().
catch
(()
=>
false
))
{
return
;
}
const
trigger
=
await
this
.
resolveColumnSettingsTrigger
();
await
this
.
click
(
trigger
,
'
打开列设置
'
);
await
expect
(
panel
,
'
列设置面板
'
).
toBeVisible
({
timeout
:
10
_000
});
}
async
expectColumnSettingsPanel
():
Promise
<
void
>
{
const
panel
=
this
.
columnSettingsPanel
();
await
expect
(
panel
).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
列展示
'
).
first
()).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
字段设置
'
).
first
()).
toBeVisible
();
await
expect
(
panel
.
getByText
(
'
重置
'
,
{
exact
:
true
})).
toBeVisible
();
}
async
closeColumnSettingsPanel
():
Promise
<
void
>
{
await
this
.
columnSettingsControls
.
closePanel
();
}
async
pickToggleColumnName
():
Promise
<
string
>
{
for
(
const
name
of
vendorData
.
columnSettingsToggleCandidates
)
{
if
(
await
this
.
columnHeaderCell
(
name
).
isVisible
().
catch
(()
=>
false
))
{
return
name
;
}
}
return
vendorData
.
columnSettingsToggleCandidates
[
0
];
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
const
isChecked
=
await
checkbox
.
isChecked
();
if
(
isChecked
!==
visible
)
{
await
row
.
click
();
}
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
if
(
visible
)
{
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
}
else
{
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
}
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
const
checkbox
=
this
.
columnSettingsCheckboxRow
(
label
).
locator
(
'
input[type="checkbox"]
'
).
first
();
if
(
checked
)
{
await
expect
(
checkbox
).
toBeChecked
();
}
else
{
await
expect
(
checkbox
).
not
.
toBeChecked
();
}
}
async
executeColumnSettingsToggleRestoreTest
():
Promise
<
void
>
{
await
executeColumnSettingsToggleRestore
({
pickToggleColumnName
:
()
=>
this
.
pickToggleColumnName
(),
openColumnSettings
:
()
=>
this
.
openColumnSettings
(),
setColumnVisibleInSettings
:
(
name
,
visible
)
=>
this
.
setColumnVisibleInSettings
(
name
,
visible
),
closeColumnSettingsPanel
:
()
=>
this
.
closeColumnSettingsPanel
(),
expectTableHeaderVisible
:
(
name
,
visible
)
=>
this
.
expectTableHeaderVisible
(
name
,
visible
),
expectSettingsCheckboxChecked
:
(
name
,
checked
)
=>
this
.
expectSettingsCheckboxChecked
(
name
,
checked
),
});
}
}
nameSearch
()
{
nameSearch
()
{
...
@@ -45,6 +195,26 @@ export class VendorQueryPage extends VendorSharedPage {
...
@@ -45,6 +195,26 @@ export class VendorQueryPage extends VendorSharedPage {
await
this
.
waitForTableReady
();
await
this
.
waitForTableReady
();
}
}
/**
* 经销商 Tab:点选首行,取厂商名称关键字并作为「厂商名称」查询条件搜索;返回关键字供用例断言。
* 现网点选行仅高亮选中,搜索框不自动回填,需显式写入后回车查询。
*/
async
searchByDistributorFirstRowSelection
():
Promise
<
string
>
{
await
this
.
switchDistributor
();
const
row
=
await
this
.
firstRow
();
const
rowName
=
normalizeSampleCellValue
(
await
this
.
tryCell
(
row
,
'
厂商名称
'
));
if
(
!
rowName
)
{
await
this
.
skipWhenDataInsufficient
(
'
经销商首行「厂商名称」为空
'
);
}
await
this
.
assertBeforeBusinessAction
(
'
选择经销商首行
'
);
await
this
.
click
(
row
,
'
选择经销商首行
'
);
await
this
.
waitSettle
(
300
);
await
expect
(
row
,
'
首行应处于选中态
'
).
toHaveClass
(
/basic-selected-row|ant-table-row-selected/
);
const
keyword
=
rowName
.
slice
(
0
,
Math
.
min
(
rowName
.
length
,
8
));
await
this
.
searchByName
(
keyword
);
return
keyword
;
}
async
searchByName
(
name
:
string
):
Promise
<
void
>
{
async
searchByName
(
name
:
string
):
Promise
<
void
>
{
await
this
.
assertBeforeBusinessAction
(
'
查询
'
);
await
this
.
assertBeforeBusinessAction
(
'
查询
'
);
await
this
.
fill
(
this
.
nameSearch
(),
name
,
'
填写厂商名称
'
);
await
this
.
fill
(
this
.
nameSearch
(),
name
,
'
填写厂商名称
'
);
...
...
Prev
1
2
3
4
5
6
7
8
9
10
…
18
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment