@opentiny/next-sdk 0.4.0 → 0.4.2

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.
Files changed (149) hide show
  1. package/agent/AgentModelProvider.ts +78 -81
  2. package/agent/type.ts +6 -9
  3. package/agent/utils/getAISDKTools.ts +0 -1
  4. package/agent/utils/getBuiltinMcpTools.ts +7 -7
  5. package/core.ts +0 -3
  6. package/dist/SimulatorMask-BHVXyogh-CARX3Rff.js +361 -0
  7. package/dist/agent/type.d.ts +4 -12
  8. package/dist/agent/utils/getBuiltinMcpTools.d.ts +3 -3
  9. package/dist/core.d.ts +0 -1
  10. package/dist/core.js +16 -17
  11. package/dist/index-R_HIbfUX.js +6604 -0
  12. package/dist/index.d.ts +11 -3
  13. package/dist/index.js +76 -4969
  14. package/dist/{initialize-builtin-WebMCP-HgObT902.js → initialize-builtin-WebMCP-JaoKwVlm.js} +1156 -1037
  15. package/dist/page-tools/a11y/build.d.ts +10 -0
  16. package/dist/page-tools/a11y/config.d.ts +96 -0
  17. package/dist/page-tools/a11y/constants.d.ts +11 -0
  18. package/dist/page-tools/a11y/search.d.ts +17 -0
  19. package/dist/page-tools/a11y/types.d.ts +95 -0
  20. package/dist/page-tools/a11y/utils.d.ts +55 -0
  21. package/dist/page-tools/a11y/vnode.d.ts +40 -0
  22. package/dist/page-tools/a11y-tree.d.ts +9 -99
  23. package/dist/page-tools/configs/console-cloud.d.ts +6 -0
  24. package/dist/page-tools/constants.d.ts +10 -0
  25. package/dist/page-tools/context.d.ts +40 -0
  26. package/dist/page-tools/handlers/browserState.d.ts +8 -0
  27. package/dist/page-tools/handlers/click.d.ts +8 -0
  28. package/dist/page-tools/handlers/executeJavascript.d.ts +8 -0
  29. package/dist/page-tools/handlers/fill.d.ts +8 -0
  30. package/dist/page-tools/handlers/scroll.d.ts +8 -0
  31. package/dist/page-tools/handlers/searchTree.d.ts +9 -0
  32. package/dist/page-tools/handlers/select.d.ts +8 -0
  33. package/dist/page-tools/page-agent-highlight/index.d.ts +21 -0
  34. package/dist/page-tools/page-agent-mask/SimulatorMask.d.ts +16 -0
  35. package/dist/page-tools/page-agent-mask/checkDarkMode.d.ts +5 -0
  36. package/dist/page-tools/page-agent-tool-event.d.ts +26 -0
  37. package/dist/page-tools/page-agent-tool.d.ts +3 -8
  38. package/dist/page-tools/schema.d.ts +44 -0
  39. package/dist/page-tools/tool-config.d.ts +50 -0
  40. package/dist/page-tools/utils/dom.d.ts +6 -0
  41. package/dist/page-tools/utils/scroll.d.ts +15 -0
  42. package/dist/runtime.d.ts +7 -0
  43. package/dist/runtime.js +732 -0
  44. package/dist/utils/builtinProxy.d.ts +1 -1
  45. package/dist/vitest.config.d.ts +2 -0
  46. package/index.ts +35 -5
  47. package/package.json +23 -29
  48. package/page-tools/a11y/build.ts +74 -0
  49. package/page-tools/a11y/config.ts +465 -0
  50. package/page-tools/a11y/constants.ts +131 -0
  51. package/page-tools/a11y/search.ts +127 -0
  52. package/page-tools/a11y/types.ts +105 -0
  53. package/page-tools/a11y/utils.ts +239 -0
  54. package/page-tools/a11y/vnode.ts +439 -0
  55. package/page-tools/a11y-tree.ts +9 -527
  56. package/page-tools/bridge.ts +23 -3
  57. package/page-tools/configs/console-cloud.ts +172 -0
  58. package/page-tools/constants.ts +12 -0
  59. package/page-tools/context.ts +50 -0
  60. package/page-tools/handlers/browserState.ts +12 -0
  61. package/page-tools/handlers/click.ts +30 -0
  62. package/page-tools/handlers/executeJavascript.ts +22 -0
  63. package/page-tools/handlers/fill.ts +65 -0
  64. package/page-tools/handlers/scroll.ts +66 -0
  65. package/page-tools/handlers/searchTree.ts +27 -0
  66. package/page-tools/handlers/select.ts +39 -0
  67. package/page-tools/page-agent-highlight/index.ts +245 -0
  68. package/page-tools/page-agent-mask/SimulatorMask.module.css +14 -0
  69. package/page-tools/page-agent-mask/SimulatorMask.ts +299 -0
  70. package/page-tools/page-agent-mask/checkDarkMode.ts +181 -0
  71. package/page-tools/page-agent-mask/cursor-border.svg +3 -0
  72. package/page-tools/page-agent-mask/cursor-fill.svg +5 -0
  73. package/page-tools/page-agent-mask/cursor.module.css +70 -0
  74. package/page-tools/page-agent-mask/hauwei.svg +25 -0
  75. package/page-tools/page-agent-prompt.md +34 -18
  76. package/page-tools/page-agent-tool-event.ts +113 -0
  77. package/page-tools/page-agent-tool.ts +146 -162
  78. package/page-tools/schema.ts +52 -0
  79. package/page-tools/tool-config.ts +100 -0
  80. package/page-tools/utils/dom.ts +158 -0
  81. package/page-tools/utils/scroll.ts +58 -0
  82. package/runtime.ts +44 -0
  83. package/test/page-tools/a11y/build.test.ts +638 -0
  84. package/test/page-tools/a11y/config.test.ts +370 -0
  85. package/test/page-tools/configs/console-cloud.test.ts +168 -0
  86. package/test/page-tools/page-agent-highlight.test.ts +110 -0
  87. package/test/page-tools/page-agent-tool-dispatch.test.ts +208 -0
  88. package/test/page-tools/page-agent-tool.test.ts +102 -0
  89. package/test/page-tools/tool-config.test.ts +112 -0
  90. package/test/page-tools/utils/dom.test.ts +122 -0
  91. package/utils/builtinProxy.ts +45 -13
  92. package/vite.config.runtime.ts +22 -0
  93. package/vite.config.ts +52 -8
  94. package/vitest.config.ts +10 -0
  95. package/McpSdk.ts +0 -14
  96. package/WebAgent.ts +0 -5
  97. package/WebMcp.ts +0 -26
  98. package/Zod.ts +0 -1
  99. package/dist/McpSdk.d.ts +0 -14
  100. package/dist/SimulatorMask-BHVXyogh-BFEGpD5S.js +0 -1048
  101. package/dist/SimulatorMask-BHVXyogh-CCYbrb84.js +0 -801
  102. package/dist/WebAgent.d.ts +0 -5
  103. package/dist/WebMcp.d.ts +0 -23
  104. package/dist/Zod.d.ts +0 -1
  105. package/dist/index.es.dev.js +0 -59017
  106. package/dist/index.es.js +0 -46795
  107. package/dist/index.umd.dev.js +0 -60355
  108. package/dist/index.umd.js +0 -1248
  109. package/dist/mcpsdk@1.25.3.dev.js +0 -22780
  110. package/dist/mcpsdk@1.25.3.es.dev.js +0 -22778
  111. package/dist/mcpsdk@1.25.3.es.js +0 -16960
  112. package/dist/mcpsdk@1.25.3.js +0 -48
  113. package/dist/transport/ExtensionClientTransport.d.ts +0 -24
  114. package/dist/transport/ExtensionContentServerTransport.d.ts +0 -39
  115. package/dist/transport/ExtensionPageServerTransport.d.ts +0 -36
  116. package/dist/transport/messages.d.ts +0 -9
  117. package/dist/vite.config.mcpSdk.d.ts +0 -2
  118. package/dist/vite.config.webAgent.d.ts +0 -2
  119. package/dist/vite.config.webMcp.d.ts +0 -2
  120. package/dist/vite.config.webMcpFull.d.ts +0 -2
  121. package/dist/vite.config.zod.d.ts +0 -2
  122. package/dist/webagent.dev.js +0 -49360
  123. package/dist/webagent.es.dev.js +0 -49071
  124. package/dist/webagent.es.js +0 -39219
  125. package/dist/webagent.js +0 -642
  126. package/dist/webmcp-full.dev.js +0 -31336
  127. package/dist/webmcp-full.es.dev.js +0 -30283
  128. package/dist/webmcp-full.es.js +0 -22889
  129. package/dist/webmcp-full.js +0 -645
  130. package/dist/webmcp.dev.js +0 -9572
  131. package/dist/webmcp.es.dev.js +0 -8518
  132. package/dist/webmcp.es.js +0 -6727
  133. package/dist/webmcp.js +0 -602
  134. package/dist/zod@3.25.76.dev.js +0 -4037
  135. package/dist/zod@3.25.76.es.dev.js +0 -4033
  136. package/dist/zod@3.25.76.es.js +0 -2945
  137. package/dist/zod@3.25.76.js +0 -1
  138. package/transport/ExtensionClientTransport.ts +0 -100
  139. package/transport/ExtensionContentServerTransport.ts +0 -162
  140. package/transport/ExtensionPageServerTransport.ts +0 -149
  141. package/transport/messages.ts +0 -63
  142. package/vite-build-tsc.ts +0 -63
  143. package/vite-env.d.ts +0 -10
  144. package/vite.config.mcpSdk.ts +0 -28
  145. package/vite.config.webAgent.ts +0 -19
  146. package/vite.config.webMcp.ts +0 -40
  147. package/vite.config.webMcpFull.ts +0 -19
  148. package/vite.config.zod.ts +0 -23
  149. /package/dist/{vite-build-tsc.d.ts → vite.config.runtime.d.ts} +0 -0
