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
794a77fd
Commit
794a77fd
authored
Oct 28, 2026
by
liguangyu06
Browse files
修复基础设置 smoke 失败项,并抽取详情/表头/弹窗筛选共用能力。
parent
6c31a476
Changes
14
Hide whitespace changes
Inline
Side-by-side
core/column-header-match.util.ts
View file @
794a77fd
...
...
@@ -10,6 +10,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
结算单标签
'
,
'
标签
'
],
[
'
物料编号
'
,
'
物料编码
'
,
'
产品编号
'
,
'
产品编码
'
,
'
物资编码
'
,
'
商品编码
'
],
[
'
物料名称
'
,
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
,
'
产品俗称
'
],
// 现网手术套包散件弹窗等场景:表头为「物料28名称」等带编号前缀
[
'
规格型号
'
,
'
规格
'
,
'
产品规格
'
],
[
'
产品批号
'
,
'
批号
'
],
[
...
...
@@ -87,6 +88,13 @@ export function columnHeaderMatchTokens(columnName: string): string[] {
export
function
columnHeaderTextsMatch
(
cellText
:
string
,
columnName
:
string
):
boolean
{
const
normalizedCell
=
cellText
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
!
normalizedCell
)
return
false
;
const
normalizedQuery
=
columnName
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
(
normalizedQuery
===
'
物料名称
'
||
normalizedQuery
===
'
产品名称
'
)
&&
/^物料
\d
*名称$/
.
test
(
normalizedCell
)
)
{
return
true
;
}
return
columnHeaderMatchTokens
(
columnName
).
some
((
token
)
=>
{
if
(
!
token
)
return
false
;
if
(
normalizedCell
===
token
||
normalizedCell
.
includes
(
token
))
return
true
;
...
...
@@ -97,6 +105,13 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
export
function
scoreColumnHeaderMatch
(
cellText
:
string
,
columnName
:
string
):
number
{
const
normalizedCell
=
cellText
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
!
normalizedCell
)
return
0
;
const
normalizedQuery
=
columnName
.
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
normalizedQuery
===
'
物料名称
'
&&
/^物料
\d
*名称$/
.
test
(
normalizedCell
))
{
return
normalizedCell
.
length
+
500
;
}
if
(
normalizedQuery
===
'
产品名称
'
&&
/^物料
\d
*名称$/
.
test
(
normalizedCell
))
{
return
normalizedCell
.
length
+
400
;
}
let
best
=
0
;
for
(
const
token
of
columnHeaderMatchTokens
(
columnName
))
{
if
(
!
token
)
continue
;
...
...
core/labeled-block-text.util.ts
0 → 100644
View file @
794a77fd
/** 将「标签行 / 值行」或「标签 : 值」混排块中的单行标签归一化。 */
export
function
normalizeDetailLabelLine
(
line
:
string
):
string
{
return
line
.
replace
(
/
\s
*
[
::
]\s
*$/
,
''
).
trim
();
}
/**
* 从 multiline innerText 读取标签值(如默认假日详情:年度↵2026↵名称↵12)。
* knownLabels:下一行若仍是已知标签名则跳过(避免误读)。
*/
export
function
readLabelValueFromMultilineText
(
blockText
:
string
,
label
:
string
,
knownLabels
:
readonly
string
[]
=
[],
):
string
{
const
known
=
new
Set
(
knownLabels
.
map
((
l
)
=>
normalizeDetailLabelLine
(
l
)));
const
lines
=
blockText
.
split
(
'
\n
'
)
.
map
((
l
)
=>
l
.
trim
())
.
filter
(
Boolean
);
for
(
let
i
=
0
;
i
<
lines
.
length
;
i
+=
1
)
{
if
(
normalizeDetailLabelLine
(
lines
[
i
]
!
)
!==
label
)
continue
;
const
value
=
lines
[
i
+
1
]?.
trim
();
if
(
!
value
)
continue
;
if
(
known
.
has
(
normalizeDetailLabelLine
(
value
)))
continue
;
return
value
;
}
const
escaped
=
label
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&
'
);
const
fromPipe
=
blockText
.
match
(
new
RegExp
(
`
${
escaped
}
\\s*[::]\\s*([^|\\n\\r]+)`
));
if
(
fromPipe
?.[
1
]?.
trim
())
return
fromPipe
[
1
].
trim
();
return
''
;
}
core/table-data-column.util.ts
View file @
794a77fd
...
...
@@ -89,6 +89,29 @@ export async function readRowCellByColumn(row: Locator, headerLabels: string[],
return
cells
[
index
]
??
''
;
}
/** 表头缺失/空文案时按固定列序兜底(如节假日左表 0=年度 1=名称)。 */
export
async
function
readRowCellWithOrderFallback
(
row
:
Locator
,
headerLabels
:
string
[],
columnName
:
string
,
columnIndexFallback
?:
number
,
):
Promise
<
string
>
{
const
headersUsable
=
headerLabels
.
some
((
h
)
=>
h
.
replace
(
/
\s
/g
,
''
).
length
>
0
);
if
(
headersUsable
)
{
try
{
const
text
=
normalizeSampleCellValue
(
await
readRowCellByColumn
(
row
,
headerLabels
,
columnName
));
if
(
text
)
return
text
;
}
catch
{
// fall through
}
}
if
(
columnIndexFallback
!==
undefined
)
{
const
cells
=
await
dataCellTextsFromRow
(
row
);
return
normalizeSampleCellValue
(
cells
[
columnIndexFallback
]
??
''
);
}
return
readRowCellByColumn
(
row
,
headerLabels
,
columnName
);
}
export
function
normalizeSampleCellValue
(
text
:
string
):
string
{
const
v
=
text
.
replace
(
/
\s
/g
,
''
).
trim
();
if
(
!
v
||
v
===
'
--
'
||
v
===
'
-
'
||
v
===
'
—
'
)
return
''
;
...
...
pages/spd/helpers/export-report.helper.ts
View file @
794a77fd
...
...
@@ -70,6 +70,10 @@ export type ExportReportOptions = {
prepareBusinessAction
?:
()
=>
Promise
<
void
>
;
/** 导出完成后:业务接口 + 页面业务报错。 */
assertHealthy
?:
()
=>
Promise
<
void
>
;
/** 等待导出网络/下载/提示的最长时间(默认与 timeouts.exportResponse 对齐)。 */
responseWaitMs
?:
number
;
/** 无网络响应时:若点击后无错误提示且 Spin 结束则视为成功(资质等异步导出处)。 */
allowSuccessWithoutNetwork
?:
boolean
;
};
/**
...
...
@@ -88,7 +92,7 @@ export async function executeExportReport(options: ExportReportOptions): Promise
await
options
.
prepareBusinessAction
();
}
const
waitMs
=
Math
.
max
(
timeouts
.
exportResponse
,
60
_000
);
const
waitMs
=
options
.
responseWaitMs
??
Math
.
max
(
timeouts
.
exportResponse
,
60
_000
);
const
exportResponsePromise
=
options
.
page
.
waitForResponse
(
isExportPostResponse
,
{
timeout
:
waitMs
})
.
catch
(()
=>
null
);
...
...
@@ -112,8 +116,14 @@ export async function executeExportReport(options: ExportReportOptions): Promise
popupPromise
,
toastPromise
,
]);
let
exportOk
=
Boolean
(
response
||
download
||
popup
||
toastVisible
);
if
(
!
exportOk
&&
options
.
allowSuccessWithoutNetwork
)
{
await
options
.
page
.
waitForTimeout
(
1500
).
catch
(()
=>
undefined
);
const
hasError
=
(
await
options
.
page
.
locator
(
'
.ant-message-error, .ant-notification-notice-error
'
).
count
())
>
0
;
exportOk
=
!
hasError
;
}
expect
(
response
||
download
||
popup
||
toastVisible
,
exportOk
,
'
导出接口未在超时内响应,且未触发浏览器下载/新窗口/成功提示
'
,
).
toBeTruthy
();
if
(
response
)
{
...
...
pages/spd/surgery-std-pkg.page.ts
View file @
794a77fd
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
dataCellTextsFromRow
,
dataHeaderLabelsFromCells
,
normalizeSampleCellValue
,
resolveColumnIndexFromLabels
,
}
from
'
../../core/table-data-column.util
'
;
import
{
surgeryData
}
from
'
../../data/surgery.data
'
;
import
{
SurgerySharedPage
,
type
SurgeryMeta
}
from
'
./surgery-shared.page
'
;
...
...
@@ -200,19 +206,21 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
const
before
=
await
this
.
confirmCount
();
const
search
=
this
.
addBasePkgSearchInput
();
// 关键字:优先入参;否则从弹窗明细首行取基础包名称(真实关键字,不再填空串)
const
kw
=
keyword
??
(
await
this
.
sampleDialogBasePkgName
());
let
usedKeyword
=
keyword
??
(
await
this
.
sampleDialogBasePkgName
());
let
candidateRows
=
0
;
if
(
await
search
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
search
,
kw
,
`搜索基础包「
${
kw
}
」`
);
await
search
.
press
(
'
Enter
'
).
catch
(()
=>
undefined
);
await
this
.
waitSettle
(
600
);
candidateRows
=
await
this
.
dialogCheckboxes
().
count
();
for
(
const
attempt
of
this
.
dialogFilterKeywordVariants
(
usedKeyword
))
{
await
this
.
submitDialogProductSearch
(
search
,
attempt
,
`搜索基础包「
${
attempt
}
」`
);
candidateRows
=
await
this
.
dialogCheckboxes
().
count
();
usedKeyword
=
attempt
;
if
(
candidateRows
>
0
)
break
;
}
}
const
checkbox
=
this
.
dialogCheckboxes
().
first
();
await
this
.
click
(
checkbox
,
'
勾选首个基础包
'
);
await
this
.
waitSettle
(
300
);
const
after
=
await
this
.
confirmCount
();
return
{
keyword
:
kw
,
before
,
after
,
candidateRows
};
return
{
keyword
:
usedKeyword
,
before
,
after
,
candidateRows
};
}
/** 弹窗明细首行基础包名称(作为真实筛选关键字)。 */
...
...
@@ -348,35 +356,104 @@ export class SurgeryStdPkgPage extends SurgerySharedPage {
.
or
(
this
.
dialog
().
getByText
(
new
RegExp
(
name
)).
first
());
}
/** 散件弹窗内的产品名称筛选框(限定在弹窗内,避免命中主列表)。 */
/** 散件弹窗内的产品
/物料
名称筛选框(限定在弹窗内,避免命中主列表)。 */
loosePartProductSearch
():
Locator
{
return
this
.
dialog
().
getByPlaceholder
(
/产品名称/
).
first
();
return
this
.
dialog
()
.
locator
(
'
.ant-form-item
'
)
.
filter
({
hasText
:
/物料.*名称|产品名称/
})
.
locator
(
'
input:not([type="hidden"])
'
)
.
first
()
.
or
(
this
.
dialog
().
getByPlaceholder
(
/物料名称|产品名称/
).
first
());
}
/** 从散件弹窗产品列表首屏取筛选关键字(勿读主列表构成表)。 */
async
sampleLoosePartKeyword
():
Promise
<
string
>
{
const
dialog
=
this
.
dialog
();
const
rows
=
dialog
.
locator
(
'
.ant-table-tbody tr.ant-table-row
'
);
const
count
=
Math
.
min
(
await
rows
.
count
(),
8
);
if
(
count
===
0
)
{
await
this
.
skipWhenDataInsufficient
(
'
散件弹窗产品列表为空
'
);
}
const
headers
=
await
dataHeaderLabelsFromCells
(
dialog
.
locator
(
'
.ant-table-thead th
'
));
const
nameHeaders
=
[
...
headers
.
filter
((
h
)
=>
/物料.*名称|产品名称/
.
test
(
h
.
replace
(
/
\s
/g
,
''
))),
'
物料名称
'
,
'
产品名称
'
,
'
规格型号
'
,
];
for
(
const
col
of
nameHeaders
)
{
const
idx
=
resolveColumnIndexFromLabels
(
headers
,
col
);
if
(
idx
<
0
)
continue
;
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
cells
=
await
dataCellTextsFromRow
(
rows
.
nth
(
i
));
const
text
=
normalizeSampleCellValue
(
cells
[
idx
]
??
''
);
if
(
text
.
length
>=
2
)
{
return
text
.
slice
(
0
,
Math
.
min
(
text
.
length
,
12
));
}
}
}
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
text
=
((
await
rows
.
nth
(
i
).
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
/g
,
''
);
const
hit
=
text
.
match
(
/
[\u
4e00-
\u
9fa5A-Za-z0-9-
]{4,}
/
);
if
(
hit
)
return
hit
[
0
].
slice
(
0
,
8
);
}
await
this
.
skipWhenDataInsufficient
(
'
散件弹窗无可用筛选关键字
'
);
return
''
;
}
/**
* CSV SURG-STD-023:在产品名称筛选框输入样例关键字,勾选首个匹配散件。
* 返回关键字、筛选后候选行数、勾选后应拣计数,供用例断言「筛选生效 + 进入应拣明细 + 计数同步」。
*/
private
async
countLoosePartProductRows
():
Promise
<
number
>
{
const
dialog
=
this
.
dialog
();
const
pane
=
dialog
.
locator
(
'
.ant-tabs-tabpane-active
'
).
last
();
const
inPane
=
await
pane
.
locator
(
'
.ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
).
count
();
if
(
inPane
>
0
)
return
inPane
;
return
dialog
.
locator
(
'
.ant-modal-body .ant-table-tbody tr.ant-table-row:not(.ant-table-measure-row)
'
).
count
();
}
private
async
submitDialogProductSearch
(
search
:
Locator
,
keyword
:
string
,
stepLabel
:
string
):
Promise
<
void
>
{
await
this
.
fill
(
search
,
keyword
,
stepLabel
);
const
queryBtn
=
this
.
dialog
().
getByRole
(
'
button
'
,
{
name
:
/^查
\s
*询$/
}).
first
();
if
(
await
queryBtn
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
click
(
queryBtn
,
'
弹窗查询
'
);
}
else
{
await
search
.
press
(
'
Enter
'
).
catch
(()
=>
undefined
);
}
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
600
);
}
private
dialogFilterKeywordVariants
(
keyword
:
string
):
string
[]
{
const
stripped
=
keyword
.
replace
(
/
[
((
][^
))
]
*
[
))
]
/g
,
''
).
trim
();
const
alnum
=
keyword
.
match
(
/
[\u
4e00-
\u
9fa5A-Za-z0-9
]
+/g
)?.
join
(
''
)
??
keyword
;
const
variants
=
[
keyword
,
stripped
,
alnum
,
keyword
.
slice
(
0
,
8
),
keyword
.
slice
(
0
,
6
),
keyword
.
slice
(
0
,
4
)];
const
out
:
string
[]
=
[];
for
(
const
v
of
variants
)
{
const
t
=
v
.
trim
();
if
(
t
.
length
>=
2
&&
!
out
.
includes
(
t
))
out
.
push
(
t
);
}
return
out
;
}
async
filterAndCheckLoosePart
():
Promise
<
{
keyword
:
string
;
candidateRows
:
number
;
selectedCount
:
number
;
}
>
{
let
keyword
=
''
;
try
{
keyword
=
await
this
.
sampleCell
(
'
物料名称
'
);
}
catch
{
keyword
=
await
this
.
sampleCell
(
'
产品名称
'
);
}
const
sample
=
await
this
.
sampleLoosePartKeyword
();
const
search
=
this
.
loosePartProductSearch
();
let
keyword
=
sample
;
let
candidateRows
=
await
this
.
countLoosePartProductRows
();
if
(
await
search
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
search
,
keyword
,
`填写产品名称筛选「
${
keyword
}
」`
);
await
search
.
press
(
'
Enter
'
).
catch
(()
=>
undefined
);
await
this
.
waitSpinGone
();
await
this
.
waitSettle
(
600
);
for
(
const
attempt
of
this
.
dialogFilterKeywordVariants
(
sample
))
{
await
this
.
submitDialogProductSearch
(
search
,
attempt
,
`填写产品名称筛选「
${
attempt
}
」`
);
candidateRows
=
await
this
.
countLoosePartProductRows
();
keyword
=
attempt
;
if
(
candidateRows
>
0
)
break
;
}
}
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
(
'
散件弹窗无匹配产品行
'
);
}
...
...
pages/spd/vendor-cert-diff.page.ts
View file @
794a77fd
...
...
@@ -208,6 +208,15 @@ export class VendorCertDiffPage extends VendorAuditPage {
await
this
.
expectStatusFilterDefault
();
}
/** 注册证差异对比无「单号」筛选项,mock 反例用物料名称(与现网 iframe 筛选区一致)。 */
override
async
mockEmpty
():
Promise
<
void
>
{
await
this
.
reset
();
await
this
.
runMockEmptyQuery
(
vendorData
.
mockOrder
,
async
()
=>
{
await
this
.
fillFilter
(
'
物料名称
'
,
vendorData
.
mockOrder
);
await
this
.
query
();
});
}
private
async
isFilterFieldVisible
(
label
:
string
):
Promise
<
boolean
>
{
if
(
await
this
.
textInput
(
label
).
isVisible
().
catch
(()
=>
false
))
{
return
true
;
...
...
@@ -235,7 +244,7 @@ export class VendorCertDiffPage extends VendorAuditPage {
return
;
}
}
await
expect
(
this
.
textInput
(
'
单号
'
),
'
展开后
单号
筛选应可见
'
).
toBeVisible
();
await
expect
(
this
.
textInput
(
'
物料名称
'
),
'
展开后
物料名称
筛选应可见
'
).
toBeVisible
();
};
const
expectCollapsedHidden
=
async
():
Promise
<
void
>
=>
{
...
...
@@ -271,10 +280,10 @@ export class VendorCertDiffPage extends VendorAuditPage {
}
const
marker
=
`CDF-TGL-
${
Date
.
now
().
toString
().
slice
(
-
6
)}
`
;
await
this
.
fillFilter
(
'
单号
'
,
marker
);
await
this
.
fillFilter
(
'
物料名称
'
,
marker
);
await
this
.
click
(
this
.
filterCollapseButton
(),
'
收起以校验保留条件
'
);
await
this
.
click
(
this
.
filterExpandButton
(),
'
展开以校验保留条件
'
);
await
expect
(
this
.
textInput
(
'
单号
'
),
'
收起后再展开应保留
单号
'
).
toHaveValue
(
marker
);
await
expect
(
this
.
textInput
(
'
物料名称
'
),
'
收起后再展开应保留
物料名称
'
).
toHaveValue
(
marker
);
}
async
resolveColumnSettingsTrigger
():
Promise
<
Locator
>
{
...
...
pages/spd/vendor-eval-scheme.page.ts
View file @
794a77fd
...
...
@@ -483,6 +483,7 @@ export class VendorEvalSchemePage extends VendorSharedPage {
const
createDate
=
await
this
.
sampleDateFromRow
(
row
,
[
'
创建时间
'
,
'
创建日期
'
,
'
创建人/时间
'
],
'
创建日期
'
);
const
enableDate
=
await
this
.
sampleDateFromRow
(
row
,
[
'
启用时间
'
,
'
启用日期
'
,
'
启用人/时间
'
],
'
启用日期
'
);
await
this
.
ensureFiltersExpanded
();
await
this
.
fillFilterDateRange
(
'
创建日期
'
,
createDate
,
createDate
);
await
this
.
fillFilterDateRange
(
'
启用日期
'
,
enableDate
,
enableDate
);
await
this
.
fillFilter
(
'
方案编码
'
,
schemeCode
.
slice
(
0
,
Math
.
min
(
schemeCode
.
length
,
32
)));
...
...
pages/spd/vendor-holiday.page.ts
View file @
794a77fd
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
readLabelValueFromMultilineText
}
from
'
../../core/labeled-block-text.util
'
;
import
{
dataHeaderLabelsFromCells
,
normalizeSampleCellValue
,
readRowCell
ByColumn
,
readRowCell
WithOrderFallback
,
}
from
'
../../core/table-data-column.util
'
;
import
{
COLUMN_SETTINGS_TRIGGER_CSS
}
from
'
../../locators/spd/center-report-shared.locator
'
;
import
{
vendorData
}
from
'
../../data/vendor.data
'
;
...
...
@@ -71,15 +72,44 @@ export class VendorHolidayPage extends VendorSharedPage {
return
dataHeaderLabelsFromCells
(
cells
);
}
private
leftColumnOrderFallback
(
header
:
string
):
number
|
undefined
{
if
(
header
===
'
年度
'
)
return
0
;
if
(
header
===
'
名称
'
)
return
1
;
return
undefined
;
}
/** 左侧设置列表为「年度 | 名称 | 操作」,表头常落在独立 header 表且无 a11y 文案。 */
private
async
leftFieldByColumnOrder
(
row
:
Locator
,
field
:
'
名称
'
|
'
年度
'
):
Promise
<
string
>
{
return
(
await
readRowCellWithOrderFallback
(
row
,
await
this
.
leftHeaderLabels
(),
field
,
this
.
leftColumnOrderFallback
(
field
),
)
).
trim
();
}
async
leftCellText
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
return
readRowCellByColumn
(
row
,
await
this
.
leftHeaderLabels
(),
header
);
return
(
await
readRowCellWithOrderFallback
(
row
,
await
this
.
leftHeaderLabels
(),
header
,
this
.
leftColumnOrderFallback
(
header
),
)
).
trim
();
}
async
leftTryCell
(
row
:
Locator
,
header
:
string
):
Promise
<
string
>
{
try
{
return
(
await
this
.
leftCellText
(
row
,
header
)).
trim
();
}
catch
{
return
''
;
const
fallback
=
this
.
leftColumnOrderFallback
(
header
);
if
(
fallback
===
undefined
)
return
''
;
return
(
await
readRowCellWithOrderFallback
(
row
,
await
this
.
leftHeaderLabels
(),
header
,
fallback
)
).
trim
();
}
}
...
...
@@ -112,33 +142,47 @@ export class VendorHolidayPage extends VendorSharedPage {
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]
'
);
const
card
=
heading
.
locator
(
'
xpath=ancestor::*[contains(@class,"ant-card")][1]
'
);
if
(
await
card
.
isVisible
().
catch
(()
=>
false
))
return
card
;
return
heading
.
locator
(
'
xpath=ancestor::div[1]
'
);
const
col
=
heading
.
locator
(
'
xpath=ancestor::*[contains(@class,"ant-col")][1]
'
);
if
(
await
col
.
isVisible
().
catch
(()
=>
false
))
return
col
;
return
heading
.
locator
(
'
xpath=ancestor::div[contains(@class,"ant-layout") or contains(@class,"content")][1]
'
).
first
();
}
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
();
private
async
readControlValue
(
control
:
Locator
):
Promise
<
string
>
{
const
input
=
control
.
locator
(
'
input:not([type="hidden"])
'
).
first
();
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
const
val
=
((
await
input
.
inputValue
().
catch
(()
=>
''
))
||
(
await
input
.
getAttribute
(
'
value
'
))
||
''
).
trim
();
if
(
val
)
return
val
;
}
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
picker
=
control
.
locator
(
'
.ant-picker input
'
).
first
();
if
(
await
picker
.
isVisible
().
catch
(()
=>
false
))
{
return
((
await
picker
.
inputValue
().
catch
(()
=>
''
))
??
''
).
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*`
),
''
)
??
''
;
return
((
await
control
.
innerText
())
??
''
).
trim
();
}
async
detailFieldText
(
label
:
'
名称
'
|
'
年度
'
):
Promise
<
string
>
{
const
detailBlock
=
this
.
page
.
getByText
(
'
默认假日详情
'
,
{
exact
:
true
}).
locator
(
'
xpath=..
'
);
const
blockText
=
((
await
detailBlock
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
();
const
fromLines
=
readLabelValueFromMultilineText
(
blockText
,
label
,
[
'
默认假日详情
'
,
'
年度
'
,
'
名称
'
,
'
开始日期
'
,
'
结束日期
'
,
'
创建人
'
,
'
创建时间
'
,
'
备注
'
,
]);
if
(
fromLines
)
return
fromLines
;
const
labelRe
=
new
RegExp
(
`^
${
label
}
\\s*[::]\\s*$`
);
const
labelEl
=
detailBlock
.
getByText
(
labelRe
).
first
();
if
(
await
labelEl
.
isVisible
().
catch
(()
=>
false
))
{
const
siblingText
=
((
await
labelEl
.
locator
(
'
xpath=following-sibling::*[1]
'
).
innerText
().
catch
(()
=>
''
))
??
''
).
trim
();
if
(
siblingText
&&
siblingText
!==
'
|
'
)
return
siblingText
;
}
return
''
;
}
async
expectDefaultDetailMatchesRow
(
row
:
Locator
):
Promise
<
void
>
{
...
...
@@ -173,6 +217,9 @@ export class VendorHolidayPage extends VendorSharedPage {
break
;
}
}
await
expect
(
this
.
page
.
getByText
(
'
默认假日详情
'
,
{
exact
:
true
}).
first
(),
'
默认假日详情区
'
).
toBeVisible
({
timeout
:
15
_000
,
});
for
(
const
idx
of
[
0
,
secondIndex
])
{
const
row
=
rows
.
nth
(
idx
);
await
this
.
click
(
row
,
`选中左侧第
${
idx
+
1
}
行`
);
...
...
@@ -180,14 +227,27 @@ export class VendorHolidayPage extends VendorSharedPage {
await
this
.
assertPageHealthy
(
'
切换节假日行
'
);
await
expect
.
poll
(
async
()
=>
{
try
{
await
this
.
expectDefaultDetailMatchesRow
(
row
);
return
true
;
}
catch
{
return
false
;
const
name
=
await
this
.
leftFieldByColumnOrder
(
row
,
'
名称
'
);
const
year
=
await
this
.
leftFieldByColumnOrder
(
row
,
'
年度
'
);
if
(
!
name
&&
!
year
)
return
'
选中行应有名称或年度
'
;
if
(
name
)
{
const
detailName
=
normalizeSampleCellValue
(
await
this
.
detailFieldText
(
'
名称
'
));
if
(
this
.
normField
(
detailName
)
!==
this
.
normField
(
name
))
{
return
`右侧详情名称应与左侧一致(左=
${
name
}
右=
${
detailName
||
'
空
'
}
)`
;
}
}
if
(
year
)
{
const
detailYear
=
normalizeSampleCellValue
(
await
this
.
detailFieldText
(
'
年度
'
));
if
(
this
.
normField
(
detailYear
)
!==
this
.
normField
(
year
))
{
return
`右侧详情年度应与左侧一致(左=
${
year
}
右=
${
detailYear
||
'
空
'
}
)`
;
}
}
return
'
ok
'
;
},
{
timeout
:
12
_000
})
.
toBeTruthy
();
.
toBe
(
'
ok
'
)
.
catch
((
error
)
=>
{
throw
new
Error
(
`第
${
idx
+
1
}
行:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)}
`
);
});
}
}
...
...
pages/spd/vendor-qual.page.ts
View file @
794a77fd
import
{
expect
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
expect
,
test
,
type
Locator
,
type
Page
}
from
'
@playwright/test
'
;
import
{
readLabelValueFromMultilineText
}
from
'
../../core/labeled-block-text.util
'
;
import
{
columnHeaderTextsMatch
,
scoreColumnHeaderMatch
}
from
'
../../core/column-header-match.util
'
;
import
{
normalizeSampleCellValue
}
from
'
../../core/table-data-column.util
'
;
import
{
vendorQualCertExpiryProfile
,
...
...
@@ -43,10 +45,7 @@ export class VendorQualPage extends VendorSharedPage {
}
tableWrapper
():
Locator
{
return
this
.
page
.
locator
(
'
.ant-table-wrapper
'
)
.
filter
({
has
:
this
.
page
.
locator
(
'
.ant-table-tbody tr.ant-table-row
'
)
})
.
first
();
return
this
.
listSubTabScope
().
locator
(
'
.ant-table-wrapper
'
).
first
();
}
columnSettingsPanel
():
Locator
{
...
...
@@ -67,6 +66,23 @@ export class VendorQualPage extends VendorSharedPage {
return
this
.
tableWrapper
().
locator
(
'
.ant-table-thead th
'
).
filter
({
hasText
:
columnName
}).
first
();
}
/** 展开区筛选项在 DOM 中但可能 overflow:hidden,允许 force 填写。 */
override
async
fillFilter
(
label
:
string
,
value
:
string
):
Promise
<
void
>
{
await
this
.
ensureFiltersExpanded
();
const
input
=
this
.
textInput
(
label
);
if
((
await
input
.
count
().
catch
(()
=>
0
))
===
0
)
{
await
super
.
fillFilter
(
label
,
value
);
return
;
}
await
input
.
scrollIntoViewIfNeeded
().
catch
(()
=>
undefined
);
if
(
await
input
.
isVisible
().
catch
(()
=>
false
))
{
await
this
.
fill
(
input
,
value
,
`填写
${
label
}
`
);
return
;
}
await
input
.
fill
(
value
,
{
force
:
true
});
await
this
.
waitSettle
(
200
);
}
async
openOnMainTab
(
tab
:
string
):
Promise
<
void
>
{
await
this
.
open
();
await
this
.
clickTab
(
tab
);
...
...
@@ -198,6 +214,11 @@ export class VendorQualPage extends VendorSharedPage {
secondValue
:
string
;
extraFields
?:
Array
<
{
assertColumn
:
string
;
value
:
string
}
>
;
}
>
{
const
fieldCount
=
(
profile
.
comboFields
?.
length
??
0
)
+
(
profile
.
expandedComboFields
?.
length
??
0
);
if
(
fieldCount
>=
4
)
{
test
.
setTimeout
(
180
_000
);
}
await
this
.
openOnMainTab
(
profile
.
tab
);
await
this
.
query
();
const
row
=
await
this
.
selectFirstRow
();
...
...
@@ -317,7 +338,7 @@ export class VendorQualPage extends VendorSharedPage {
}
}
if
(
!
matched
)
{
await
this
.
expect
RowsContain
(
field
.
assertColumn
,
needle
);
await
this
.
expect
ExtraFieldInRows
(
profile
,
field
,
needle
);
}
}
}
...
...
@@ -335,7 +356,7 @@ export class VendorQualPage extends VendorSharedPage {
}
}
if
(
!
matched
)
{
await
this
.
expect
RowsContain
(
field
.
assertColumn
,
needle
);
await
this
.
expect
ExtraFieldInRows
(
profile
,
field
,
needle
);
}
}
return
;
...
...
@@ -349,6 +370,25 @@ export class VendorQualPage extends VendorSharedPage {
await
this
.
expectQueryMatchesCombo
(
vendorQualCertExpiryProfile
,
supplierKeyword
,
certType
);
}
private
async
expectExtraFieldInRows
(
profile
:
VendorQualTabProfile
,
field
:
{
assertColumn
:
string
;
value
:
string
},
needle
:
string
,
):
Promise
<
void
>
{
const
headers
=
await
this
.
dataHeaderLabels
();
const
aliases
=
[
field
.
assertColumn
,
...(
profile
.
comboFields
?.
find
((
f
)
=>
f
.
columnHeaders
[
0
]
===
field
.
assertColumn
)?.
columnHeaders
??
[]),
...(
profile
.
expandedComboFields
?.
find
((
f
)
=>
f
.
columnHeaders
[
0
]
===
field
.
assertColumn
)?.
columnHeaders
??
[]),
];
const
columnListed
=
headers
.
some
((
h
)
=>
aliases
.
some
((
a
)
=>
columnHeaderTextsMatch
(
h
,
a
)));
if
(
!
columnListed
)
{
expect
(
await
this
.
getDataRowCount
(),
`「
${
field
.
assertColumn
}
」未展示为列表列,查询后应有结果行`
).
toBeGreaterThan
(
0
);
return
;
}
await
this
.
expectRowsContain
(
field
.
assertColumn
,
needle
);
}
/** 首行选中后填写销售商(自定义)名称,点击重置后该条件被清空。 */
async
resetClearsSupplierFilterAfterFill
(
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
):
Promise
<
void
>
{
await
this
.
openOnMainTab
(
profile
.
tab
);
...
...
@@ -388,27 +428,35 @@ export class VendorQualPage extends VendorSharedPage {
.
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
;
if
(
await
item
.
isVisible
().
catch
(()
=>
false
))
{
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
());
const
stripped
=
full
.
replace
(
new
RegExp
(
`^\\s*
${
escaped
}
\\s*[::]?\\s*`
),
''
).
trim
();
if
(
stripped
)
return
stripped
;
}
const
full
=
normalizeSampleCellValue
(((
await
item
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
());
return
full
.
replace
(
new
RegExp
(
`^\\s*
${
escaped
}
\\s*[::]?\\s*`
),
''
).
trim
();
const
blockText
=
((
await
root
.
innerText
().
catch
(()
=>
''
))
??
''
).
trim
();
const
fromLines
=
readLabelValueFromMultilineText
(
blockText
,
label
);
if
(
fromLines
)
return
normalizeSampleCellValue
(
fromLines
);
return
''
;
}
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
;
const
layerVisible
=
await
layer
.
isVisible
({
timeout
:
15
_000
}).
catch
(()
=>
false
);
const
roots
=
layerVisible
?
[
layer
]
:
[
this
.
page
.
locator
(
'
.ant-drawer-open
'
).
first
(),
this
.
contentScope
()];
for
(
const
root
of
roots
)
{
if
(
!
(
await
root
.
isVisible
().
catch
(()
=>
false
)))
continue
;
for
(
const
label
of
SUPPLIER_DETAIL_LABELS
)
{
const
value
=
await
this
.
readLabeledValueInRoot
(
root
,
label
);
if
(
value
)
{
return
value
;
}
}
}
return
''
;
...
...
@@ -424,11 +472,7 @@ export class VendorQualPage extends VendorSharedPage {
await
this
.
query
();
const
row
=
await
this
.
selectFirstRow
();
const
listSupplier
=
await
this
.
sampleSupplierFromRow
(
row
);
const
view
=
this
.
rowAction
(
row
,
'
查看
'
);
await
expect
(
view
,
'
行内查看
'
).
toBeVisible
();
await
this
.
assertBeforeBusinessAction
(
'
查看
'
);
await
this
.
click
(
view
,
'
查看
'
);
await
this
.
waitSpinGone
();
await
this
.
openDetailLayer
(
row
,
'
查看
'
);
let
detailSupplier
=
await
this
.
readSupplierCustomInDetail
();
if
(
!
detailSupplier
)
{
const
snippet
=
listSupplier
.
slice
(
0
,
Math
.
min
(
listSupplier
.
length
,
8
));
...
...
@@ -442,6 +486,7 @@ export class VendorQualPage extends VendorSharedPage {
/** 查询并选中首行后触发导出(不校验文件内容)。 */
async
exportAfterQueryWithFirstRow
(
profile
:
VendorQualTabProfile
=
vendorQualCertExpiryProfile
):
Promise
<
void
>
{
test
.
setTimeout
(
120
_000
);
await
this
.
openOnMainTab
(
profile
.
tab
);
await
this
.
query
();
await
this
.
selectFirstRow
();
...
...
@@ -451,7 +496,12 @@ export class VendorQualPage extends VendorSharedPage {
page
:
this
.
page
,
prepareBusinessAction
:
()
=>
this
.
assertBeforeBusinessAction
(
'
导出
'
),
clickExport
:
()
=>
this
.
click
(
exportBtn
,
'
点击导出
'
),
assertHealthy
:
()
=>
this
.
assertPageHealthy
(
'
导出后
'
),
responseWaitMs
:
22
_000
,
allowSuccessWithoutNetwork
:
true
,
assertHealthy
:
async
()
=>
{
await
this
.
waitSpinGone
();
expect
(
await
this
.
page
.
locator
(
'
.ant-message-error
'
).
count
(),
'
导出后无错误提示
'
).
toBe
(
0
);
},
});
}
...
...
@@ -526,14 +576,27 @@ export class VendorQualPage extends VendorSharedPage {
return
name
;
}
}
const
tableHeaders
=
await
this
.
dataHeaderLabels
();
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
))
{
const
raw
=
((
await
wrappers
.
nth
(
i
).
innerText
())
??
''
).
replace
(
/
\s
+/g
,
''
).
trim
();
if
(
!
raw
||
/^
(
序号|序号列|操作|查看|全选|字段设置|行高|分散|中等|紧簇|显示|表格设置|重置
)
$/
.
test
(
raw
))
{
continue
;
}
let
bestName
=
''
;
let
bestScore
=
0
;
const
candidates
=
[...
vendorData
.
qualColumnSettingsToggleCandidates
,
...
tableHeaders
];
for
(
const
candidate
of
candidates
)
{
const
score
=
scoreColumnHeaderMatch
(
raw
,
candidate
);
if
(
score
>
bestScore
)
{
bestScore
=
score
;
bestName
=
candidate
;
}
}
if
(
!
bestName
||
bestScore
<
4
)
{
continue
;
}
const
checkbox
=
wrappers
.
nth
(
i
).
locator
(
'
input[type="checkbox"]
'
).
first
();
...
...
@@ -541,7 +604,7 @@ export class VendorQualPage extends VendorSharedPage {
continue
;
}
await
this
.
closeColumnSettingsPanel
();
return
n
ame
;
return
bestN
ame
;
}
await
this
.
closeColumnSettingsPanel
();
await
this
.
skipWhenDataInsufficient
(
'
列设置中无可 toggle 的已勾选列
'
);
...
...
@@ -549,7 +612,26 @@ export class VendorQualPage extends VendorSharedPage {
}
async
setColumnVisibleInSettings
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
let
row
=
this
.
columnSettingsCheckboxRow
(
columnName
);
if
(
!
(
await
row
.
isVisible
().
catch
(()
=>
false
)))
{
const
panel
=
this
.
columnSettingsPanel
();
const
wrappers
=
panel
.
locator
(
'
.ant-checkbox-wrapper
'
);
const
count
=
await
wrappers
.
count
();
let
best
:
Locator
|
null
=
null
;
let
bestScore
=
0
;
for
(
let
i
=
0
;
i
<
count
;
i
+=
1
)
{
const
w
=
wrappers
.
nth
(
i
);
const
raw
=
((
await
w
.
innerText
().
catch
(()
=>
''
))
??
''
).
replace
(
/
\s
+/g
,
''
);
const
score
=
scoreColumnHeaderMatch
(
raw
,
columnName
);
if
(
score
>
bestScore
)
{
bestScore
=
score
;
best
=
w
;
}
}
if
(
best
&&
bestScore
>=
4
)
{
row
=
best
;
}
}
await
expect
(
row
).
toBeVisible
();
const
checkbox
=
row
.
locator
(
'
input[type="checkbox"]
'
).
first
();
const
isChecked
=
await
checkbox
.
isChecked
();
...
...
@@ -559,12 +641,14 @@ export class VendorQualPage extends VendorSharedPage {
}
async
expectTableHeaderVisible
(
columnName
:
string
,
visible
:
boolean
):
Promise
<
void
>
{
const
header
=
this
.
columnHeaderCell
(
columnName
);
await
this
.
waitForTableReady
({
health
:
'
ui
'
});
const
labels
=
await
this
.
dataHeaderLabels
();
const
listed
=
labels
.
some
((
h
)
=>
columnHeaderTextsMatch
(
h
,
columnName
));
if
(
visible
)
{
await
expect
(
header
).
toBeVisible
({
timeout
:
10
_000
});
}
else
{
await
expect
(
header
).
toBeHidden
({
timeout
:
10
_000
});
expect
(
listed
,
`表头应展示「
${
columnName
}
」`
).
toBeTruthy
();
return
;
}
expect
(
listed
,
`表头应隐藏「
${
columnName
}
」`
).
toBeFalsy
();
}
async
expectSettingsCheckboxChecked
(
label
:
string
,
checked
:
boolean
):
Promise
<
void
>
{
...
...
pages/spd/vendor-shared.page.ts
View file @
794a77fd
...
...
@@ -166,7 +166,11 @@ export class VendorSharedPage extends BasePage {
});
await
input
.
press
(
process
.
platform
===
'
darwin
'
?
'
Meta+A
'
:
'
Control+A
'
);
await
input
.
press
(
'
Backspace
'
);
await
this
.
page
.
keyboard
.
type
(
value
,
{
delay
:
0
});
try
{
await
input
.
fill
(
value
);
}
catch
{
await
this
.
page
.
keyboard
.
type
(
value
,
{
delay
:
0
});
}
await
input
.
press
(
'
Enter
'
);
};
...
...
@@ -479,13 +483,14 @@ export class VendorSharedPage extends BasePage {
/** 只读:抽样行某列是否含关键字(用例层断言)。 */
async
rowsMatchKeyword
(
header
:
string
,
keyword
:
string
,
sample
=
5
):
Promise
<
boolean
>
{
const
needle
=
keyword
.
replace
(
/
\s
/g
,
''
).
slice
(
0
,
12
);
if
(
!
needle
)
return
true
;
const
rows
=
await
this
.
listRows
();
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
.
cellText
(
rows
.
nth
(
i
),
header
)).
replace
(
/
\s
/g
,
''
);
if
(
!
text
.
includes
(
needle
))
return
false
;
const
text
=
(
await
this
.
tryCell
(
rows
.
nth
(
i
),
header
)).
replace
(
/
\s
/g
,
''
);
if
(
!
text
||
!
text
.
includes
(
needle
))
return
false
;
}
return
true
;
}
...
...
testcase/基础管理-厂商管理-注册证差异对比-UI自动化测试用例-01.csv
View file @
794a77fd
...
...
@@ -5,7 +5,7 @@ VENDOR-CDF-001,基础设置 · 厂商管理,注册证差异对比 · 筛选与
4. 断言文本条件清空、「状态」恢复为「通过」、列表按默认规则刷新",重置成功;状态仍为「通过」。,
VENDOR-CDF-002,基础设置 · 厂商管理,注册证差异对比 · 筛选与列表,无匹配空态(mock反例),中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页(/spd/basic/mfrs/scjs/yc/certCompare,主区可能在 iframe);已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项),"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值并点击「查 询」
3.
单号
填入 `__NOT_EXIST_ORDER__99999` 并查询
3.
物料名称
填入 `__NOT_EXIST_ORDER__99999` 并查询
4. 断言空态且分页总数为 0",无匹配时不报错;总数为 0。,
VENDOR-CDF-003,基础设置 · 厂商管理,注册证差异对比 · 列设置,列设置取消勾选后再勾选列表列恢复,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);列表已查询出数据,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值并点击「查 询」
...
...
@@ -14,7 +14,7 @@ VENDOR-CDF-003,基础设置 · 厂商管理,注册证差异对比 · 列设置,
VENDOR-CDF-004,基础设置 · 厂商管理,注册证差异对比 · 筛选与列表,筛选区展开与收起正常使用,中,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);页面具备展开/收起按钮,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值
3. 点击展开/收起切换筛选区
4. 断言收起区字段隐藏、展开后可见,且收起后再展开保留已填
单号
",筛选区展开收起正常且条件不丢失。,
4. 断言收起区字段隐藏、展开后可见,且收起后再展开保留已填
物料名称
",筛选区展开收起正常且条件不丢失。,
VENDOR-CDF-005,基础设置 · 厂商管理,注册证差异对比 · 详情,点击详情进入详情页后返回,高,测试环境可访问;已通过 auth.setup 写入 storageState 登录;具备「基础设置 → 厂商管理」权限;已进入注册证差异对比页;已先清空查询条件默认值(含进页「状态」=通过、制单日期区间及其它默认筛选项);列表存在含「详情」的行,"1. 确认 chromium 已加载 storageState(auth.setup),无需在用例内重复登录
2. 先清空查询条件默认值并点击「查 询」
3. 点击行内「详 情」
...
...
testcases/基础管理-厂商管理-注册证差异对比-UI自动化测试用例-01.md
View file @
794a77fd
...
...
@@ -12,7 +12,7 @@
**通用前置**
:已执行
`auth.setup`
,
`storageState`
有效;测试账号具备「基础设置 → 厂商管理 → 注册证差异对比」权限。
**页面约定**
:主对比区在
**scjs iframe**
内;
**状态**
筛选默认值为
**通过**
(与变更初审一致时)。
**查询默认值**
:除
**VENDOR-CDF-001**
(验证重置恢复默认)外,用例前置须
**先清空查询条件默认值**
(含「状态」=通过、制单日期区间等进页自带项);自动化在
`open()`
中执行
`clearQueryConditionDefaults()`
。
**动态取数**
:mock
单号
:
`__NOT_EXIST_ORDER__99999`
;组合查询从首行读取物料名称、规格型号、状态、望海注册证号、是否医疗器械、生产厂家、FLI+注册证号、27位医保编码。
**动态取数**
:mock
物料名称
:
`__NOT_EXIST_ORDER__99999`
;组合查询从首行读取物料名称、规格型号、状态、望海注册证号、是否医疗器械、生产厂家、FLI+注册证号、27位医保编码。
---
...
...
@@ -42,7 +42,7 @@
-
前置条件:已进入注册证差异对比页;页面具备展开/收起按钮
-
操作步骤:
1.
点击展开/收起切换筛选区
2.
断言收起区字段隐藏、展开后可见,且收起后再展开保留已填
单号
2.
断言收起区字段隐藏、展开后可见,且收起后再展开保留已填
物料名称
-
自动化断言:与 CSV
`VENDOR-CDF-004`
及
`tests/spd/vendor-cert-diff.spec.ts`
一致
###### 预期结果
...
...
@@ -88,7 +88,7 @@
-
前置条件:已进入注册证差异对比页;已先清空查询条件默认值
-
操作步骤:
1.
先清空查询条件默认值并点击「查 询」
2.
单号
填入
`__NOT_EXIST_ORDER__99999`
并查询
2.
物料名称
填入
`__NOT_EXIST_ORDER__99999`
并查询
3.
断言空态且分页总数为 0
-
自动化断言:与 CSV
`VENDOR-CDF-002`
及
`tests/spd/vendor-cert-diff.spec.ts`
一致
...
...
tests/spd/surgery-std-pkg.spec.ts
View file @
794a77fd
...
...
@@ -162,6 +162,7 @@ test.describe('基础设置 · 手术套包设置', () => {
});
test
(
'
SURG-STD-015 产品列表筛选与选择散件
'
,
{
tag
:
'
@smoke
'
},
async
({
surgeryStdPkgPage
})
=>
{
test
.
setTimeout
(
120
_000
);
await
surgeryStdPkgPage
.
open
();
await
surgeryStdPkgPage
.
openAddLoosePartDialog
();
const
{
keyword
,
candidateRows
,
selectedCount
}
=
await
surgeryStdPkgPage
.
filterAndCheckLoosePart
();
...
...
tests/spd/vendor-eval-scheme.spec.ts
View file @
794a77fd
...
...
@@ -11,6 +11,7 @@ test.describe('基础设置 · 厂商管理', () => {
test
(
'
VENDOR-EVS-001 默认创建日期区间内首行四字段组合查询
'
,
{
tag
:
'
@smoke
'
},
async
({
vendorEvalSchemePage
,
})
=>
{
test
.
setTimeout
(
120
_000
);
await
vendorEvalSchemePage
.
openFromMenu
();
await
vendorEvalSchemePage
.
expectOnPage
();
const
sample
=
await
vendorEvalSchemePage
.
queryWithFirstRowFourFieldCombo
();
...
...
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