@opentiny/next-sdk 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SimulatorMask-BHVXyogh-CARX3Rff.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
- package/dist/{index-R_HIbfUX.js → index-CHMa2zG4.js} +1065 -1042
- package/dist/index.js +1 -1
- package/dist/page-tools/a11y/config.d.ts +6 -3
- package/dist/page-tools/a11y/constants.d.ts +20 -0
- package/dist/page-tools/a11y/utils.d.ts +14 -0
- package/dist/page-tools/constants.d.ts +4 -2
- package/dist/page-tools/context.d.ts +2 -0
- package/dist/page-tools/handlers/hover.d.ts +9 -0
- package/dist/page-tools/schema.d.ts +3 -3
- package/dist/page-tools/utils/dom.d.ts +8 -2
- package/dist/runtime.js +19 -172
- package/package.json +6 -1
- package/WebMcpClient.ts +0 -505
- package/WebMcpServer.ts +0 -447
- package/agent/AgentModelProvider.ts +0 -932
- package/agent/type.ts +0 -91
- package/agent/utils/generateReActPrompt.ts +0 -71
- package/agent/utils/getAISDKTools.ts +0 -34
- package/agent/utils/getBuiltinMcpTools.ts +0 -86
- package/agent/utils/parseReActAction.ts +0 -34
- package/core.ts +0 -26
- package/index.ts +0 -91
- package/page-tools/a11y/build.ts +0 -74
- package/page-tools/a11y/config.ts +0 -465
- package/page-tools/a11y/constants.ts +0 -131
- package/page-tools/a11y/search.ts +0 -127
- package/page-tools/a11y/types.ts +0 -105
- package/page-tools/a11y/utils.ts +0 -239
- package/page-tools/a11y/vnode.ts +0 -439
- package/page-tools/a11y-tree.ts +0 -14
- package/page-tools/bridge.ts +0 -722
- package/page-tools/configs/console-cloud.ts +0 -172
- package/page-tools/constants.ts +0 -12
- package/page-tools/context.ts +0 -50
- package/page-tools/effects.ts +0 -345
- package/page-tools/handlers/browserState.ts +0 -12
- package/page-tools/handlers/click.ts +0 -30
- package/page-tools/handlers/executeJavascript.ts +0 -22
- package/page-tools/handlers/fill.ts +0 -65
- package/page-tools/handlers/scroll.ts +0 -66
- package/page-tools/handlers/searchTree.ts +0 -27
- package/page-tools/handlers/select.ts +0 -39
- package/page-tools/initialize-builtin-WebMCP.ts +0 -20
- package/page-tools/page-agent-highlight/index.ts +0 -245
- package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
- package/page-tools/page-agent-mask/SimulatorMask.ts +0 -299
- package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
- package/page-tools/page-agent-mask/cursor-border.svg +0 -3
- package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
- package/page-tools/page-agent-mask/cursor.module.css +0 -70
- package/page-tools/page-agent-mask/hauwei.svg +0 -25
- package/page-tools/page-agent-prompt.md +0 -139
- package/page-tools/page-agent-tool-event.ts +0 -113
- package/page-tools/page-agent-tool.ts +0 -207
- package/page-tools/page-state-cache.ts +0 -78
- package/page-tools/schema.ts +0 -52
- package/page-tools/tool-config.ts +0 -100
- package/page-tools/utils/dom.ts +0 -158
- package/page-tools/utils/scroll.ts +0 -58
- package/remoter/QrCode.ts +0 -54
- package/remoter/createRemoter.ts +0 -957
- package/remoter/svgs/chat.svg +0 -43
- package/remoter/svgs/icon-copy.svg +0 -25
- package/remoter/svgs/link.svg +0 -28
- package/remoter/svgs/logo.svg +0 -45
- package/remoter/svgs/qrcode.svg +0 -35
- package/remoter/svgs/scan.svg +0 -21
- package/remoter/tooltips.ts +0 -260
- package/runtime.html +0 -39
- package/runtime.ts +0 -44
- package/script/utils.ts +0 -26
- package/skills/index.ts +0 -287
- package/test/page-tools/a11y/build.test.ts +0 -638
- package/test/page-tools/a11y/config.test.ts +0 -370
- package/test/page-tools/configs/console-cloud.test.ts +0 -168
- package/test/page-tools/page-agent-highlight.test.ts +0 -110
- package/test/page-tools/page-agent-tool-dispatch.test.ts +0 -208
- package/test/page-tools/page-agent-tool.test.ts +0 -102
- package/test/page-tools/tool-config.test.ts +0 -112
- package/test/page-tools/utils/dom.test.ts +0 -122
- package/tsconfig.json +0 -16
- package/utils/builtinProxy.ts +0 -144
- package/utils/env.ts +0 -13
- package/utils/uuid.ts +0 -10
- package/vite.config.runtime.ts +0 -22
- package/vite.config.ts +0 -63
- package/vitest.config.ts +0 -10
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { buildA11yTree } from '../../../page-tools/a11y/build'
|
|
3
|
-
|
|
4
|
-
function setupRoot(html: string): HTMLElement {
|
|
5
|
-
const root = document.createElement('div')
|
|
6
|
-
root.innerHTML = html.trim()
|
|
7
|
-
document.body.appendChild(root)
|
|
8
|
-
return root
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
document.body.innerHTML = ''
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
document.body.innerHTML = ''
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
describe('buildA11yTree - 白名单/黑名单', () => {
|
|
20
|
-
it('黑名单:Element 引用会排除该元素及其子树', () => {
|
|
21
|
-
const root = setupRoot(`
|
|
22
|
-
<button id="keep">保留</button>
|
|
23
|
-
<div id="excluded"><button id="child">子按钮</button></div>
|
|
24
|
-
`)
|
|
25
|
-
const excluded = root.querySelector('#excluded') as Element
|
|
26
|
-
const { yaml } = buildA11yTree(root, { blacklist: [excluded] })
|
|
27
|
-
expect(yaml).toContain('保留')
|
|
28
|
-
expect(yaml).not.toContain('子按钮')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('黑名单:CSS 选择器字符串(含属性选择器)动态解析排除元素', () => {
|
|
32
|
-
const root = setupRoot(`
|
|
33
|
-
<button data-track="pixel">追踪像素按钮</button>
|
|
34
|
-
<button>正常按钮</button>
|
|
35
|
-
`)
|
|
36
|
-
const { yaml } = buildA11yTree(root, { blacklist: ['[data-track="pixel"]'] })
|
|
37
|
-
expect(yaml).not.toContain('追踪像素按钮')
|
|
38
|
-
expect(yaml).toContain('正常按钮')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('白名单:Element 引用强制分配 ref(即使原本不是交互元素)', () => {
|
|
42
|
-
const root = setupRoot(`<div id="card">自定义卡片</div>`)
|
|
43
|
-
const card = root.querySelector('#card') as Element
|
|
44
|
-
const { refMap } = buildA11yTree(root, { whitelist: [card] })
|
|
45
|
-
expect(Array.from(refMap.values())).toContain(card)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('白名单:CSS 选择器字符串强制分配 ref', () => {
|
|
49
|
-
const root = setupRoot(`<div class="custom-clickable-card">卡片内容</div>`)
|
|
50
|
-
const { yaml, refMap } = buildA11yTree(root, { whitelist: ['.custom-clickable-card'] })
|
|
51
|
-
expect(refMap.size).toBeGreaterThan(0)
|
|
52
|
-
expect(yaml).toMatch(/#\d+.*"卡片内容"/)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('白名单与黑名单混合使用:分别生效且不互相影响', () => {
|
|
56
|
-
const root = setupRoot(`
|
|
57
|
-
<div id="whitelisted">白名单卡片</div>
|
|
58
|
-
<button id="blacklisted">黑名单按钮</button>
|
|
59
|
-
`)
|
|
60
|
-
const whitelisted = root.querySelector('#whitelisted') as Element
|
|
61
|
-
const blacklisted = root.querySelector('#blacklisted') as Element
|
|
62
|
-
const { yaml, refMap } = buildA11yTree(root, { whitelist: [whitelisted], blacklist: [blacklisted] })
|
|
63
|
-
expect(Array.from(refMap.values())).toContain(whitelisted)
|
|
64
|
-
expect(yaml).not.toContain('黑名单按钮')
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('buildA11yTree - roles 自定义角色规则', () => {
|
|
69
|
-
it('无 role 属性的 .tab-item 被识别为 tab 并出现在 YAML 输出中', () => {
|
|
70
|
-
const root = setupRoot(`<div class="my-tabs"><div class="tab-item">概览</div></div>`)
|
|
71
|
-
const { yaml } = buildA11yTree(root, {
|
|
72
|
-
roles: [{ role: 'tab', selector: '.tab-item' }],
|
|
73
|
-
})
|
|
74
|
-
expect(yaml).toMatch(/-\s+tab\b.*"概览"/)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('显式 role 不会被普通规则覆盖,force:true 时才覆盖', () => {
|
|
78
|
-
const root = setupRoot(`<div class="tab-item" role="button">按钮态Tab</div>`)
|
|
79
|
-
const notForced = buildA11yTree(root, { roles: [{ role: 'tab', selector: '.tab-item' }] })
|
|
80
|
-
expect(notForced.yaml).toContain('button')
|
|
81
|
-
expect(notForced.yaml).not.toMatch(/-\s+tab\b/)
|
|
82
|
-
|
|
83
|
-
const forced = buildA11yTree(root, { roles: [{ role: 'tab', selector: '.tab-item', force: true }] })
|
|
84
|
-
expect(forced.yaml).toMatch(/-\s+tab\b/)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('buildA11yTree - states 自定义状态规则', () => {
|
|
89
|
-
it('aria-disabled 的 role=button 不分配 ref', () => {
|
|
90
|
-
const root = setupRoot(`<div role="button" aria-disabled="true" tabindex="0">禁用</div>`)
|
|
91
|
-
const { refMap } = buildA11yTree(root)
|
|
92
|
-
expect(refMap.size).toBe(0)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('按钮组通过 class 判定 selected', () => {
|
|
96
|
-
const root = setupRoot(`<div class="btn-group"><button class="btn is-checked">选中项</button></div>`)
|
|
97
|
-
const { yaml } = buildA11yTree(root, {
|
|
98
|
-
states: { selected: { selector: '.btn-group .btn.is-checked' } },
|
|
99
|
-
})
|
|
100
|
-
expect(yaml).toMatch(/\[selected\]/)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('报错元素通过 match 函数判定 error', () => {
|
|
104
|
-
const root = setupRoot(`<div class="err-text">用户名不能为空</div>`)
|
|
105
|
-
const errEl = root.querySelector('.err-text') as HTMLElement
|
|
106
|
-
errEl.style.color = 'rgb(245, 34, 45)'
|
|
107
|
-
const { yaml } = buildA11yTree(root, {
|
|
108
|
-
states: { error: { match: (e) => window.getComputedStyle(e).color === 'rgb(245, 34, 45)' } },
|
|
109
|
-
})
|
|
110
|
-
expect(yaml).toMatch(/\[error\]/)
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe('buildA11yTree - 树形状选项回归(不受本次重构影响)', () => {
|
|
115
|
-
it('pruneUnnamed 默认 true:无 ref 且无 name 的容器被透明穿透', () => {
|
|
116
|
-
const root = setupRoot(`<div><div><button>按钮</button></div></div>`)
|
|
117
|
-
const { yaml } = buildA11yTree(root)
|
|
118
|
-
// 外层无意义的 generic 容器被穿透,只剩 button 一行结构
|
|
119
|
-
const lines = yaml.split('\n').filter((l) => l.trim().startsWith('-'))
|
|
120
|
-
expect(lines.some((l) => l.includes('button'))).toBe(true)
|
|
121
|
-
expect(lines.every((l) => !l.includes('generic'))).toBe(true)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('preserveRoles 强制保留指定角色,即使无 name 也不穿透', () => {
|
|
125
|
-
const root = setupRoot(`<table><tbody><tr><td>单元格</td></tr></tbody></table>`)
|
|
126
|
-
const { yaml } = buildA11yTree(root, { preserveRoles: ['table', 'row'] })
|
|
127
|
-
expect(yaml).toContain('table')
|
|
128
|
-
expect(yaml).toContain('row')
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 非内容节点与 CSS 噪声过滤
|
|
134
|
-
*
|
|
135
|
-
* 背景(华为云控制台实测):body 内联 iconfont `<style>@font-face { src: url(data:font/...;base64,...) }</style>`,
|
|
136
|
-
* 若被 textContent / 静态上提吸进 generic name,单行可达数万 token。
|
|
137
|
-
*
|
|
138
|
-
* 规则:
|
|
139
|
-
* - style / script / noscript / template 等不进树
|
|
140
|
-
* - 文本收集跳过上述子树,禁止裸 textContent
|
|
141
|
-
* - 名称若像 @font-face / data:font / 长 base64 则视为噪声丢弃
|
|
142
|
-
*/
|
|
143
|
-
describe('buildA11yTree - 非内容节点与 CSS 噪声过滤', () => {
|
|
144
|
-
/** 模拟控制台 iconfont 内联样式(base64 截短但仍足够触发噪声检测) */
|
|
145
|
-
const ICONFONT_STYLE = `
|
|
146
|
-
@font-face {
|
|
147
|
-
font-family: iconfont;
|
|
148
|
-
src: url("data:font/woff2;base64,d09GMgABAAAAAAPEAAsAAAAAB/gAAAN2AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDHAqDIIJ2ATYCJAMQCwoABCAFhGcHSxsBB8gehXEzfcQqKjPbRwsC1KUnyf7/2EHgrYTsFJCm8kQEY8JTYfXRWpt/xGxCOouop7eEncye2R5WCUk0kSmNBCF+oxKiaAKzwvv0p3JGCZ8FQDrGpjlobfxg8QIFqHtarSAbqFdPBbkKsDNYi/wvQQAG2cySr2/cLkH71FE8gGFqMfgC4kIxdZKpQiNvR6spFLgQONCVGmI7OsgN8iWwzf+86CBXuECiK4x9eo5fMeCpmc4GZdkWqJ+4PATkaAnogALSF+YUE+v9o6kJU+nUYJT9ppZCABcuJDTTvKqzwbZJ5ab0LvMvDwQSBcKBke8OQD6VVtCMzUFA8yoOEjobsijgqYx3AU5gLXCCkoT3kEhUXOzYYK0h7hLPja/vC9F01jQ3nQbrlumx6U66/u5LY/6+TQH9FG+VA5TjK64t/7G7/XdvDNwbOH5vU9AMK3i/4b7xggA0A8L3OYVmXTO9Np0Vqu/o64CA6pcB+56AOaB/M8ZF/49935Y+7vtAZ07XjucSD9zz3soVceepcQ+eMR7VHjGeI8JKof9h9WH76dnH4lLP42pn3Ycd2L2rxsPXs9q2eNREgQPhG7rypU6nz8Xm0P8XfQJp6lawCzj50PeTb6t/SM+zf7a7LTng0y+3p82/y/+wf79ti9c7haYiB1mLene8qxHHEtv+tP1IXK/3ulfyqil5D+sVUWTfrjZW/DPwTndjjsVIYCbEWeqWqgXs1T3lDdjUcVX9DcVhX3Vjywiv8n8uUwPgd7dLP/2gTaOb4hsoYPkXMwwFiZoaBk0rU9vKf4Gx4ZITEjBMF3yiHPjyOoTRD84HgQu/GBInYVC4iKU0SSp0THLhwEUFDNJYWW/iR0QNQnMDEjkjIPDmERJPrkHhzQulSarQCeQNDrz5hUF/4bGbSaIYU5eQyslAbRHaY8KAWSbuCy2QmXlqUhSvUJLaZBRlQcpm1yigpI856XxT4XMZstmhj6vseeR5IUazQ4d0LlhzeTQpiqzsKoI+w4clEggVDmGAaJyA2OiEBDCXzNPfV4AwyfBQSUCqkkpIsUnhiIyABFBr+gDquJB9Ug4xUeBwGAgjITI+yCrvJTwlFYJE5cMcCB2OgNUhETGJaJIYqhemN/q3twswMPbThBRKaELHsXCSeqrajePHBWE8MNvmTHQH6VaWkpupNgAAAA==") format("woff2");
|
|
149
|
-
}
|
|
150
|
-
`
|
|
151
|
-
|
|
152
|
-
it(
|
|
153
|
-
'场景:body 内联 iconfont <style> 含 @font-face data:font;base64(控制台实测)\n' +
|
|
154
|
-
'问题:style 被当成 generic,整段 CSS/base64 进入 YAML,token 爆炸\n' +
|
|
155
|
-
'期望:style 不进树;交互节点文案保留;无 @font-face / base64 / data:font',
|
|
156
|
-
() => {
|
|
157
|
-
const root = setupRoot(`
|
|
158
|
-
<div>
|
|
159
|
-
<style type="text/css">${ICONFONT_STYLE}</style>
|
|
160
|
-
<a href="/console">云服务器控制台</a>
|
|
161
|
-
<button type="button">包年/包月</button>
|
|
162
|
-
</div>
|
|
163
|
-
`)
|
|
164
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
165
|
-
|
|
166
|
-
expect(yaml).not.toMatch(/@font-face/i)
|
|
167
|
-
expect(yaml).not.toMatch(/base64,/i)
|
|
168
|
-
expect(yaml).not.toMatch(/data:font\//i)
|
|
169
|
-
expect(lines.some((l) => /link.*"云服务器控制台"/.test(l))).toBe(true)
|
|
170
|
-
expect(lines.some((l) => /button.*"包年\/包月"/.test(l))).toBe(true)
|
|
171
|
-
},
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
it(
|
|
175
|
-
'场景:混合子树中 style 与面包屑/链接并列(对应日志里 generic "@font-face..." 包住整页)\n' +
|
|
176
|
-
'问题:Static-Lift 把 style 文案上提到含交互子树的父 generic name\n' +
|
|
177
|
-
'期望:父级不上提 CSS;面包屑链接正常;YAML 无字体噪声',
|
|
178
|
-
() => {
|
|
179
|
-
const root = setupRoot(`
|
|
180
|
-
<div id="page-root">
|
|
181
|
-
<style type="text/css">${ICONFONT_STYLE}</style>
|
|
182
|
-
<span>/</span>
|
|
183
|
-
<a href="/ecs">云服务器控制台</a>
|
|
184
|
-
<span>/</span>
|
|
185
|
-
<a href="/ecs/list">弹性云服务器</a>
|
|
186
|
-
<span>购买弹性云服务器</span>
|
|
187
|
-
</div>
|
|
188
|
-
`)
|
|
189
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
190
|
-
|
|
191
|
-
expect(yaml).not.toMatch(/@font-face/i)
|
|
192
|
-
expect(yaml).not.toMatch(/base64,/i)
|
|
193
|
-
expect(yaml).not.toMatch(/data:font\//i)
|
|
194
|
-
// 父节点 name 不得是 CSS
|
|
195
|
-
expect(lines.every((l) => !/generic\s+"@font-face/.test(l))).toBe(true)
|
|
196
|
-
expect(lines.some((l) => /link.*"云服务器控制台"/.test(l))).toBe(true)
|
|
197
|
-
expect(lines.some((l) => /link.*"弹性云服务器"/.test(l))).toBe(true)
|
|
198
|
-
expect(yaml).toContain('购买弹性云服务器')
|
|
199
|
-
},
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
it(
|
|
203
|
-
'场景:<script> 内含业务字符串\n' +
|
|
204
|
-
'问题:script 文本若被收集,会污染 name 且泄露无用源码\n' +
|
|
205
|
-
'期望:script 不进树,YAML 不含脚本内字符串',
|
|
206
|
-
() => {
|
|
207
|
-
const root = setupRoot(`
|
|
208
|
-
<div>
|
|
209
|
-
<script>window.__SECRET_MARKER__ = "should-not-appear-in-a11y"</script>
|
|
210
|
-
<button type="button">提交</button>
|
|
211
|
-
</div>
|
|
212
|
-
`)
|
|
213
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
214
|
-
|
|
215
|
-
expect(yaml).not.toContain('should-not-appear-in-a11y')
|
|
216
|
-
expect(yaml).not.toContain('__SECRET_MARKER__')
|
|
217
|
-
expect(lines.some((l) => /button.*"提交"/.test(l))).toBe(true)
|
|
218
|
-
},
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
it(
|
|
222
|
-
'场景:<noscript> 降级文案与可见按钮并存\n' +
|
|
223
|
-
'问题:noscript 内容对开启 JS 的 Agent 无意义,却可能被 textContent 吸收\n' +
|
|
224
|
-
'期望:noscript 不进树;按钮文案保留',
|
|
225
|
-
() => {
|
|
226
|
-
const root = setupRoot(`
|
|
227
|
-
<div>
|
|
228
|
-
<noscript>请启用 JavaScript 以使用控制台</noscript>
|
|
229
|
-
<button type="button">立即购买</button>
|
|
230
|
-
</div>
|
|
231
|
-
`)
|
|
232
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
233
|
-
|
|
234
|
-
expect(yaml).not.toContain('请启用 JavaScript')
|
|
235
|
-
expect(lines.some((l) => /button.*"立即购买"/.test(l))).toBe(true)
|
|
236
|
-
},
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
it(
|
|
240
|
-
'场景:<template> 内含未激活的 DOM 文案\n' +
|
|
241
|
-
'问题:template 内容默认不可见,不应出现在 a11y YAML\n' +
|
|
242
|
-
'期望:template 内文案不出现;页面真实按钮保留',
|
|
243
|
-
() => {
|
|
244
|
-
const root = setupRoot(`
|
|
245
|
-
<div>
|
|
246
|
-
<template><span>模板内隐藏文案-TEMPLATE_MARKER</span></template>
|
|
247
|
-
<button type="button">自定义购买</button>
|
|
248
|
-
</div>
|
|
249
|
-
`)
|
|
250
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
251
|
-
|
|
252
|
-
expect(yaml).not.toContain('TEMPLATE_MARKER')
|
|
253
|
-
expect(lines.some((l) => /button.*"自定义购买"/.test(l))).toBe(true)
|
|
254
|
-
},
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
it(
|
|
258
|
-
'场景:纯静态折叠时父节点 textContent 含 style(无交互子树)\n' +
|
|
259
|
-
'问题:规则 1 折叠兜底若用裸 textContent,仍会把 @font-face 拼进父 name\n' +
|
|
260
|
-
'期望:折叠后的 option/generic 名称不含 CSS 噪声,只保留可见文案',
|
|
261
|
-
() => {
|
|
262
|
-
const root = setupRoot(`
|
|
263
|
-
<div role="option">
|
|
264
|
-
<style type="text/css">${ICONFONT_STYLE}</style>
|
|
265
|
-
<span>可用区</span>
|
|
266
|
-
<span>随机分配</span>
|
|
267
|
-
</div>
|
|
268
|
-
`)
|
|
269
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
270
|
-
const contentLines = lines.filter((l) => l.trim().startsWith('-'))
|
|
271
|
-
|
|
272
|
-
expect(yaml).not.toMatch(/@font-face/i)
|
|
273
|
-
expect(yaml).not.toMatch(/base64,/i)
|
|
274
|
-
expect(contentLines.some((l) => l.includes('可用区'))).toBe(true)
|
|
275
|
-
expect(contentLines.some((l) => l.includes('随机分配'))).toBe(true)
|
|
276
|
-
},
|
|
277
|
-
)
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 父子文字去重与折叠(Static-Lift + Interactive-Keep)
|
|
282
|
-
*
|
|
283
|
-
* 背景(华为云控制台 ECS 快速购买页实测):
|
|
284
|
-
* AccName 会把 button/link 等子节点文案汇总进 row/cell/generic 父节点,
|
|
285
|
-
* YAML 出现大量「父 name ≈ 全部子文案拼接」的重复,浪费 LLM token。
|
|
286
|
-
*
|
|
287
|
-
* 规则:
|
|
288
|
-
* 1. 子树全静态 → 折叠为一行,文字留在父节点
|
|
289
|
-
* 2. 混合子树 → 静态文案上提到父 name(成功才省略静态子);交互分支保留;
|
|
290
|
-
* 禁止吸收交互文案;上提失败则静态子独立输出(文字不丢弃)
|
|
291
|
-
*/
|
|
292
|
-
describe('buildA11yTree - 父子文字去重与折叠', () => {
|
|
293
|
-
it(
|
|
294
|
-
'场景:混合子树——静态标签 + 多个按钮(计费模式)\n' +
|
|
295
|
-
'问题:AccName 汇总成 row/cell "计费模式 包年/包月 按需计费",与子 button 重复\n' +
|
|
296
|
-
'期望:静态文案上提到 cell,交互按钮保留为子节点:\n' +
|
|
297
|
-
' - cell "计费模式"\n' +
|
|
298
|
-
' - button "包年/包月"\n' +
|
|
299
|
-
' - button "按需计费"',
|
|
300
|
-
() => {
|
|
301
|
-
const root = setupRoot(`
|
|
302
|
-
<div role="row">
|
|
303
|
-
<div role="cell">
|
|
304
|
-
<span>计费模式</span>
|
|
305
|
-
<button type="button">包年/包月</button>
|
|
306
|
-
<button type="button">按需计费</button>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
`)
|
|
310
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
311
|
-
|
|
312
|
-
expect(yaml.split('包年/包月').length - 1).toBe(1)
|
|
313
|
-
expect(yaml.split('按需计费').length - 1).toBe(1)
|
|
314
|
-
expect(yaml.split('计费模式').length - 1).toBe(1)
|
|
315
|
-
expect(yaml).not.toMatch(/"计费模式\s*包年\/包月\s*按需计费"/)
|
|
316
|
-
expect(lines.some((l) => /cell\s+"计费模式"/.test(l))).toBe(true)
|
|
317
|
-
expect(lines.some((l) => /button.*"包年\/包月"/.test(l))).toBe(true)
|
|
318
|
-
expect(lines.some((l) => /button.*"按需计费"/.test(l))).toBe(true)
|
|
319
|
-
// 静态 span 已上提,不应再单独输出一层 generic "计费模式"
|
|
320
|
-
expect(lines.filter((l) => /"计费模式"/.test(l))).toHaveLength(1)
|
|
321
|
-
},
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
it(
|
|
325
|
-
'场景:可交互按钮外包一层同文案的布局容器(如顶栏「打开服务列表」)\n' +
|
|
326
|
-
'问题:修复前输出两层同名节点浪费 token:\n' +
|
|
327
|
-
' - generic "打开服务列表"\n' +
|
|
328
|
-
' - button "打开服务列表"\n' +
|
|
329
|
-
'期望:无静态兄弟可上提时父容器穿透,只保留一行 button(文字不丢)',
|
|
330
|
-
() => {
|
|
331
|
-
const root = setupRoot(`
|
|
332
|
-
<div>
|
|
333
|
-
<button type="button" title="打开服务列表">打开服务列表</button>
|
|
334
|
-
</div>
|
|
335
|
-
`)
|
|
336
|
-
const { lines } = buildA11yTree(root)
|
|
337
|
-
const contentLines = lines.filter((l) => l.trim().startsWith('-'))
|
|
338
|
-
|
|
339
|
-
expect(contentLines).toHaveLength(1)
|
|
340
|
-
expect(contentLines[0]).toMatch(/button.*"打开服务列表"/)
|
|
341
|
-
expect(yamlHasDuplicateAccessibleName(lines, '打开服务列表')).toBe(false)
|
|
342
|
-
},
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
it(
|
|
346
|
-
'场景:纯展示信息的嵌套 option/region/span(如「其他配置 → 可用区 : 随机分配」)\n' +
|
|
347
|
-
'问题:修复前层层重复同一语义:\n' +
|
|
348
|
-
' - option "可用区 : 随机分配"\n' +
|
|
349
|
-
' - region "可用区: 随机分配"\n' +
|
|
350
|
-
' - generic "可用区"\n' +
|
|
351
|
-
'期望:子树无可交互节点时折叠为一行,文字只留在父 option 上',
|
|
352
|
-
() => {
|
|
353
|
-
const root = setupRoot(`
|
|
354
|
-
<div role="option">
|
|
355
|
-
<div role="region" aria-label="可用区: 随机分配">
|
|
356
|
-
<span>可用区</span>
|
|
357
|
-
<span>随机分配</span>
|
|
358
|
-
</div>
|
|
359
|
-
</div>
|
|
360
|
-
`)
|
|
361
|
-
const { lines } = buildA11yTree(root)
|
|
362
|
-
const contentLines = lines.filter((l) => l.trim().startsWith('-'))
|
|
363
|
-
|
|
364
|
-
expect(contentLines).toHaveLength(1)
|
|
365
|
-
expect(contentLines[0]).toMatch(/option/)
|
|
366
|
-
expect(contentLines[0]).toMatch(/可用区/)
|
|
367
|
-
expect(contentLines[0]).toMatch(/随机分配/)
|
|
368
|
-
expect(contentLines.some((l) => l.includes('region'))).toBe(false)
|
|
369
|
-
},
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
it(
|
|
373
|
-
'场景:带 aria-label 的 region 内仅含交互控件(如「基础配置」)\n' +
|
|
374
|
-
'问题:若父节点吸收子按钮文案,会变成 "基础配置 包年/包月"\n' +
|
|
375
|
-
'期望:父保留 aria-label,子 button 独立保留文案',
|
|
376
|
-
() => {
|
|
377
|
-
const root = setupRoot(`
|
|
378
|
-
<section aria-label="基础配置">
|
|
379
|
-
<button type="button">包年/包月</button>
|
|
380
|
-
</section>
|
|
381
|
-
`)
|
|
382
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
383
|
-
|
|
384
|
-
expect(lines.some((l) => /region.*"基础配置"/.test(l))).toBe(true)
|
|
385
|
-
expect(lines.some((l) => /button.*"包年\/包月"/.test(l))).toBe(true)
|
|
386
|
-
expect(yaml).not.toMatch(/"基础配置\s*包年\/包月"/)
|
|
387
|
-
},
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
it(
|
|
391
|
-
'场景:aria-label 与额外静态说明并存(声明名 ≠ 说明文案)\n' +
|
|
392
|
-
'问题:若因有 aria-label 而丢弃说明 span,AI 会丢失业务提示\n' +
|
|
393
|
-
'期望:region 保留「基础配置」,说明「以下选项创建后不可更改」仍作为子节点输出,按钮独立',
|
|
394
|
-
() => {
|
|
395
|
-
const root = setupRoot(`
|
|
396
|
-
<section aria-label="基础配置">
|
|
397
|
-
<span>以下选项创建后不可更改</span>
|
|
398
|
-
<button type="button">包年/包月</button>
|
|
399
|
-
</section>
|
|
400
|
-
`)
|
|
401
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
402
|
-
|
|
403
|
-
expect(lines.some((l) => /region.*"基础配置"/.test(l))).toBe(true)
|
|
404
|
-
expect(lines.some((l) => l.includes('以下选项创建后不可更改'))).toBe(true)
|
|
405
|
-
expect(lines.some((l) => /button.*"包年\/包月"/.test(l))).toBe(true)
|
|
406
|
-
expect(yaml).not.toMatch(/"基础配置\s*以下选项/)
|
|
407
|
-
expect(yaml).not.toMatch(/"基础配置\s*包年\/包月"/)
|
|
408
|
-
},
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
it(
|
|
412
|
-
'场景:可点击父容器内再嵌套可点击子节点(如用户头像菜单 GT-zzcr)\n' +
|
|
413
|
-
'问题:修复前父子同名:\n' +
|
|
414
|
-
' - generic #15 [cursor=pointer] "GT-zzcr"\n' +
|
|
415
|
-
' - menuitem #16 "GT-zzcr"\n' +
|
|
416
|
-
'期望:父节点可保留 ref,但不输出从子节点吸收的同名文案;「GT-zzcr」只出现在子交互节点上',
|
|
417
|
-
() => {
|
|
418
|
-
const root = setupRoot(`
|
|
419
|
-
<div id="user-menu">
|
|
420
|
-
<button type="button">GT-zzcr</button>
|
|
421
|
-
</div>
|
|
422
|
-
`)
|
|
423
|
-
const userMenu = root.querySelector('#user-menu') as Element
|
|
424
|
-
const { lines } = buildA11yTree(root, { whitelist: [userMenu] })
|
|
425
|
-
const namedLines = lines.filter((l) => /"GT-zzcr"/.test(l))
|
|
426
|
-
|
|
427
|
-
expect(namedLines).toHaveLength(1)
|
|
428
|
-
expect(namedLines[0]).toMatch(/button.*"GT-zzcr"/)
|
|
429
|
-
const parentWithRef = lines.find((l) => /^\s*- generic #\d+/.test(l))
|
|
430
|
-
expect(parentWithRef).toBeTruthy()
|
|
431
|
-
expect(parentWithRef!).not.toMatch(/"GT-zzcr"/)
|
|
432
|
-
},
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
it(
|
|
436
|
-
'场景:混合子树——标签 + combobox + 说明 + 链接(区域行)\n' +
|
|
437
|
-
'问题:cell AccName 把交互文案也拼进长 name,与子节点重复且浪费 token\n' +
|
|
438
|
-
'期望:静态文案全部上提到 cell(文字不丢),交互节点保留为子级:\n' +
|
|
439
|
-
' - cell "区域 云服务器创建后无法更改区域。"\n' +
|
|
440
|
-
' - combobox "华北-北京四"\n' +
|
|
441
|
-
' - link "如何选择区域"',
|
|
442
|
-
() => {
|
|
443
|
-
const root = setupRoot(`
|
|
444
|
-
<div role="row">
|
|
445
|
-
<div role="cell">
|
|
446
|
-
<span>区域</span>
|
|
447
|
-
<div role="combobox" tabindex="0" aria-label="华北-北京四">华北-北京四</div>
|
|
448
|
-
<span>云服务器创建后无法更改区域。</span>
|
|
449
|
-
<a href="/help">如何选择区域</a>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
`)
|
|
453
|
-
const { yaml, lines } = buildA11yTree(root)
|
|
454
|
-
|
|
455
|
-
// 保字:两段静态文案都必须出现
|
|
456
|
-
expect(yaml).toContain('区域')
|
|
457
|
-
expect(yaml).toContain('云服务器创建后无法更改区域。')
|
|
458
|
-
expect(lines.some((l) => /combobox.*"华北-北京四"/.test(l))).toBe(true)
|
|
459
|
-
expect(lines.some((l) => /link.*"如何选择区域"/.test(l))).toBe(true)
|
|
460
|
-
// 上提后挂在 cell 上,且不得吸收交互文案
|
|
461
|
-
expect(lines.some((l) => /cell\s+"区域\s+云服务器创建后无法更改区域。"/.test(l))).toBe(true)
|
|
462
|
-
expect(yaml).not.toMatch(/cell\s+"[^"]*华北-北京四[^"]*"/)
|
|
463
|
-
expect(yaml).not.toMatch(/cell\s+"[^"]*如何选择区域[^"]*"/)
|
|
464
|
-
},
|
|
465
|
-
)
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* 通用 cursor:pointer 可点击元素识别
|
|
470
|
-
*
|
|
471
|
-
* 背景(华为云控制台实测):Angular/自定义组件中大量「无 role、无 tabindex、仅靠 (click) 绑定」的
|
|
472
|
-
* 可点击卡片(如 <div id="resource-card" class="card-wrapper">),此前因 role=generic 被漏判为不可交互。
|
|
473
|
-
*
|
|
474
|
-
* 规则(对齐 browser-use 等业界 DOM 提取器):cursor:pointer 是可点击性的通用兜底信号;
|
|
475
|
-
* 但 CSS cursor 会向子孙继承,故以「自身声明指针手势」(元素为 pointer 且父级非 pointer)
|
|
476
|
-
* 定位真正的可点击边界元素,避免容器把 ref 传染给全部子孙。
|
|
477
|
-
*/
|
|
478
|
-
describe('buildA11yTree - 通用 cursor:pointer 可点击元素识别', () => {
|
|
479
|
-
it(
|
|
480
|
-
'场景:无 role / 无 tabindex 的自定义可点击卡片(cursor:pointer)\n' +
|
|
481
|
-
'问题:role=generic 被漏判为不可交互,卡片拿不到 ref\n' +
|
|
482
|
-
'期望:卡片自身分配 ref,且带 cursor=pointer token 与内容名',
|
|
483
|
-
() => {
|
|
484
|
-
const root = setupRoot(`
|
|
485
|
-
<div class="card-list">
|
|
486
|
-
<div id="resource-card" class="card-wrapper">
|
|
487
|
-
<span class="image-ECS"></span>
|
|
488
|
-
<div class="info">
|
|
489
|
-
<div class="info-title">弹性云服务器 ECS</div>
|
|
490
|
-
<div class="info-content">
|
|
491
|
-
<span class="instance-number">1</span>
|
|
492
|
-
<span class="service-name">云服务器</span>
|
|
493
|
-
</div>
|
|
494
|
-
</div>
|
|
495
|
-
</div>
|
|
496
|
-
</div>
|
|
497
|
-
`)
|
|
498
|
-
const card = root.querySelector('#resource-card') as HTMLElement
|
|
499
|
-
card.style.cursor = 'pointer'
|
|
500
|
-
|
|
501
|
-
const { yaml, refMap } = buildA11yTree(root)
|
|
502
|
-
|
|
503
|
-
expect(Array.from(refMap.values())).toContain(card)
|
|
504
|
-
expect(yaml).toMatch(/#\d+.*cursor=pointer.*"[^"]*弹性云服务器 ECS[^"]*"/)
|
|
505
|
-
},
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
it(
|
|
509
|
-
'场景:可点击卡片内部子孙继承 cursor:pointer\n' +
|
|
510
|
-
'问题:若仅凭 computed cursor 判定,父容器可点击性会传染给全部子孙,产生大量误报 ref\n' +
|
|
511
|
-
'期望:只有自身声明 pointer 的卡片拿到 ref,仅继承 pointer 的内部子节点不再各自分配 ref',
|
|
512
|
-
() => {
|
|
513
|
-
const root = setupRoot(`
|
|
514
|
-
<div class="card-list">
|
|
515
|
-
<div id="resource-card" class="card-wrapper">
|
|
516
|
-
<div id="inner-info" class="info">
|
|
517
|
-
<div class="info-title">弹性云服务器 ECS</div>
|
|
518
|
-
</div>
|
|
519
|
-
</div>
|
|
520
|
-
</div>
|
|
521
|
-
`)
|
|
522
|
-
const card = root.querySelector('#resource-card') as HTMLElement
|
|
523
|
-
const inner = root.querySelector('#inner-info') as HTMLElement
|
|
524
|
-
// 卡片与子节点的 computed cursor 都是 pointer(模拟继承),但只有卡片是"自身声明"边界
|
|
525
|
-
card.style.cursor = 'pointer'
|
|
526
|
-
inner.style.cursor = 'pointer'
|
|
527
|
-
|
|
528
|
-
const { refMap } = buildA11yTree(root)
|
|
529
|
-
const refElements = Array.from(refMap.values())
|
|
530
|
-
|
|
531
|
-
expect(refElements).toContain(card)
|
|
532
|
-
expect(refElements).not.toContain(inner)
|
|
533
|
-
},
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
it(
|
|
537
|
-
'场景:无 name 的 tp-icon.common-icon 箭头图标(父级 cursor=auto,自身 cursor=pointer)\n' +
|
|
538
|
-
'问题:虽是真正可点击边界,但因 role=generic 且无 name 被漏判\n' +
|
|
539
|
-
'期望:tp-icon 分配 ref,并通过 exposedAttributes 输出 data-qa-id token',
|
|
540
|
-
() => {
|
|
541
|
-
const root = setupRoot(`
|
|
542
|
-
<div class="ti-container-head">
|
|
543
|
-
<span class="suite-icon-container">
|
|
544
|
-
<span>
|
|
545
|
-
<tp-icon class="common-icon" data-qa-id="ecm.ecs-dashboard.b" id="arrow-icon">
|
|
546
|
-
<svg><path d="M15 11"/></svg>
|
|
547
|
-
</tp-icon>
|
|
548
|
-
</span>
|
|
549
|
-
</span>
|
|
550
|
-
</div>
|
|
551
|
-
`)
|
|
552
|
-
const icon = root.querySelector('#arrow-icon') as HTMLElement
|
|
553
|
-
icon.style.cursor = 'pointer'
|
|
554
|
-
|
|
555
|
-
const { yaml, refMap } = buildA11yTree(root, {
|
|
556
|
-
roles: [{ role: 'button', selector: 'tp-icon.common-icon' }],
|
|
557
|
-
exposedAttributes: ['data-qa-id'],
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
expect(Array.from(refMap.values())).toContain(icon)
|
|
561
|
-
expect(yaml).toMatch(/button #\d+.*cursor=pointer.*data-qa-id=.*ecm\.ecs-dashboard\.b/)
|
|
562
|
-
},
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
it(
|
|
566
|
-
'场景:普通静态 generic 容器(无 cursor:pointer)\n' +
|
|
567
|
-
'期望:不因本次改动误判为可交互(无 ref)',
|
|
568
|
-
() => {
|
|
569
|
-
const root = setupRoot(`
|
|
570
|
-
<div class="plain-card">
|
|
571
|
-
<div class="title">纯展示卡片</div>
|
|
572
|
-
</div>
|
|
573
|
-
`)
|
|
574
|
-
const plain = root.querySelector('.plain-card') as HTMLElement
|
|
575
|
-
|
|
576
|
-
const { refMap } = buildA11yTree(root)
|
|
577
|
-
expect(Array.from(refMap.values())).not.toContain(plain)
|
|
578
|
-
},
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
it(
|
|
582
|
-
'场景(华为云控制台实测):卡片的 cursor:pointer 定义在 :hover 伪类上\n' +
|
|
583
|
-
'问题:静止态 getComputedStyle 读到 auto,仅凭 computed cursor 会漏判这类可点击卡片\n' +
|
|
584
|
-
'期望:扫描样式表识别 :hover 手势,卡片自身分配 ref;仅继承的子孙不分配',
|
|
585
|
-
() => {
|
|
586
|
-
const root = setupRoot(`
|
|
587
|
-
<style>
|
|
588
|
-
.container-wrapper .shadow:hover { cursor: pointer; }
|
|
589
|
-
</style>
|
|
590
|
-
<div class="container-wrapper">
|
|
591
|
-
<div id="resource-card" class="shadow card-wrapper">
|
|
592
|
-
<span class="mr20 image-ECS"></span>
|
|
593
|
-
<div class="info">
|
|
594
|
-
<div class="info-title">弹性云服务器 ECS</div>
|
|
595
|
-
<div class="info-content">
|
|
596
|
-
<span class="instance-number">1</span>
|
|
597
|
-
<span class="service-name">云服务器</span>
|
|
598
|
-
</div>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
</div>
|
|
602
|
-
`)
|
|
603
|
-
const card = root.querySelector('#resource-card') as HTMLElement
|
|
604
|
-
const info = root.querySelector('.info') as HTMLElement
|
|
605
|
-
|
|
606
|
-
const { yaml, refMap } = buildA11yTree(root)
|
|
607
|
-
const refElements = Array.from(refMap.values())
|
|
608
|
-
|
|
609
|
-
expect(refElements).toContain(card)
|
|
610
|
-
expect(refElements).not.toContain(info)
|
|
611
|
-
expect(yaml).toMatch(/#\d+.*"[^"]*弹性云服务器 ECS[^"]*"/)
|
|
612
|
-
},
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
it(
|
|
616
|
-
'场景::focus 伪类下声明 cursor:pointer\n' +
|
|
617
|
-
'期望:同样识别为可点击(覆盖 focus/active 等交互态手势)',
|
|
618
|
-
() => {
|
|
619
|
-
const root = setupRoot(`
|
|
620
|
-
<style>
|
|
621
|
-
.focusable-tile:focus { cursor: pointer; }
|
|
622
|
-
</style>
|
|
623
|
-
<div class="focusable-tile">可聚焦磁贴</div>
|
|
624
|
-
`)
|
|
625
|
-
const tile = root.querySelector('.focusable-tile') as HTMLElement
|
|
626
|
-
|
|
627
|
-
const { refMap } = buildA11yTree(root)
|
|
628
|
-
expect(Array.from(refMap.values())).toContain(tile)
|
|
629
|
-
},
|
|
630
|
-
)
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
/** 统计某 accessible name 是否在多行 YAML 中重复出现(父子同名的典型信号) */
|
|
634
|
-
function yamlHasDuplicateAccessibleName(lines: string[], name: string): boolean {
|
|
635
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
636
|
-
const re = new RegExp(`"${escaped}"`)
|
|
637
|
-
return lines.filter((l) => re.test(l)).length > 1
|
|
638
|
-
}
|