@@ -0,0 +1,370 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import {
3
+ resolveA11yRole,
4
+ resolveA11yStates,
5
+ resolveA11yInfo,
6
+ mergeA11yConfig,
7
+ mergeA11yConfigs,
8
+ ensureResolvedA11yConfig,
9
+ isResolvedA11yConfig,
10
+ defineA11yConfig,
11
+ extractSelectors,
12
+ type A11yConfig,
13
+ } from '../../../page-tools/a11y/config'
14
+
15
+ function el(html: string): Element {
16
+ const wrapper = document.createElement('div')
17
+ wrapper.innerHTML = html.trim()
18
+ document.body.appendChild(wrapper)
19
+ return wrapper.firstElementChild as Element
20
+ }
21
+
22
+ beforeEach(() => {
23
+ document.body.innerHTML = ''
24
+ })
25
+
26
+ afterEach(() => {
27
+ document.body.innerHTML = ''
28
+ })
29
+
30
+ describe('resolveA11yRole', () => {
31
+ it('标签隐式角色:无规则时按 TAG_ROLE_MAP 推断', () => {
32
+ expect(resolveA11yRole(el('<button>btn</button>'))).toBe('button')
33
+ expect(resolveA11yRole(el('<a href="#">link</a>'))).toBe('link')
34
+ })
35
+
36
+ it('INPUT_TYPE_ROLE:不同 type 的 input 角色不同', () => {
37
+ expect(resolveA11yRole(el('<input type="checkbox" />'))).toBe('checkbox')
38
+ expect(resolveA11yRole(el('<input type="radio" />'))).toBe('radio')
39
+ expect(resolveA11yRole(el('<input type="text" />'))).toBe('textbox')
40
+ })
41
+
42
+ it('无匹配时兜底为 generic', () => {
43
+ expect(resolveA11yRole(el('<div></div>'))).toBe('generic')
44
+ })
45
+
46
+ it('显式 role 属性优先,且不会被普通规则覆盖', () => {
47
+ const target = el('<div role="dialog" class="tab-item"></div>')
48
+ const config = defineA11yConfig({ roles: [{ role: 'tab', selector: '.tab-item' }] })
49
+ expect(resolveA11yRole(target, config)).toBe('dialog')
50
+ })
51
+
52
+ it('自定义 selector 规则命中后赋予新角色', () => {
53
+ const target = el('<div class="tab-item"></div>')
54
+ const config = defineA11yConfig({ roles: [{ role: 'tab', selector: '.tab-item' }] })
55
+ expect(resolveA11yRole(target, config)).toBe('tab')
56
+ })
57
+
58
+ it('roles 的 selector 仅匹配自身,不把角色传染给子孙', () => {
59
+ const wrap = el('<div class="tab-item"><span class="label">总览</span></div>')
60
+ const config = defineA11yConfig({ roles: [{ role: 'tab', selector: '.tab-item' }] })
61
+ expect(resolveA11yRole(wrap, config)).toBe('tab')
62
+ expect(resolveA11yRole(wrap.querySelector('.label')!, config)).toBe('generic')
63
+ })
64
+
65
+ it('自定义 match 函数规则命中后赋予新角色', () => {
66
+ const target = el('<div data-kind="switcher"></div>')
67
+ const config = defineA11yConfig({
68
+ roles: [{ role: 'switch', match: (e) => e.getAttribute('data-kind') === 'switcher' }],
69
+ })
70
+ expect(resolveA11yRole(target, config)).toBe('switch')
71
+ })
72
+
73
+ it('force: true 时覆盖元素已有的显式 role', () => {
74
+ const target = el('<div role="button" class="tab-item"></div>')
75
+ const config = defineA11yConfig({ roles: [{ role: 'tab', selector: '.tab-item', force: true }] })
76
+ expect(resolveA11yRole(target, config)).toBe('tab')
77
+ })
78
+
79
+ it('多条规则按数组顺序命中第一条生效', () => {
80
+ const target = el('<div class="a b"></div>')
81
+ const config = defineA11yConfig({
82
+ roles: [
83
+ { role: 'first', selector: '.a' },
84
+ { role: 'second', selector: '.b' },
85
+ ],
86
+ })
87
+ expect(resolveA11yRole(target, config)).toBe('first')
88
+ })
89
+ })
90
+
91
+ describe('resolveA11yStates - 标准 ARIA 零配置检测', () => {
92
+ it('checked: aria-checked=true/false', () => {
93
+ expect(resolveA11yStates(el('<div role="checkbox" aria-checked="true"></div>'))).toContain('checked')
94
+ expect(resolveA11yStates(el('<div role="checkbox" aria-checked="false"></div>'))).toContain('unchecked')
95
+ })
96
+
97
+ it('checked: aria-checked=mixed 三态', () => {
98
+ expect(resolveA11yStates(el('<div role="checkbox" aria-checked="mixed"></div>'))).toContain('checked=mixed')
99
+ })
100
+
101
+ it('checked: 原生 checkbox/radio 的 checked 属性', () => {
102
+ const checkbox = el('<input type="checkbox" />') as HTMLInputElement
103
+ checkbox.checked = true
104
+ expect(resolveA11yStates(checkbox)).toContain('checked')
105
+
106
+ const radio = el('<input type="radio" />') as HTMLInputElement
107
+ expect(resolveA11yStates(radio)).toContain('unchecked')
108
+ })
109
+
110
+ it('checked: label[for] 关联的 checkbox 状态', () => {
111
+ const wrapper = document.createElement('div')
112
+ wrapper.innerHTML = '<input id="c1" type="checkbox" /><label for="c1">选项</label>'
113
+ document.body.appendChild(wrapper)
114
+ const input = wrapper.querySelector('#c1') as HTMLInputElement
115
+ input.checked = true
116
+ const label = wrapper.querySelector('label') as Element
117
+ expect(resolveA11yStates(label)).toContain('checked')
118
+ })
119
+
120
+ it('selected: aria-selected=true', () => {
121
+ expect(resolveA11yStates(el('<div role="tab" aria-selected="true"></div>'))).toContain('selected')
122
+ })
123
+
124
+ it('selected: 内置 class 启发式(如 is-active)', () => {
125
+ expect(resolveA11yStates(el('<button class="is-active">Tab1</button>'))).toContain('selected')
126
+ })
127
+
128
+ it('pressed: aria-pressed 三态', () => {
129
+ expect(resolveA11yStates(el('<button aria-pressed="true"></button>'))).toContain('pressed')
130
+ expect(resolveA11yStates(el('<button aria-pressed="mixed"></button>'))).toContain('pressed=mixed')
131
+ expect(resolveA11yStates(el('<button aria-pressed="false"></button>'))).not.toContain('pressed')
132
+ })
133
+
134
+ it('current: aria-current 各种取值', () => {
135
+ expect(resolveA11yStates(el('<a aria-current="true"></a>'))).toContain('current')
136
+ expect(resolveA11yStates(el('<a aria-current="page"></a>'))).toContain('current=page')
137
+ expect(resolveA11yStates(el('<a aria-current="step"></a>'))).toContain('current=step')
138
+ })
139
+
140
+ it('disabled: aria-disabled 与原生 disabled', () => {
141
+ expect(resolveA11yStates(el('<div aria-disabled="true"></div>'))).toContain('disabled')
142
+ const btn = el('<button disabled></button>') as HTMLButtonElement
143
+ expect(resolveA11yStates(btn)).toContain('disabled')
144
+ })
145
+
146
+ it('expanded: aria-expanded=true', () => {
147
+ expect(resolveA11yStates(el('<button aria-expanded="true"></button>'))).toContain('expanded')
148
+ })
149
+
150
+ it('hasPopup: aria-haspopup 非 false 值', () => {
151
+ expect(resolveA11yStates(el('<button aria-haspopup="menu"></button>'))).toContain('hasPopup')
152
+ expect(resolveA11yStates(el('<button aria-haspopup="false"></button>'))).not.toContain('hasPopup')
153
+ })
154
+
155
+ it('invalid: aria-invalid 取值', () => {
156
+ expect(resolveA11yStates(el('<input aria-invalid="true" />'))).toContain('invalid')
157
+ expect(resolveA11yStates(el('<input aria-invalid="spelling" />'))).toContain('invalid=spelling')
158
+ })
159
+
160
+ it('readonly: aria-readonly 与原生 readOnly', () => {
161
+ expect(resolveA11yStates(el('<input aria-readonly="true" />'))).toContain('readonly')
162
+ const input = el('<input />') as HTMLInputElement
163
+ input.readOnly = true
164
+ expect(resolveA11yStates(input)).toContain('readonly')
165
+ })
166
+
167
+ it('required: aria-required 与原生 required', () => {
168
+ expect(resolveA11yStates(el('<input aria-required="true" />'))).toContain('required')
169
+ const input = el('<input />') as HTMLInputElement
170
+ input.required = true
171
+ expect(resolveA11yStates(input)).toContain('required')
172
+ })
173
+
174
+ it('busy: aria-busy=true', () => {
175
+ expect(resolveA11yStates(el('<div aria-busy="true"></div>'))).toContain('busy')
176
+ })
177
+
178
+ it('orientation: aria-orientation', () => {
179
+ expect(resolveA11yStates(el('<div aria-orientation="vertical"></div>'))).toContain('orientation=vertical')
180
+ })
181
+
182
+ it('sort: aria-sort,排除 none', () => {
183
+ // 用 div + role="columnheader" 代替裸 <th>,避免 innerHTML 解析时因缺少 <table>/<tr> 上下文被浏览器/jsdom 丢弃
184
+ expect(resolveA11yStates(el('<div role="columnheader" aria-sort="ascending"></div>'))).toContain('sort=ascending')
185
+ expect(resolveA11yStates(el('<div role="columnheader" aria-sort="none"></div>'))).not.toContain('sort=none')
186
+ })
187
+
188
+ it('multiselectable: aria-multiselectable=true', () => {
189
+ expect(resolveA11yStates(el('<div aria-multiselectable="true"></div>'))).toContain('multiselectable')
190
+ })
191
+
192
+ it('valuenow/valuetext', () => {
193
+ const tokens = resolveA11yStates(el('<div aria-valuenow="50" aria-valuetext="中"></div>'))
194
+ expect(tokens).toContain('valuenow="50"')
195
+ expect(tokens).toContain('valuetext="中"')
196
+ })
197
+
198
+ it('heading level:h1-h6 与 aria-level', () => {
199
+ expect(resolveA11yStates(el('<h2>标题</h2>'))).toContain('level=2')
200
+ expect(resolveA11yStates(el('<div role="heading" aria-level="3"></div>'))).toContain('level=3')
201
+ })
202
+
203
+ it('cursor=pointer', () => {
204
+ const target = el('<div></div>') as HTMLElement
205
+ target.style.cursor = 'pointer'
206
+ expect(resolveA11yStates(target)).toContain('cursor=pointer')
207
+ })
208
+
209
+ it('value="":input/textarea/select 有值时输出', () => {
210
+ const input = el('<input />') as HTMLInputElement
211
+ input.value = 'hello'
212
+ expect(resolveA11yStates(input)).toContain('value="hello"')
213
+ })
214
+
215
+ it('target:a[target] 输出原生属性值', () => {
216
+ expect(resolveA11yStates(el('<a target="_blank"></a>'))).toContain('target=_blank')
217
+ expect(resolveA11yStates(el('<a target="_self"></a>'))).toContain('target=_self')
218
+ expect(resolveA11yStates(el('<a></a>'))).not.toContain('target=_blank')
219
+ })
220
+ })
221
+
222
+ describe('resolveA11yStates - 自定义规则', () => {
223
+ it('states.<标准名> 用 selector 命中标准状态名', () => {
224
+ const target = el('<button class="btn is-checked"></button>')
225
+ const config = defineA11yConfig({ states: { selected: { selector: '.is-checked' } } })
226
+ expect(resolveA11yStates(target, config)).toContain('selected')
227
+ })
228
+
229
+ it('states 的 selector 用 closest,子孙可继承容器选中态', () => {
230
+ const wrap = el('<li class="ti3-tab-li ti3-tab-active"><span class="ti3-tabs-text">总览</span></li>')
231
+ const config = defineA11yConfig({ states: { selected: { selector: 'li.ti3-tab-li.ti3-tab-active' } } })
232
+ expect(resolveA11yStates(wrap.querySelector('.ti3-tabs-text')!, config)).toContain('selected')
233
+ })
234
+
235
+ it('默认配置:Tiny3 按钮组 ti3-active 在容器上时,内部 button 仍输出 selected', () => {
236
+ const wrap = el(
237
+ '<div class="ti3-btn-item-container ti3-active"><button class="ti3-btn-item ti3-btn-item-radio" aria-label="竞价计费">竞价计费</button></div>',
238
+ )
239
+ const btn = wrap.querySelector('button')!
240
+ expect(resolveA11yStates(btn)).toContain('selected')
241
+ // 未激活的兄弟按钮不应误标 selected
242
+ const inactive = el(
243
+ '<div class="ti3-btn-item-container"><button class="ti3-btn-item ti3-btn-item-radio">按需计费</button></div>',
244
+ )
245
+ expect(resolveA11yStates(inactive.querySelector('button')!)).not.toContain('selected')
246
+ })
247
+
248
+ it('states.<标准名> 用 match 函数命中', () => {
249
+ const target = el('<span class="form-tip--warn">警告文本</span>')
250
+ const config = defineA11yConfig({ states: { warning: { selector: '.form-tip--warn' } } })
251
+ expect(resolveA11yStates(target, config)).toContain('warning')
252
+ })
253
+
254
+ it('selector 支持字符串数组:命中数组中任意一个 class 均视为选中', () => {
255
+ const legacy = el('<button class="btn is-active"></button>')
256
+ const modern = el('<button class="btn is-checked"></button>')
257
+ const none = el('<button class="btn"></button>')
258
+ const config = defineA11yConfig({
259
+ states: { selected: { selector: ['.is-active', '.is-checked'] } },
260
+ })
261
+ expect(resolveA11yStates(legacy, config)).toContain('selected')
262
+ expect(resolveA11yStates(modern, config)).toContain('selected')
263
+ expect(resolveA11yStates(none, config)).not.toContain('selected')
264
+ })
265
+
266
+ it('error 与 warning 同时命中时只输出 error(优先级)', () => {
267
+ const target = el('<div class="is-error is-warning">错误</div>')
268
+ const config = defineA11yConfig({
269
+ states: {
270
+ error: { selector: '.is-error' },
271
+ warning: { selector: '.is-warning' },
272
+ },
273
+ })
274
+ const tokens = resolveA11yStates(target, config)
275
+ expect(tokens).toContain('error')
276
+ expect(tokens).not.toContain('warning')
277
+ })
278
+
279
+ it('命中非标准自定义状态名时直接输出同名 token', () => {
280
+ const target = el('<div class="highlight-row"></div>')
281
+ const config = defineA11yConfig({ states: { highlighted: { selector: '.highlight-row' } } })
282
+ expect(resolveA11yStates(target, config)).toContain('highlighted')
283
+ })
284
+
285
+ it('exposedAttributes:属性存在时输出属性 token,不存在时不输出', () => {
286
+ const withAttr = el('<div data-testid="foo"></div>')
287
+ const withoutAttr = el('<div></div>')
288
+ const config = defineA11yConfig({ exposedAttributes: ['data-testid'] })
289
+ expect(resolveA11yStates(withAttr, config)).toContain('data-testid="foo"')
290
+ const tokensWithout = resolveA11yStates(withoutAttr, config)
291
+ expect(tokensWithout.some((t) => t.includes('data-testid'))).toBe(false)
292
+ })
293
+ })
294
+
295
+ describe('resolveA11yInfo', () => {
296
+ it('整合 role 与 tokens 的返回结构', () => {
297
+ const target = el('<div class="tab-item" aria-selected="true"></div>')
298
+ const config = defineA11yConfig({ roles: [{ role: 'tab', selector: '.tab-item' }] })
299
+ const info = resolveA11yInfo(target, config)
300
+ expect(info.role).toBe('tab')
301
+ expect(info.tokens).toContain('selected')
302
+ })
303
+ })
304
+
305
+ describe('mergeA11yConfig / mergeA11yConfigs', () => {
306
+ it('默认值与用户配置按数组拼接,不丢默认规则', () => {
307
+ const merged = mergeA11yConfig({ roles: [{ role: 'tab', selector: '.tab' }] })
308
+ expect(merged.roles).toEqual([{ role: 'tab', selector: '.tab' }])
309
+ // 默认 states.error/warning/selected 规则仍然存在
310
+ expect(merged.states.error?.length).toBeGreaterThan(0)
311
+ expect(merged.states.warning?.length).toBeGreaterThan(0)
312
+ expect(merged.states.selected?.length).toBeGreaterThan(0)
313
+ })
314
+
315
+ it('states 按 key 独立合并', () => {
316
+ const merged = mergeA11yConfig({ states: { current: { selector: '.step-current' } } })
317
+ expect(merged.states.current).toEqual([{ selector: '.step-current' }])
318
+ expect(merged.states.error?.length).toBeGreaterThan(0)
319
+ })
320
+
321
+ it('未提供的字段回退默认空数组', () => {
322
+ const merged = mergeA11yConfig()
323
+ expect(merged.whitelist).toEqual([])
324
+ expect(merged.blacklist).toEqual([])
325
+ expect(merged.exposedAttributes).toEqual([])
326
+ })
327
+
328
+ it('mergeA11yConfigs 对两份任意 A11yConfig 做加法合并', () => {
329
+ const a: A11yConfig = { whitelist: ['.a'] }
330
+ const b: A11yConfig = { whitelist: ['.b'] }
331
+ const merged = mergeA11yConfigs(a, b)
332
+ expect(merged.whitelist).toEqual(['.a', '.b'])
333
+ })
334
+
335
+ it('已 Resolved 的配置再次 mergeA11yConfig / ensureResolved 保持同引用且规则不翻倍', () => {
336
+ const once = mergeA11yConfig({ roles: [{ role: 'tab', selector: '.tab' }] })
337
+ const selectedLen = once.states.selected!.length
338
+ expect(isResolvedA11yConfig(once)).toBe(true)
339
+ expect(mergeA11yConfig(once)).toBe(once)
340
+ expect(ensureResolvedA11yConfig(once)).toBe(once)
341
+ expect(once.states.selected!.length).toBe(selectedLen)
342
+ // 未标记的普通 A11yConfig 仍会与默认合并
343
+ const raw: A11yConfig = { roles: [{ role: 'tab', selector: '.tab' }] }
344
+ expect(isResolvedA11yConfig(raw)).toBe(false)
345
+ expect(ensureResolvedA11yConfig(raw)).not.toBe(raw)
346
+ })
347
+ })
348
+
349
+ describe('defineA11yConfig', () => {
350
+ it('恒等返回', () => {
351
+ const config: A11yConfig = { roles: [{ role: 'tab', selector: '.tab' }] }
352
+ expect(defineA11yConfig(config)).toBe(config)
353
+ })
354
+ })
355
+
356
+ describe('extractSelectors', () => {
357
+ it('从规则数组中提取纯选择器,忽略只有 match 的规则', () => {
358
+ const selectors = extractSelectors([{ selector: '.a' }, { match: () => true }, { selector: '.b' }])
359
+ expect(selectors).toEqual(['.a', '.b'])
360
+ })
361
+
362
+ it('数组类型的 selector 会被展开为独立的选择器字符串', () => {
363
+ const selectors = extractSelectors([{ selector: ['.a', '.b'] }, { selector: '.c' }])
364
+ expect(selectors).toEqual(['.a', '.b', '.c'])
365
+ })
366
+
367
+ it('无规则时返回空数组', () => {
368
+ expect(extractSelectors(undefined)).toEqual([])
369
+ })
370
+ })
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { resolveA11yInfo } from '../../../page-tools/a11y/config'
3
+ import {
4
+ consoleCloudPageAgentToolOptions,
5
+ isConsoleCloudHost,
6
+ } from '../../../page-tools/configs/console-cloud'
7
+ import { buildA11yTree } from '../../../page-tools/a11y-tree'
8
+ import { collectTitleLabel } from '../../../page-tools/a11y/utils'
9
+
10
+ function el(html: string): Element {
11
+ const wrapper = document.createElement('div')
12
+ wrapper.innerHTML = html.trim()
13
+ document.body.appendChild(wrapper)
14
+ return wrapper.firstElementChild as Element
15
+ }
16
+
17
+ function setupRoot(html: string): HTMLElement {
18
+ const root = document.createElement('div')
19
+ root.innerHTML = html.trim()
20
+ document.body.appendChild(root)
21
+ return root
22
+ }
23
+
24
+ beforeEach(() => {
25
+ document.body.innerHTML = ''
26
+ })
27
+
28
+ afterEach(() => {
29
+ document.body.innerHTML = ''
30
+ })
31
+
32
+ const a11yConfig = consoleCloudPageAgentToolOptions.a11yConfig!
33
+
34
+ describe('isConsoleCloudHost', () => {
35
+ it('匹配控制台主域与子控制台,且要求 console 前为域名边界', () => {
36
+ expect(isConsoleCloudHost('console.huaweicloud.com')).toBe(true)
37
+ expect(isConsoleCloudHost('ecs.console.huaweicloud.com')).toBe(true)
38
+ expect(isConsoleCloudHost('www.huaweicloud.com')).toBe(false)
39
+ expect(isConsoleCloudHost('www.baidu.com')).toBe(false)
40
+ expect(isConsoleCloudHost('xconsole.huaweicloud.com')).toBe(false)
41
+ })
42
+ })
43
+
44
+ describe('consoleCloudPageAgentToolOptions', () => {
45
+ it('导出完整 PageAgentToolOptions,并暴露 cf-uba / data-qa-id / name', () => {
46
+ expect(consoleCloudPageAgentToolOptions.enableHighlight).toBe(false)
47
+ expect(a11yConfig.exposedAttributes).toContain('cf-uba')
48
+ expect(a11yConfig.exposedAttributes).toContain('data-qa-id')
49
+ expect(a11yConfig.exposedAttributes).toContain('name')
50
+ })
51
+
52
+ it('data-qa-id 仅输出属性 token,不自动赋予 ref', () => {
53
+ const root = setupRoot(`
54
+ <div id="page-root">
55
+ <div data-qa-id="static-marker">静态文案</div>
56
+ </div>
57
+ `)
58
+ const container = root.querySelector('#page-root') as Element
59
+ const staticEl = root.querySelector('[data-qa-id="static-marker"]') as Element
60
+ const info = resolveA11yInfo(staticEl, a11yConfig)
61
+ expect(info.tokens).toContain('data-qa-id="static-marker"')
62
+ const { refMap } = buildA11yTree(container, a11yConfig)
63
+ expect(Array.from(refMap.values())).not.toContain(staticEl)
64
+ })
65
+
66
+ it('Tiny3 Tab:.ti3-tabs-text → tab,选中态随父 li.ti3-tab-active', () => {
67
+ const wrap = el(
68
+ '<ul class="ti3-tabs"><li class="ti3-tab-li ti3-tab-active"><span class="ti3-tabs-text" tabindex="0">总览</span></li></ul>',
69
+ )
70
+ const tab = wrap.querySelector('.ti3-tabs-text')!
71
+ const info = resolveA11yInfo(tab, a11yConfig)
72
+ expect(info.role).toBe('tab')
73
+ expect(info.tokens).toContain('selected')
74
+ // li 本身不再标成 tab,避免与内部可聚焦节点双重角色
75
+ expect(resolveA11yInfo(wrap.querySelector('li')!, a11yConfig).role).toBe('listitem')
76
+ })
77
+
78
+ it('Tiny3 Tablist:ul.ti3-tabs → tablist,且不把角色传染给子节点', () => {
79
+ const list = el('<ul class="ti3-tabs"><li class="ti3-tab-li"><span class="ti3-tabs-text">A</span></li></ul>')
80
+ expect(resolveA11yInfo(list, a11yConfig).role).toBe('tablist')
81
+ const child = list.querySelector('li')!
82
+ expect(resolveA11yInfo(child, a11yConfig).role).not.toBe('tablist')
83
+ expect(resolveA11yInfo(list.querySelector('.ti3-tabs-text')!, a11yConfig).role).toBe('tab')
84
+ })
85
+
86
+ it('服务列表图标容器 → button + hasPopup', () => {
87
+ const btn = el('<div class="modules-service-list-menu-service-icon-container" cf-uba="serviceList..open"></div>')
88
+ const info = resolveA11yInfo(btn, a11yConfig)
89
+ expect(info.role).toBe('button')
90
+ expect(info.tokens).toContain('hasPopup')
91
+ expect(info.tokens.some((t) => t.includes('cf-uba='))).toBe(true)
92
+ })
93
+
94
+ it(
95
+ '场景:帮助中心头部 ti-icon / tp-icon(固定/全屏/关闭)\n' +
96
+ '问题:自定义图标标签无 button 语义,Agent 无法点击固定/全屏/关闭\n' +
97
+ '期望:识别为 button;有 name 属性的图标输出 name token,正文可为空',
98
+ () => {
99
+ const root = el(`
100
+ <div class="ti-global-help-panel-header-right">
101
+ <tp-icon name="cloudx-action-fixed" class="ti-global-help-panel-header-icon"></tp-icon>
102
+ <ti-icon class="ti-global-help-panel-header-icon ti3-icon-full-screen ti3-icon"></ti-icon>
103
+ <ti-icon name="close" class="ti-global-help-panel-header-icon ti-global-help-panel-close ti3-icon-close ti3-icon"></ti-icon>
104
+ </div>
105
+ `)
106
+
107
+ const fixed = root.querySelector('tp-icon')!
108
+ const fullScreen = root.querySelector('ti-icon.ti3-icon-full-screen')!
109
+ const close = root.querySelector('ti-icon[name="close"]')!
110
+
111
+ expect(resolveA11yInfo(fixed, a11yConfig).role).toBe('button')
112
+ expect(resolveA11yInfo(fullScreen, a11yConfig).role).toBe('button')
113
+ expect(resolveA11yInfo(close, a11yConfig).role).toBe('button')
114
+
115
+ const { yaml, refMap, lines } = buildA11yTree(root, a11yConfig)
116
+ expect(refMap.size).toBeGreaterThanOrEqual(3)
117
+ expect(Array.from(refMap.values())).toEqual(expect.arrayContaining([fixed, fullScreen, close]))
118
+ expect(lines.some((l) => /button.*\[.*name=.*cloudx-action-fixed/.test(l))).toBe(true)
119
+ expect(lines.some((l) => /button.*\[.*name=.*close/.test(l))).toBe(true)
120
+ expect(lines.some((l) => /button.*\[.*name=full-screen/.test(l))).toBe(false)
121
+ expect(lines.some((l) => /button.*"cloudx-action-fixed"/.test(l))).toBe(false)
122
+ expect(lines.some((l) => /button.*"full-screen"/.test(l))).toBe(false)
123
+ expect(lines.some((l) => /button.*"close"/.test(l))).toBe(false)
124
+ expect(yaml).not.toMatch(/@font-face/i)
125
+ },
126
+ )
127
+
128
+ it('区域选择触发器 → combobox', () => {
129
+ const box = el('<div class="ti3-select-dominator-container"><span>华北-北京四</span></div>')
130
+ expect(resolveA11yInfo(box, a11yConfig).role).toBe('combobox')
131
+ })
132
+
133
+ it('服务列表侧栏项 → menuitem + selected', () => {
134
+ const item = el(
135
+ '<ul class="components-service-list-left-box-sidebar-visit-panel"><li class="components-service-list-left-box-active">收藏和访问</li></ul>',
136
+ )
137
+ const li = item.querySelector('li')!
138
+ const info = resolveA11yInfo(li, a11yConfig)
139
+ expect(info.role).toBe('menuitem')
140
+ expect(info.tokens).toContain('selected')
141
+ })
142
+ })
143
+
144
+ describe('collectTitleLabel', () => {
145
+ it('优先取自身 title,否则取子孙 title', () => {
146
+ expect(collectTitleLabel(el('<div title="自身"></div>'))).toBe('自身')
147
+ expect(
148
+ collectTitleLabel(el('<div class="icon"><span title="打开服务列表"></span></div>')),
149
+ ).toBe('打开服务列表')
150
+ })
151
+ })
152
+
153
+ describe('consoleCloud 图标动作标识', () => {
154
+ it('ti-icon / tp-icon:name 属性经 exposedAttributes 输出 token', () => {
155
+ expect(resolveA11yInfo(el('<tp-icon name="cloudx-action-fixed"></tp-icon>'), a11yConfig).tokens).toContain(
156
+ 'name="cloudx-action-fixed"',
157
+ )
158
+ expect(resolveA11yInfo(el('<ti-icon name="close" class="ti3-icon-close"></ti-icon>'), a11yConfig).tokens).toContain(
159
+ 'name="close"',
160
+ )
161
+ expect(
162
+ resolveA11yInfo(
163
+ el('<ti-icon class="ti-global-help-panel-header-icon ti3-icon-full-screen ti3-icon"></ti-icon>'),
164
+ a11yConfig,
165
+ ).tokens,
166
+ ).not.toContain('name=full-screen')
167
+ })
168
+ })
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { resolveHighlightRect } from '../../page-tools/page-agent-highlight'
3
+
4
+ const originalGetComputedStyle = window.getComputedStyle
5
+
6
+ const originalInnerWidth = Object.getOwnPropertyDescriptor(window, 'innerWidth')
7
+ const originalInnerHeight = Object.getOwnPropertyDescriptor(window, 'innerHeight')
8
+
9
+ beforeEach(() => {
10
+ document.body.innerHTML = ''
11
+ })
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = ''
15
+ Object.defineProperty(window, 'getComputedStyle', {
16
+ configurable: true,
17
+ value: originalGetComputedStyle,
18
+ })
19
+ if (originalInnerWidth) Object.defineProperty(window, 'innerWidth', originalInnerWidth)
20
+ if (originalInnerHeight) Object.defineProperty(window, 'innerHeight', originalInnerHeight)
21
+ })
22
+
23
+ describe('resolveHighlightRect', () => {
24
+ it(
25
+ '场景:ti-icon 宿主 height=0(字形在 ::before),外包有 28×28 可点父节点\n' +
26
+ '问题:旧高亮逻辑跳过 width/height 为 0 的节点,帮助中心 full-screen/close 有 ref 却无框\n' +
27
+ '期望:回退到紧凑父节点矩形',
28
+ () => {
29
+ const wrap = document.createElement('div')
30
+ wrap.style.cssText = 'width:28px;height:28px;position:absolute;left:100px;top:80px;'
31
+ const icon = document.createElement('ti-icon') as HTMLElement
32
+ icon.className = 'ti-global-help-panel-header-icon ti3-icon-full-screen ti3-icon'
33
+ icon.style.cssText = 'width:20px;height:0;display:block;font-size:20px;'
34
+ wrap.appendChild(icon)
35
+ document.body.appendChild(wrap)
36
+
37
+ // jsdom 对 getBoundingClientRect 支持有限,手动 stub
38
+ icon.getBoundingClientRect = () =>
39
+ ({ top: 80.5, left: 964, width: 20, height: 0, bottom: 80.5, right: 984, x: 964, y: 80.5, toJSON() {} }) as DOMRect
40
+ wrap.getBoundingClientRect = () =>
41
+ ({ top: 70, left: 960, width: 28, height: 28, bottom: 98, right: 988, x: 960, y: 70, toJSON() {} }) as DOMRect
42
+
43
+ const rect = resolveHighlightRect(icon)
44
+ expect(rect).toEqual({ top: 70, left: 960, width: 28, height: 28 })
45
+ },
46
+ )
47
+
48
+ it('自身有宽高时直接使用自身矩形,不误用父节点', () => {
49
+ const el = document.createElement('button')
50
+ document.body.appendChild(el)
51
+ el.getBoundingClientRect = () =>
52
+ ({ top: 10, left: 20, width: 40, height: 24, bottom: 34, right: 60, x: 20, y: 10, toJSON() {} }) as DOMRect
53
+
54
+ expect(resolveHighlightRect(el)).toEqual({ top: 10, left: 20, width: 40, height: 24 })
55
+ })
56
+
57
+ it('宿主仅有宽度时用 font-size 补齐高度', () => {
58
+ const el = document.createElement('ti-icon') as HTMLElement
59
+ document.body.appendChild(el)
60
+ el.getBoundingClientRect = () =>
61
+ ({ top: 80, left: 100, width: 20, height: 0, bottom: 80, right: 120, x: 100, y: 80, toJSON() {} }) as DOMRect
62
+ Object.defineProperty(window, 'getComputedStyle', {
63
+ configurable: true,
64
+ value: () => ({ fontSize: '20px' }) as CSSStyleDeclaration,
65
+ })
66
+
67
+ const rect = resolveHighlightRect(el)
68
+ expect(rect).not.toBeNull()
69
+ expect(rect!.width).toBe(20)
70
+ expect(rect!.height).toBe(20)
71
+ })
72
+ })
73
+
74
+ describe('isRectInViewport(通过 highlight 间接验证)', () => {
75
+ it(
76
+ '场景:侧栏链接 left 略为负值,旧逻辑要求矩形完全落在视口内会整框跳过\n' +
77
+ '期望:与视口有交集即绘制高亮',
78
+ async () => {
79
+ const { highlight, unhighlight, HIGHLIGHT_CONTAINER_ID } = await import(
80
+ '../../page-tools/page-agent-highlight'
81
+ )
82
+ const el = document.createElement('a')
83
+ document.body.appendChild(el)
84
+ el.getBoundingClientRect = () =>
85
+ ({
86
+ top: 100,
87
+ left: -17,
88
+ width: 259,
89
+ height: 32,
90
+ bottom: 132,
91
+ right: 242,
92
+ x: -17,
93
+ y: 100,
94
+ toJSON() {},
95
+ }) as DOMRect
96
+
97
+ Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1100 })
98
+ Object.defineProperty(window, 'innerHeight', { configurable: true, value: 962 })
99
+
100
+ const refMap = new Map<number, HTMLElement>([[42, el]])
101
+ highlight(refMap)
102
+ const overlay = document.querySelector(
103
+ `#${HIGHLIGHT_CONTAINER_ID} [data-ref-index="42"]`,
104
+ ) as HTMLElement | null
105
+ expect(overlay).not.toBeNull()
106
+ expect(overlay!.style.left).toBe('-17px')
107
+ unhighlight()
108
+ },
109
+ )
110
+ })