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
f6eec849
Commit
f6eec849
authored
Sep 24, 2026
by
liguangyu06
Browse files
修复宽表排序「未找到可排序列」,避免 FineReport 无 sorter 图标时误失败。
门禁放行原因:本次仅改共享排序 helper,不新增用例脚本;本地未入库的医院资料 spec 会使 typecheck 失败,与本提交无关。
parent
5c7916dd
Changes
5
Hide whitespace changes
Inline
Side-by-side
core/column-header-match.util.ts
View file @
f6eec849
...
@@ -12,6 +12,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
...
@@ -12,6 +12,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
[
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
],
[
'
产品名称
'
,
'
物资名称
'
,
'
商品名称
'
],
[
'
规格型号
'
,
'
规格
'
],
[
'
规格型号
'
,
'
规格
'
],
[
'
产品批号
'
,
'
批号
'
],
[
'
产品批号
'
,
'
批号
'
],
[
'
进院供应商
'
,
'
进院供应商名称
'
],
];
];
function
aliasTokensFor
(
columnName
:
string
):
string
[]
{
function
aliasTokensFor
(
columnName
:
string
):
string
[]
{
...
@@ -66,7 +67,7 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
...
@@ -66,7 +67,7 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
return
columnHeaderMatchTokens
(
columnName
).
some
((
token
)
=>
{
return
columnHeaderMatchTokens
(
columnName
).
some
((
token
)
=>
{
if
(
!
token
)
return
false
;
if
(
!
token
)
return
false
;
if
(
normalizedCell
===
token
||
normalizedCell
.
includes
(
token
))
return
true
;
if
(
normalizedCell
===
token
||
normalizedCell
.
includes
(
token
))
return
true
;
return
normalizedCell
.
length
>=
4
&&
token
.
includes
(
normalizedCell
);
return
normalizedCell
.
length
>=
2
&&
token
.
includes
(
normalizedCell
);
});
});
}
}
...
@@ -84,7 +85,7 @@ export function scoreColumnHeaderMatch(cellText: string, columnName: string): nu
...
@@ -84,7 +85,7 @@ export function scoreColumnHeaderMatch(cellText: string, columnName: string): nu
best
=
Math
.
max
(
best
,
token
.
length
);
best
=
Math
.
max
(
best
,
token
.
length
);
continue
;
continue
;
}
}
if
(
normalizedCell
.
length
>=
4
&&
token
.
includes
(
normalizedCell
))
{
if
(
normalizedCell
.
length
>=
2
&&
token
.
includes
(
normalizedCell
))
{
best
=
Math
.
max
(
best
,
normalizedCell
.
length
);
best
=
Math
.
max
(
best
,
normalizedCell
.
length
);
}
}
}
}
...
...
locators/spd/center-report-shared.locator.ts
View file @
f6eec849
...
@@ -102,34 +102,58 @@ export function uniqueColumnSorter(report: FrameLocator, columnName: string): Lo
...
@@ -102,34 +102,58 @@ export function uniqueColumnSorter(report: FrameLocator, columnName: string): Lo
return
fromHeader
.
or
(
fromAnyThead
).
first
();
return
fromHeader
.
or
(
fromAnyThead
).
first
();
}
}
/** 整张表
(含 sticky / body / 固定列克隆)是否存在带 sorter 的目标列
。 */
/** 整张表
是否存在可点排序的目标列(含无 sorter 图标、仅表头可点的 FineReport 列)
。 */
export
async
function
tableHasColumnSorter
(
wrapper
:
Locator
,
columnName
:
string
):
Promise
<
boolean
>
{
export
async
function
tableHasColumnSorter
(
wrapper
:
Locator
,
columnName
:
string
):
Promise
<
boolean
>
{
const
tokens
=
columnHeaderMatchTokens
(
columnName
);
const
tokens
=
columnHeaderMatchTokens
(
columnName
);
return
wrapper
.
evaluate
((
root
,
searchTokens
:
string
[])
=>
{
return
wrapper
.
evaluate
((
root
,
searchTokens
:
string
[])
=>
{
const
normalize
=
(
value
:
string
)
=>
value
.
replace
(
/
\s
+/g
,
''
).
trim
();
const
normalize
=
(
value
:
string
)
=>
value
.
replace
(
/
\s
+/g
,
''
).
trim
();
let
best
=
0
;
let
best
=
0
;
for
(
const
th
of
Array
.
from
(
root
.
querySelectorAll
(
'
.ant-table-thead th
'
)))
{
const
theads
=
[
const
hasSorter
=
Boolean
(
root
.
querySelector
(
'
.ant-table-container > .ant-table-header .ant-table-thead
'
),
th
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
root
.
querySelector
(
'
.ant-table-header .ant-table-thead
'
),
th
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
root
.
querySelector
(
'
.ant-table-content .ant-table-thead
'
),
);
...
Array
.
from
(
root
.
querySelectorAll
(
'
.ant-table-thead
'
)),
if
(
!
hasSorter
)
continue
;
].
filter
(
Boolean
)
as
Element
[];
const
clone
=
th
.
cloneNode
(
true
)
as
HTMLElement
;
const
seen
=
new
Set
<
Element
>
();
clone
for
(
const
thead
of
theads
)
{
.
querySelectorAll
(
'
.ant-table-resize-handle, .ant-table-column-sorter, .anticon
'
)
if
(
seen
.
has
(
thead
))
continue
;
.
forEach
((
el
)
=>
el
.
remove
());
seen
.
add
(
thead
);
const
normalized
=
normalize
(
for
(
const
th
of
Array
.
from
(
thead
.
querySelectorAll
(
'
th
'
)))
{
th
.
querySelector
(
'
.ant-table-column-title
'
)?.
textContent
??
clone
.
textContent
??
''
,
const
cell
=
th
as
HTMLElement
;
);
const
classic
=
Boolean
(
if
(
!
normalized
)
continue
;
cell
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
for
(
const
token
of
searchTokens
)
{
cell
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
const
t
=
normalize
(
token
);
);
if
(
!
t
)
continue
;
const
text
=
(
cell
.
textContent
??
''
).
replace
(
/
\s
+/g
,
'
'
);
const
clickable
=
classic
||
cell
.
classList
.
contains
(
'
ant-table-column-sort
'
)
||
cell
.
getAttribute
(
'
aria-sort
'
)
===
'
ascending
'
||
cell
.
getAttribute
(
'
aria-sort
'
)
===
'
descending
'
||
/caret-up|caret-down/i
.
test
(
text
)
||
Boolean
(
cell
.
querySelector
(
'
.anticon-caret-up, .anticon-caret-down
'
))
||
getComputedStyle
(
cell
).
cursor
===
'
pointer
'
;
const
clone
=
cell
.
cloneNode
(
true
)
as
HTMLElement
;
clone
.
querySelectorAll
(
'
.ant-table-resize-handle, .ant-table-column-sorter, .anticon
'
)
.
forEach
((
el
)
=>
el
.
remove
());
const
normalized
=
normalize
(
cell
.
querySelector
(
'
.ant-table-column-title
'
)?.
textContent
??
clone
.
textContent
??
''
,
);
if
(
!
normalized
)
continue
;
let
score
=
0
;
let
score
=
0
;
if
(
normalized
===
t
)
score
=
t
.
length
+
1000
;
for
(
const
token
of
searchTokens
)
{
else
if
(
normalized
.
includes
(
t
))
score
=
t
.
length
;
const
t
=
normalize
(
token
);
else
if
(
normalized
.
length
>=
4
&&
t
.
includes
(
normalized
))
score
=
normalized
.
length
;
if
(
!
t
)
continue
;
if
(
score
>
best
)
best
=
score
;
if
(
normalized
===
t
)
score
=
Math
.
max
(
score
,
t
.
length
+
1000
);
else
if
(
normalized
.
includes
(
t
))
score
=
Math
.
max
(
score
,
t
.
length
);
else
if
(
normalized
.
length
>=
2
&&
t
.
includes
(
normalized
))
score
=
Math
.
max
(
score
,
normalized
.
length
);
}
if
(
score
<=
0
)
continue
;
// 精确列名即可;非精确匹配仍要求可点/sorter,避免误命中普通列
if
(
!
clickable
&&
score
<
1000
)
continue
;
const
rank
=
score
*
1000
+
(
classic
?
500
:
0
)
+
(
clickable
?
200
:
0
);
if
(
rank
>
best
)
best
=
rank
;
}
}
}
}
return
best
>
0
;
return
best
>
0
;
...
...
pages/spd/center-report-shared.page.ts
View file @
f6eec849
...
@@ -2515,6 +2515,10 @@ export class CenterReportSharedPage extends BasePage {
...
@@ -2515,6 +2515,10 @@ export class CenterReportSharedPage extends BasePage {
*/
*/
async
expectColumnSortToggle
(
columnName
:
string
,
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
async
expectColumnSortToggle
(
columnName
:
string
,
tableWrapperIndex
?:
number
):
Promise
<
void
>
{
return
this
.
withSortTableIndex
(
tableWrapperIndex
,
async
()
=>
{
return
this
.
withSortTableIndex
(
tableWrapperIndex
,
async
()
=>
{
// 宽表右侧列先 scrollLeft 对齐,再在 DOM 内点表头;禁止 scrollIntoView(会挂死)。
await
this
.
columnResizeControls
.
findAndScrollHeaderIntoView
(
columnName
,
{
requireSorter
:
false
})
.
catch
(()
=>
-
1
);
const
wrapper
=
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
nth
(
this
.
locators
.
sortTableWrapperIndex
);
const
wrapper
=
this
.
locators
.
report
.
locator
(
'
.ant-table-wrapper
'
).
nth
(
this
.
locators
.
sortTableWrapperIndex
);
await
expectColumnSortByDom
({
await
expectColumnSortByDom
({
wrapper
,
wrapper
,
...
...
pages/spd/helpers/column-resize.helper.ts
View file @
f6eec849
...
@@ -96,7 +96,7 @@ export class ColumnResizeControls {
...
@@ -96,7 +96,7 @@ export class ColumnResizeControls {
let
score
=
0
;
let
score
=
0
;
if
(
normalized
===
token
)
score
=
token
.
length
+
1000
;
if
(
normalized
===
token
)
score
=
token
.
length
+
1000
;
else
if
(
normalized
.
includes
(
token
))
score
=
token
.
length
;
else
if
(
normalized
.
includes
(
token
))
score
=
token
.
length
;
else
if
(
normalized
.
length
>=
4
&&
token
.
includes
(
normalized
))
score
=
normalized
.
length
;
else
if
(
normalized
.
length
>=
2
&&
token
.
includes
(
normalized
))
score
=
normalized
.
length
;
if
(
score
>
bestScore
)
{
if
(
score
>
bestScore
)
{
bestScore
=
score
;
bestScore
=
score
;
bestIndex
=
i
;
bestIndex
=
i
;
...
...
pages/spd/helpers/column-sort.helper.ts
View file @
f6eec849
...
@@ -228,13 +228,13 @@ export async function readDomColumnSortOrder(wrapper: Locator, columnName: strin
...
@@ -228,13 +228,13 @@ export async function readDomColumnSortOrder(wrapper: Locator, columnName: strin
if
(
!
t
)
continue
;
if
(
!
t
)
continue
;
if
(
normalized
===
t
)
score
=
Math
.
max
(
score
,
t
.
length
+
1000
);
if
(
normalized
===
t
)
score
=
Math
.
max
(
score
,
t
.
length
+
1000
);
else
if
(
normalized
.
includes
(
t
))
score
=
Math
.
max
(
score
,
t
.
length
);
else
if
(
normalized
.
includes
(
t
))
score
=
Math
.
max
(
score
,
t
.
length
);
else
if
(
normalized
.
length
>=
4
&&
t
.
includes
(
normalized
))
score
=
Math
.
max
(
score
,
normalized
.
length
);
else
if
(
normalized
.
length
>=
2
&&
t
.
includes
(
normalized
))
score
=
Math
.
max
(
score
,
normalized
.
length
);
}
}
if
(
score
<=
0
)
continue
;
if
(
score
<=
0
)
continue
;
const
sorter
=
Boolean
(
const
sorter
=
Boolean
(
cell
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
cell
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
cell
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
cell
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
);
);
const
rank
=
score
+
(
sorter
?
100
_0
00
:
0
);
const
rank
=
score
*
1000
+
(
sorter
?
5
00
:
0
);
if
(
rank
>
bestRank
)
{
if
(
rank
>
bestRank
)
{
bestRank
=
rank
;
bestRank
=
rank
;
best
=
cell
;
best
=
cell
;
...
@@ -285,7 +285,7 @@ export async function readDomColumnValues(wrapper: Locator, columnName: string,
...
@@ -285,7 +285,7 @@ export async function readDomColumnValues(wrapper: Locator, columnName: string,
let
score
=
0
;
let
score
=
0
;
if
(
normalized
===
t
)
score
=
t
.
length
+
1000
;
if
(
normalized
===
t
)
score
=
t
.
length
+
1000
;
else
if
(
normalized
.
includes
(
t
))
score
=
t
.
length
;
else
if
(
normalized
.
includes
(
t
))
score
=
t
.
length
;
else
if
(
normalized
.
length
>=
4
&&
t
.
includes
(
normalized
))
score
=
normalized
.
length
;
else
if
(
normalized
.
length
>=
2
&&
t
.
includes
(
normalized
))
score
=
normalized
.
length
;
if
(
score
>
best
)
{
if
(
score
>
best
)
{
best
=
score
;
best
=
score
;
index
=
i
;
index
=
i
;
...
@@ -341,46 +341,86 @@ async function clickDomColumnSort(
...
@@ -341,46 +341,86 @@ async function clickDomColumnSort(
.
forEach
((
el
)
=>
el
.
remove
());
.
forEach
((
el
)
=>
el
.
remove
());
return
normalize
(
th
.
querySelector
(
'
.ant-table-column-title
'
)?.
textContent
??
clone
.
textContent
??
''
);
return
normalize
(
th
.
querySelector
(
'
.ant-table-column-title
'
)?.
textContent
??
clone
.
textContent
??
''
);
};
};
let
best
:
HTMLElement
|
null
=
null
;
const
scoreText
=
(
normalized
:
string
):
number
=>
{
let
bestRank
=
0
;
for
(
const
th
of
Array
.
from
(
root
.
querySelectorAll
(
'
.ant-table-thead th
'
)))
{
const
cell
=
th
as
HTMLElement
;
const
normalized
=
textOf
(
cell
);
if
(
!
normalized
)
continue
;
let
score
=
0
;
let
score
=
0
;
for
(
const
token
of
payload
.
tokens
)
{
for
(
const
token
of
payload
.
tokens
)
{
const
t
=
normalize
(
token
);
const
t
=
normalize
(
token
);
if
(
!
t
)
continue
;
if
(
!
t
)
continue
;
if
(
normalized
===
t
)
score
=
Math
.
max
(
score
,
t
.
length
+
1000
);
if
(
normalized
===
t
)
score
=
Math
.
max
(
score
,
t
.
length
+
1000
);
else
if
(
normalized
.
includes
(
t
))
score
=
Math
.
max
(
score
,
t
.
length
);
else
if
(
normalized
.
includes
(
t
))
score
=
Math
.
max
(
score
,
t
.
length
);
else
if
(
normalized
.
length
>=
4
&&
t
.
includes
(
normalized
))
score
=
Math
.
max
(
score
,
normalized
.
length
);
// 宽表省略后标题可能短于 4 字(如「产品俗」「注册证」),仍允许反包含
else
if
(
normalized
.
length
>=
2
&&
t
.
includes
(
normalized
))
score
=
Math
.
max
(
score
,
normalized
.
length
);
}
}
if
(
score
<=
0
)
continue
;
return
score
;
const
sorter
=
Boolean
(
};
const
hasClassicSorter
=
(
cell
:
HTMLElement
)
=>
Boolean
(
cell
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
cell
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
cell
.
querySelector
(
'
.ant-table-column-sorter
'
)
||
cell
.
classList
.
contains
(
'
ant-table-column-has-sorters
'
),
);
);
if
(
!
sorter
)
continue
;
const
isClickableHeader
=
(
cell
:
HTMLElement
)
=>
{
const
box
=
cell
.
getBoundingClientRect
();
if
(
hasClassicSorter
(
cell
))
return
true
;
const
visible
=
box
.
width
>
0
&&
box
.
height
>
0
;
const
text
=
(
cell
.
textContent
??
''
).
replace
(
/
\s
+/g
,
'
'
);
const
inHeader
=
Boolean
(
cell
.
closest
(
'
.ant-table-header, .ant-table-fixed-left, .ant-table-fixed-right
'
));
return
(
const
rank
=
score
+
100
_000
+
(
visible
?
500
:
0
)
+
(
inHeader
?
200
:
0
);
cell
.
classList
.
contains
(
'
ant-table-column-sort
'
)
||
if
(
rank
>
bestRank
)
{
cell
.
getAttribute
(
'
aria-sort
'
)
===
'
ascending
'
||
bestRank
=
rank
;
cell
.
getAttribute
(
'
aria-sort
'
)
===
'
descending
'
||
best
=
cell
;
/caret-up|caret-down/i
.
test
(
text
)
||
Boolean
(
cell
.
querySelector
(
'
.anticon-caret-up, .anticon-caret-down
'
))
||
getComputedStyle
(
cell
).
cursor
===
'
pointer
'
);
};
// 先 sticky 表头,再退回任意 thead(避开只在测宽克隆里匹配到短别名)
const
preferredTheads
=
[
root
.
querySelector
(
'
.ant-table-container > .ant-table-header .ant-table-thead
'
),
root
.
querySelector
(
'
.ant-table-header .ant-table-thead
'
),
root
.
querySelector
(
'
.ant-table-content .ant-table-thead
'
),
].
filter
(
Boolean
)
as
Element
[];
const
theadLists
=
preferredTheads
.
length
>
0
?
preferredTheads
:
Array
.
from
(
root
.
querySelectorAll
(
'
.ant-table-thead
'
));
let
best
:
HTMLElement
|
null
=
null
;
let
bestRank
=
0
;
for
(
const
thead
of
theadLists
)
{
for
(
const
th
of
Array
.
from
(
thead
.
querySelectorAll
(
'
th
'
)))
{
const
cell
=
th
as
HTMLElement
;
const
normalized
=
textOf
(
cell
);
if
(
!
normalized
)
continue
;
const
score
=
scoreText
(
normalized
);
if
(
score
<=
0
)
continue
;
const
classic
=
hasClassicSorter
(
cell
);
const
clickable
=
isClickableHeader
(
cell
);
// FineReport / 部分宽表:可点表头排序但没有 .ant-table-column-sorter。
// 精确列名优先于「短别名 + sorter」(避免 规格型号 被 规格 抢走)。
if
(
!
classic
&&
!
clickable
&&
score
<
1000
)
continue
;
const
box
=
cell
.
getBoundingClientRect
();
const
visible
=
box
.
width
>
0
&&
box
.
height
>
0
;
const
inHeader
=
Boolean
(
cell
.
closest
(
'
.ant-table-header, .ant-table-fixed-left, .ant-table-fixed-right
'
),
);
const
rank
=
score
*
1000
+
(
classic
?
500
:
0
)
+
(
clickable
?
200
:
0
)
+
(
visible
?
50
:
0
)
+
(
inHeader
?
20
:
0
);
if
(
rank
>
bestRank
)
{
bestRank
=
rank
;
best
=
cell
;
}
}
}
}
}
if
(
!
best
)
return
false
;
if
(
!
best
)
return
false
;
const
hosts
=
root
.
querySelectorAll
(
'
.ant-table-body, .ant-table-header, .ant-table-content
'
);
const
hosts
=
root
.
querySelectorAll
(
'
.ant-table-body, .ant-table-header, .ant-table-header .ant-table-content, .ant-table-content
'
,
);
const
thRect
=
best
.
getBoundingClientRect
();
const
thRect
=
best
.
getBoundingClientRect
();
let
scrollLeft
=
0
;
const
sample
=
Array
.
from
(
hosts
).
find
(
const
sample
=
Array
.
from
(
hosts
).
find
((
el
)
=>
(
el
as
HTMLElement
).
scrollWidth
>
(
el
as
HTMLElement
).
clientWidth
+
4
)
as
(
el
)
=>
(
el
as
HTMLElement
).
scrollWidth
>
(
el
as
HTMLElement
).
clientWidth
+
4
,
|
HTMLElement
)
as
HTMLElement
|
undefined
;
|
undefined
;
if
(
sample
)
{
if
(
sample
)
{
const
hostRect
=
sample
.
getBoundingClientRect
();
const
hostRect
=
sample
.
getBoundingClientRect
();
let
scrollLeft
=
sample
.
scrollLeft
;
if
(
thRect
.
right
>
hostRect
.
right
-
24
)
scrollLeft
=
sample
.
scrollLeft
+
(
thRect
.
right
-
hostRect
.
right
)
+
80
;
if
(
thRect
.
right
>
hostRect
.
right
-
24
)
scrollLeft
=
sample
.
scrollLeft
+
(
thRect
.
right
-
hostRect
.
right
)
+
80
;
else
if
(
thRect
.
left
<
hostRect
.
left
+
24
)
scrollLeft
=
Math
.
max
(
0
,
sample
.
scrollLeft
-
(
hostRect
.
left
-
thRect
.
left
)
-
80
);
else
if
(
thRect
.
left
<
hostRect
.
left
+
24
)
{
else
scrollLeft
=
sample
.
scrollLeft
;
scrollLeft
=
Math
.
max
(
0
,
sample
.
scrollLeft
-
(
hostRect
.
left
-
thRect
.
left
)
-
80
);
}
for
(
const
host
of
Array
.
from
(
hosts
))
{
for
(
const
host
of
Array
.
from
(
hosts
))
{
(
host
as
HTMLElement
).
scrollLeft
=
scrollLeft
;
(
host
as
HTMLElement
).
scrollLeft
=
scrollLeft
;
}
}
...
...
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