@anweat/dsh-browser 0.1.17 → 0.2.0

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 (169) hide show
  1. package/README.md +266 -88
  2. package/assets/skills/dsh-browser/SKILL.md +120 -0
  3. package/assets/skills/dsh-browser/references/develop.md +48 -0
  4. package/assets/skills/dsh-browser/references/observe.md +91 -0
  5. package/assets/skills/dsh-browser/references/opencli.md +25 -0
  6. package/assets/skills/dsh-browser/references/recipes.md +149 -0
  7. package/assets/skills/dsh-browser/references/userscripts.md +44 -0
  8. package/cordis.patch.yml +11 -3
  9. package/lib/actions/act.d.ts +3 -0
  10. package/lib/actions/act.js +147 -0
  11. package/lib/actions/act.js.map +1 -0
  12. package/lib/actions/automation.d.ts +3 -0
  13. package/lib/actions/automation.js +356 -0
  14. package/lib/actions/automation.js.map +1 -0
  15. package/lib/actions/crawl.d.ts +3 -0
  16. package/lib/actions/crawl.js +28 -0
  17. package/lib/actions/crawl.js.map +1 -0
  18. package/lib/actions/errors.d.ts +53 -0
  19. package/lib/actions/errors.js +178 -0
  20. package/lib/actions/errors.js.map +1 -0
  21. package/lib/actions/index-view.d.ts +42 -0
  22. package/lib/actions/index-view.js +268 -0
  23. package/lib/actions/index-view.js.map +1 -0
  24. package/lib/actions/inspect.d.ts +3 -0
  25. package/lib/actions/inspect.js +35 -0
  26. package/lib/actions/inspect.js.map +1 -0
  27. package/lib/actions/observe.d.ts +3 -0
  28. package/lib/actions/observe.js +121 -0
  29. package/lib/actions/observe.js.map +1 -0
  30. package/lib/actions/opencli.d.ts +3 -0
  31. package/lib/actions/opencli.js +51 -0
  32. package/lib/actions/opencli.js.map +1 -0
  33. package/lib/actions/registry.d.ts +35 -0
  34. package/lib/actions/registry.js +89 -0
  35. package/lib/actions/registry.js.map +1 -0
  36. package/lib/actions/run.d.ts +34 -0
  37. package/lib/actions/run.js +226 -0
  38. package/lib/actions/run.js.map +1 -0
  39. package/lib/actions/runtime.d.ts +3 -0
  40. package/lib/actions/runtime.js +33 -0
  41. package/lib/actions/runtime.js.map +1 -0
  42. package/lib/actions/schema.d.ts +31 -0
  43. package/lib/actions/schema.js +309 -0
  44. package/lib/actions/schema.js.map +1 -0
  45. package/lib/actions/script.d.ts +3 -0
  46. package/lib/actions/script.js +97 -0
  47. package/lib/actions/script.js.map +1 -0
  48. package/lib/actions/shared.d.ts +31 -0
  49. package/lib/actions/shared.js +45 -0
  50. package/lib/actions/shared.js.map +1 -0
  51. package/lib/actions/surface.d.ts +22 -0
  52. package/lib/actions/surface.js +18 -0
  53. package/lib/actions/surface.js.map +1 -0
  54. package/lib/actions/target.d.ts +3 -0
  55. package/lib/actions/target.js +66 -0
  56. package/lib/actions/target.js.map +1 -0
  57. package/lib/actions/types.d.ts +181 -0
  58. package/lib/actions/types.js +48 -0
  59. package/lib/actions/types.js.map +1 -0
  60. package/lib/activation-rules.d.ts +29 -0
  61. package/lib/activation-rules.js +33 -0
  62. package/lib/activation-rules.js.map +1 -0
  63. package/lib/approval-policy.d.ts +40 -2
  64. package/lib/approval-policy.js +184 -97
  65. package/lib/approval-policy.js.map +1 -1
  66. package/lib/automation-assets-rpc.d.ts +8 -2
  67. package/lib/automation-assets-rpc.js +68 -10
  68. package/lib/automation-assets-rpc.js.map +1 -1
  69. package/lib/automation-assets.d.ts +152 -4
  70. package/lib/automation-assets.js +255 -15
  71. package/lib/automation-assets.js.map +1 -1
  72. package/lib/automation-convert.d.ts +23 -0
  73. package/lib/automation-convert.js +110 -0
  74. package/lib/automation-convert.js.map +1 -0
  75. package/lib/automation-development.d.ts +26 -0
  76. package/lib/automation-development.js +47 -0
  77. package/lib/automation-development.js.map +1 -1
  78. package/lib/automation-execution.d.ts +57 -1
  79. package/lib/automation-execution.js +167 -11
  80. package/lib/automation-execution.js.map +1 -1
  81. package/lib/automation-journal.d.ts +92 -0
  82. package/lib/automation-journal.js +449 -0
  83. package/lib/automation-journal.js.map +1 -0
  84. package/lib/automation-v2.d.ts +138 -0
  85. package/lib/automation-v2.js +631 -0
  86. package/lib/automation-v2.js.map +1 -0
  87. package/lib/automation.d.ts +82 -5
  88. package/lib/automation.js +239 -82
  89. package/lib/automation.js.map +1 -1
  90. package/lib/browser-service.d.ts +309 -32
  91. package/lib/browser-service.js +636 -182
  92. package/lib/browser-service.js.map +1 -1
  93. package/lib/client/SettingsCard.js +95 -57
  94. package/lib/client/SettingsCard.js.map +1 -1
  95. package/lib/client/asset-editor.d.ts +55 -0
  96. package/lib/client/asset-editor.js +101 -0
  97. package/lib/client/asset-editor.js.map +1 -0
  98. package/lib/client/automation-assets-client.d.ts +99 -2
  99. package/lib/client/automation-assets-client.js +243 -10
  100. package/lib/client/automation-assets-client.js.map +1 -1
  101. package/lib/client/form.d.ts +48 -4
  102. package/lib/client/form.js +142 -29
  103. package/lib/client/form.js.map +1 -1
  104. package/lib/client/index.d.ts +25 -2
  105. package/lib/client/index.js +5 -1
  106. package/lib/client/index.js.map +1 -1
  107. package/lib/client/locales.d.ts +85 -6
  108. package/lib/client/locales.js +64 -12
  109. package/lib/client/locales.js.map +1 -1
  110. package/lib/client/prompts-form.d.ts +67 -0
  111. package/lib/client/prompts-form.js +140 -0
  112. package/lib/client/prompts-form.js.map +1 -0
  113. package/lib/client/prompts-status-client.d.ts +34 -0
  114. package/lib/client/prompts-status-client.js +53 -0
  115. package/lib/client/prompts-status-client.js.map +1 -0
  116. package/lib/client/settings-namespace.d.ts +1 -1
  117. package/lib/client/settings-namespace.js +1 -1
  118. package/lib/client/settings-namespace.js.map +1 -1
  119. package/lib/client.js +1915 -330
  120. package/lib/client.js.map +1 -1
  121. package/lib/config.d.ts +50 -1
  122. package/lib/config.js +31 -1
  123. package/lib/config.js.map +1 -1
  124. package/lib/dialogs.d.ts +37 -0
  125. package/lib/dialogs.js +73 -0
  126. package/lib/dialogs.js.map +1 -0
  127. package/lib/freedom.d.ts +23 -7
  128. package/lib/freedom.js +48 -59
  129. package/lib/freedom.js.map +1 -1
  130. package/lib/index.d.ts +5 -13
  131. package/lib/index.js +51 -30
  132. package/lib/index.js.map +1 -1
  133. package/lib/journal.d.ts +109 -0
  134. package/lib/journal.js +238 -0
  135. package/lib/journal.js.map +1 -0
  136. package/lib/locator.d.ts +82 -0
  137. package/lib/locator.js +190 -0
  138. package/lib/locator.js.map +1 -0
  139. package/lib/observe-script.d.ts +27 -0
  140. package/lib/observe-script.js +565 -0
  141. package/lib/observe-script.js.map +1 -0
  142. package/lib/observe.d.ts +179 -0
  143. package/lib/observe.js +368 -0
  144. package/lib/observe.js.map +1 -0
  145. package/lib/policy-keys.d.ts +30 -0
  146. package/lib/policy-keys.js +34 -0
  147. package/lib/policy-keys.js.map +1 -0
  148. package/lib/prompt-limits.d.ts +30 -0
  149. package/lib/prompt-limits.js +31 -0
  150. package/lib/prompt-limits.js.map +1 -0
  151. package/lib/prompts.d.ts +131 -0
  152. package/lib/prompts.js +295 -0
  153. package/lib/prompts.js.map +1 -0
  154. package/lib/skill.d.ts +100 -0
  155. package/lib/skill.js +181 -0
  156. package/lib/skill.js.map +1 -0
  157. package/lib/tool-defs.d.ts +72 -0
  158. package/lib/tool-defs.js +67 -0
  159. package/lib/tool-defs.js.map +1 -0
  160. package/lib/tools.d.ts +19 -4
  161. package/lib/tools.js +124 -922
  162. package/lib/tools.js.map +1 -1
  163. package/package.json +15 -11
  164. package/scripts/check-host-rpc.mjs +11 -4
  165. package/scripts/dump-prompts.mjs +29 -0
  166. package/scripts/measure-tool-surface.mjs +190 -0
  167. package/lib/settings-scope.d.ts +0 -56
  168. package/lib/settings-scope.js +0 -51
  169. package/lib/settings-scope.js.map +0 -1
@@ -0,0 +1,565 @@
1
+ /**
2
+ * The in-page half of observe.read: one function, shipped to the page as source text.
3
+ *
4
+ * It is a raw string on purpose: it runs inside the page, so it must not carry transpiler helpers, and
5
+ * it must not use template literals (the string is written with String.raw). It reads the DOM and
6
+ * returns plain data; it never changes the page. The only thing it leaves behind is a WeakMap stored
7
+ * under a registered symbol on each scanned window, which maps each listed element to its number, so
8
+ * that the Node side can later prove that a candidate locator points at that very element.
9
+ *
10
+ * Coverage: the document, open shadow roots (recursively), and same-origin iframes (recursively, up to
11
+ * the frame-path limit). A cross-origin or sandboxed iframe is listed as a frame with its limitation,
12
+ * never entered. Closed shadow roots cannot be seen from page script and are not covered.
13
+ * @module dsh-browser/observe-script
14
+ */
15
+ /** Symbol (registered with Symbol.for) under which each scanned window keeps its element-number map. */
16
+ export const OBSERVE_SYMBOL = 'dsh.observe';
17
+ /** Maximum iframe nesting the scan enters; matches the locator framePath limit. */
18
+ export const OBSERVE_MAX_FRAME_DEPTH = 5;
19
+ /** Maximum number of iframes entered in one observation. */
20
+ export const OBSERVE_MAX_FRAMES = 20;
21
+ /** Reads, in the element's own window, the numbers of the elements a locator matched. */
22
+ export const IDENTITY_SOURCE = String.raw `(els) => {
23
+ const map = window[Symbol.for('dsh.observe')]
24
+ return els.map((el) => { const n = map ? map.get(el) : undefined; return typeof n === 'number' ? n : -1 })
25
+ }`;
26
+ /**
27
+ * `(root, opts) => result`. `root` is a Document or the Element an observation is scoped to.
28
+ * opts: { controls, links, tables, maxItems, includeValues, region?, maxFrameDepth, maxFrames }.
29
+ */
30
+ export const OBSERVE_SOURCE = String.raw `(root, opts) => {
31
+ const SYM = Symbol.for('dsh.observe')
32
+ const MAX = opts.maxItems
33
+ const out = { controls: [], links: [], tables: [], frames: [], framesOmitted: 0, counts: { controls: 0, links: 0, tables: 0 }, shadowRoots: 0 }
34
+ let nextId = 0
35
+ const items = []
36
+ const radioGroups = new Map()
37
+ const formIds = new WeakMap()
38
+ let nextForm = 0
39
+ const state = { framesEntered: 0 }
40
+
41
+ const norm = (s) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim()
42
+ const cut = (s, n) => { s = String(s); return s.length > n ? s.slice(0, n) : s }
43
+ const cssStr = (s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
44
+ const cssId = (s) => (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c)
45
+ const has = (el, a) => el.hasAttribute(a)
46
+ const attr = (el, a) => { const v = el.getAttribute(a); return v == null ? undefined : v }
47
+ const isEl = (n) => n && n.nodeType === 1
48
+
49
+ // ---- element numbering (identity proof for the Node side) ----
50
+ const numbered = (el, win) => {
51
+ let map = win[SYM]
52
+ if (!map) { map = new WeakMap(); try { Object.defineProperty(win, SYM, { value: map, configurable: true, enumerable: false, writable: true }) } catch (e) { win[SYM] = map } }
53
+ const id = nextId++
54
+ map.set(el, id)
55
+ return id
56
+ }
57
+
58
+ // ---- visibility and geometry ----
59
+ const rectOf = (el) => { try { return el.getBoundingClientRect() } catch (e) { return { left: 0, top: 0, width: 0, height: 0 } } }
60
+ const isVisible = (el) => {
61
+ try {
62
+ if (typeof el.checkVisibility === 'function' && !el.checkVisibility({ checkVisibilityCSS: true })) return false
63
+ const r = rectOf(el)
64
+ if (r.width > 0 && r.height > 0) return true
65
+ return false
66
+ } catch (e) { return false }
67
+ }
68
+ const inRegion = (el, frame) => {
69
+ if (!opts.region) return true
70
+ const r = rectOf(el)
71
+ const x = r.left + frame.ox, y = r.top + frame.oy
72
+ const g = opts.region
73
+ return x < g.x + g.width && x + r.width > g.x && y < g.y + g.height && y + r.height > g.y
74
+ }
75
+
76
+ // ---- roles, names ----
77
+ const INPUT_ROLE = { checkbox: 'checkbox', radio: 'radio', button: 'button', submit: 'button', reset: 'button', image: 'button', range: 'slider', number: 'spinbutton', search: 'searchbox', email: 'textbox', tel: 'textbox', url: 'textbox', text: 'textbox' }
78
+ const WIDGET_ROLES = new Set(['button', 'checkbox', 'radio', 'switch', 'combobox', 'textbox', 'searchbox', 'slider', 'spinbutton', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'tab', 'option', 'treeitem'])
79
+ const explicitRole = (el) => { const r = norm(attr(el, 'role') || '').split(' ')[0].toLowerCase(); return r || '' }
80
+ const typeOf = (el) => (el.localName === 'input' ? String(el.type || 'text').toLowerCase() : '')
81
+ const implicitRole = (el) => {
82
+ const tag = el.localName
83
+ if (tag === 'input') {
84
+ const t = typeOf(el)
85
+ if ((t === 'text' || t === 'search' || t === 'tel' || t === 'url' || t === 'email') && el.list) return 'combobox'
86
+ return INPUT_ROLE[t] || ''
87
+ }
88
+ if (tag === 'select') return (el.multiple || el.size > 1) ? 'listbox' : 'combobox'
89
+ if (tag === 'textarea') return 'textbox'
90
+ if (tag === 'button') return 'button'
91
+ if (tag === 'a' && has(el, 'href')) return 'link'
92
+ if (tag === 'table') return 'table'
93
+ return ''
94
+ }
95
+ const idText = (el, ids) => {
96
+ const rootNode = el.getRootNode ? el.getRootNode() : document
97
+ return ids.split(/\s+/).map((id) => { const t = rootNode.getElementById ? rootNode.getElementById(id) : null; return t ? norm(t.innerText || t.textContent) : '' }).filter(Boolean).join(' ')
98
+ }
99
+ const nameOf = (el, role) => {
100
+ const tag = el.localName
101
+ const lb = attr(el, 'aria-labelledby')
102
+ if (lb) { const t = idText(el, lb); if (t) return cut(t, 200) }
103
+ const al = norm(attr(el, 'aria-label') || '')
104
+ if (al) return cut(al, 200)
105
+ if (el.labels && el.labels.length) {
106
+ const t = norm(Array.from(el.labels).map((l) => l.innerText || l.textContent).join(' '))
107
+ if (t) return cut(t, 200)
108
+ }
109
+ if (tag === 'input') {
110
+ const t = typeOf(el)
111
+ if (t === 'button' || t === 'submit' || t === 'reset') { const v = norm(el.value); if (v) return cut(v, 200) }
112
+ if (t === 'image') { const v = norm(attr(el, 'alt') || attr(el, 'value') || ''); if (v) return cut(v, 200) }
113
+ }
114
+ if (tag === 'table') { const c = el.caption ? norm(el.caption.innerText || el.caption.textContent) : ''; if (c) return cut(c, 200) }
115
+ if (tag === 'button' || tag === 'summary' || tag === 'a' || role === 'button' || role === 'link' || role === 'tab' || role === 'menuitem' || role === 'menuitemcheckbox' || role === 'menuitemradio' || role === 'option' || role === 'treeitem' || role === 'checkbox' || role === 'radio' || role === 'switch') {
116
+ let t = norm(el.innerText || el.textContent)
117
+ if (!t && tag === 'a') { const img = el.querySelector('img[alt]'); if (img) t = norm(img.getAttribute('alt')) }
118
+ if (t) return cut(t, 200)
119
+ }
120
+ const title = norm(attr(el, 'title') || '')
121
+ if (title) return cut(title, 200)
122
+ const ph = norm(attr(el, 'placeholder') || '')
123
+ if (ph) return cut(ph, 200)
124
+ return ''
125
+ }
126
+ const labelText = (el) => {
127
+ const lb = attr(el, 'aria-labelledby')
128
+ if (lb) { const t = idText(el, lb); if (t) return cut(t, 200) }
129
+ const al = norm(attr(el, 'aria-label') || '')
130
+ if (al) return cut(al, 200)
131
+ if (el.labels && el.labels.length) { const t = norm(Array.from(el.labels).map((l) => l.innerText || l.textContent).join(' ')); if (t) return cut(t, 200) }
132
+ return ''
133
+ }
134
+
135
+ // ---- sensitive values ----
136
+ const SENSITIVE = /(^|[^a-z])(pass(word|wd|phrase|code)?|pwd|secret|token|api[-_ ]?key|credential|bearer|jwt|otp|csrf|xsrf|session[-_ ]?(id|key)?|ssn|cvv|cvc|private[-_ ]?key|authori[sz]ation|auth[-_ ]?(token|key|code)|one[-_ ]?time[-_ ]?code|card[-_ ]?(number|num|no)|cc[-_ ]?(number|num|csc|exp)|iban|account[-_ ]?(number|num))([^a-z]|$)/i
137
+ const cardLike = (v) => {
138
+ const digits = String(v).replace(/[ -]/g, '')
139
+ if (!/^\d{13,19}$/.test(digits)) return false
140
+ let sum = 0
141
+ for (let i = 0; i < digits.length; i++) { let d = Number(digits[digits.length - 1 - i]); if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9 } sum += d }
142
+ return sum % 10 === 0
143
+ }
144
+ const tokenLike = (v) => {
145
+ if (cardLike(v)) return true
146
+ if (!v || v.length < 20 || /\s/.test(v)) return false
147
+ if (/^eyJ[\w-]+\.[\w-]+\.[\w-]*$/.test(v)) return true
148
+ return /^[A-Za-z0-9_\-+\/=.~]+$/.test(v) && /\d/.test(v) && /[A-Za-z]/.test(v)
149
+ }
150
+ const camel = (s) => String(s).replace(/([a-z])([A-Z])/g, '$1 $2')
151
+ const nameHint = (el) => {
152
+ const parts = [attr(el, 'name'), el.id, attr(el, 'autocomplete'), attr(el, 'aria-label'), attr(el, 'placeholder'), attr(el, 'data-testid')]
153
+ return parts.some((p) => p && SENSITIVE.test(camel(p)))
154
+ }
155
+
156
+ // ---- frames: CSS selectors for iframe elements, unique in their own document ----
157
+ const allRoots = (frame) => {
158
+ if (frame.roots) return frame.roots
159
+ const roots = [frame.doc]
160
+ const walk = (node) => { node.querySelectorAll('*').forEach((e) => { if (e.shadowRoot) { roots.push(e.shadowRoot); walk(e.shadowRoot) } }) }
161
+ walk(frame.doc)
162
+ frame.roots = roots
163
+ return roots
164
+ }
165
+ const countDeep = (frame, selector) => {
166
+ let n = 0
167
+ try { for (const r of allRoots(frame)) n += r.querySelectorAll(selector).length } catch (e) { return -1 }
168
+ return n
169
+ }
170
+ const cssPath = (el) => {
171
+ const parts = []
172
+ let cur = el
173
+ while (cur && cur.nodeType === 1) {
174
+ const tag = cur.localName
175
+ if (cur.id && cur !== el) { parts.unshift(tag + '#' + cssId(cur.id)); break }
176
+ const p = cur.parentNode
177
+ let idx = 1
178
+ if (p && p.children) for (const s of p.children) { if (s === cur) break; if (s.localName === tag) idx++ }
179
+ parts.unshift(tag + ':nth-of-type(' + idx + ')')
180
+ if (!p || p.nodeType !== 1) break
181
+ cur = p
182
+ }
183
+ return parts.join(' > ')
184
+ }
185
+ const frameSelector = (node, frame) => {
186
+ const tag = node.localName
187
+ const tries = []
188
+ if (node.id) tries.push(tag + '#' + cssId(node.id))
189
+ const name = attr(node, 'name'); if (name) tries.push(tag + '[name="' + cssStr(name) + '"]')
190
+ const title = attr(node, 'title'); if (title) tries.push(tag + '[title="' + cssStr(title) + '"]')
191
+ const src = attr(node, 'src'); if (src) tries.push(tag + '[src="' + cssStr(src) + '"]')
192
+ for (const s of tries) if (countDeep(frame, s) === 1) return s
193
+ return cssPath(node)
194
+ }
195
+ const enterFrame = (node, frame) => {
196
+ if (out.frames.length >= 50) { out.framesOmitted++; return }
197
+ let doc = null
198
+ try { doc = node.contentDocument } catch (e) { doc = null }
199
+ const selector = frameSelector(node, frame)
200
+ const path = frame.path.concat([selector])
201
+ const rec = { framePath: path }
202
+ const src = attr(node, 'src'); if (src) rec.src = cut(src, 300)
203
+ if (!doc) {
204
+ rec.crossOrigin = true
205
+ rec.limitation = 'content not observed (cross-origin or sandboxed frame)'
206
+ } else if (path.length > opts.maxFrameDepth || opts.noFrames) {
207
+ rec.skipped = 'frame nesting limit reached'
208
+ } else if (state.framesEntered >= opts.maxFrames) {
209
+ rec.skipped = 'frame count limit reached'
210
+ } else {
211
+ state.framesEntered++
212
+ rec.sameOrigin = true
213
+ try { rec.url = cut(doc.location.href, 300) } catch (e) {}
214
+ const r = rectOf(node)
215
+ const child = { path, doc, win: doc.defaultView, ox: frame.ox + r.left, oy: frame.oy + r.top, roots: null }
216
+ out.frames.push(rec)
217
+ if (doc.documentElement) scan(doc.documentElement, child)
218
+ return
219
+ }
220
+ out.frames.push(rec)
221
+ }
222
+
223
+ // ---- classification ----
224
+ const NATIVE_INPUT_FILL = new Set(['text', 'search', 'tel', 'url', 'email', 'password', 'number', 'date', 'time', 'datetime-local', 'month', 'week', 'color', 'range'])
225
+ const classify = (el) => {
226
+ const tag = el.localName
227
+ const role = explicitRole(el)
228
+ if (tag === 'input') return { native: true, kind: 'input' }
229
+ if (tag === 'option' || tag === 'optgroup') return null
230
+ if (tag === 'select' || tag === 'textarea' || tag === 'button') return { native: true, kind: tag }
231
+ if (tag === 'summary' && el.parentNode && el.parentNode.localName === 'details') return { native: true, kind: 'summary' }
232
+ if (has(el, 'contenteditable') && attr(el, 'contenteditable') !== 'false' && el.isContentEditable) return { native: true, kind: 'contenteditable' }
233
+ if (role && WIDGET_ROLES.has(role)) return { native: false, kind: 'aria' }
234
+ return null
235
+ }
236
+
237
+ const stateOf = (el, kind) => {
238
+ const s = {}
239
+ s.visible = isVisible(el)
240
+ const native = kind.native
241
+ const tag = el.localName
242
+ if (native && (tag === 'input' || tag === 'select' || tag === 'textarea' || tag === 'button')) s.disabled = !!el.matches(':disabled')
243
+ else if (has(el, 'aria-disabled')) s.disabled = attr(el, 'aria-disabled') === 'true'
244
+ if (native && (tag === 'textarea' || (tag === 'input' && !['checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file', 'hidden', 'range', 'color'].includes(typeOf(el))))) s.readonly = !!el.readOnly
245
+ else if (has(el, 'aria-readonly')) s.readonly = attr(el, 'aria-readonly') === 'true'
246
+ if (tag === 'input' && (typeOf(el) === 'checkbox' || typeOf(el) === 'radio')) s.checked = el.indeterminate ? 'mixed' : !!el.checked
247
+ else if (has(el, 'aria-checked')) { const v = attr(el, 'aria-checked'); s.checked = v === 'mixed' ? 'mixed' : v === 'true' }
248
+ if (tag === 'summary' && el.parentNode && el.parentNode.localName === 'details') s.expanded = !!el.parentNode.open
249
+ else if (has(el, 'aria-expanded')) s.expanded = attr(el, 'aria-expanded') === 'true'
250
+ return s
251
+ }
252
+
253
+ const constraintsOf = (el, kind) => {
254
+ const c = {}
255
+ const tag = el.localName
256
+ if (tag === 'input' || tag === 'select' || tag === 'textarea') {
257
+ if (has(el, 'required')) c.required = true
258
+ for (const a of ['min', 'max', 'step', 'pattern', 'accept']) if (tag === 'input' && has(el, a)) c[a] = cut(attr(el, a), 200)
259
+ for (const a of ['minlength', 'maxlength']) if ((tag === 'input' || tag === 'textarea') && has(el, a)) { const n = Number(attr(el, a)); c[a] = Number.isFinite(n) ? n : cut(attr(el, a), 20) }
260
+ if (has(el, 'multiple') && (tag === 'select' || typeOf(el) === 'file' || typeOf(el) === 'email')) c.multiple = true
261
+ } else {
262
+ if (attr(el, 'aria-required') === 'true') c.required = true
263
+ if (has(el, 'aria-valuemin')) c.min = attr(el, 'aria-valuemin')
264
+ if (has(el, 'aria-valuemax')) c.max = attr(el, 'aria-valuemax')
265
+ }
266
+ if (tag === 'input' || tag === 'select' || tag === 'textarea') { if (attr(el, 'aria-required') === 'true') c.required = true }
267
+ return c
268
+ }
269
+
270
+ const validityOf = (el) => {
271
+ try {
272
+ if (!el.willValidate || !el.validity) return undefined
273
+ const v = el.validity
274
+ if (v.valid) return { valid: true }
275
+ const flags = []
276
+ for (const k of ['valueMissing', 'typeMismatch', 'patternMismatch', 'tooLong', 'tooShort', 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput', 'customError']) if (v[k]) flags.push(k)
277
+ return { valid: false, flags, validationMessage: cut(el.validationMessage || '', 200) }
278
+ } catch (e) { return undefined }
279
+ }
280
+
281
+ const valueOf = (el, kind) => {
282
+ const tag = el.localName
283
+ const info = {}
284
+ if (tag === 'input') {
285
+ const t = typeOf(el)
286
+ if (t === 'checkbox' || t === 'radio' || t === 'button' || t === 'submit' || t === 'reset' || t === 'image') return info
287
+ if (t === 'file') { info.hasValue = !!(el.files && el.files.length); return info }
288
+ const v = String(el.value == null ? '' : el.value)
289
+ info.hasValue = v.length > 0
290
+ if (t === 'password' || t === 'hidden' || nameHint(el) || tokenLike(v)) { info.sensitive = true; return info }
291
+ if (opts.includeValues) info.value = cut(v, 500)
292
+ return info
293
+ }
294
+ if (tag === 'textarea') {
295
+ const v = String(el.value || '')
296
+ info.hasValue = v.length > 0
297
+ if (nameHint(el) || tokenLike(v)) { info.sensitive = true; return info }
298
+ if (opts.includeValues) info.value = cut(v, 500)
299
+ return info
300
+ }
301
+ if (tag === 'select') {
302
+ const sel = Array.from(el.selectedOptions || [])
303
+ info.hasValue = sel.some((o) => o.value !== '')
304
+ if (nameHint(el)) { info.sensitive = true; return info }
305
+ if (opts.includeValues) info.value = el.multiple ? sel.map((o) => cut(o.value, 200)) : (sel[0] ? cut(sel[0].value, 200) : '')
306
+ return info
307
+ }
308
+ if (kind.kind === 'contenteditable') {
309
+ const v = norm(el.innerText || el.textContent)
310
+ info.hasValue = v.length > 0
311
+ if (nameHint(el)) { info.sensitive = true; return info }
312
+ if (opts.includeValues) info.value = cut(v, 500)
313
+ }
314
+ return info
315
+ }
316
+
317
+ const actionsFor = (el, kind, role, st) => {
318
+ if (st.visible === false || st.disabled === true) return []
319
+ const tag = el.localName
320
+ if (tag === 'input') {
321
+ const t = typeOf(el)
322
+ if (t === 'hidden') return []
323
+ if (t === 'checkbox' || t === 'radio') return ['check', 'click']
324
+ if (t === 'file') return ['upload']
325
+ if (t === 'button' || t === 'submit' || t === 'reset' || t === 'image') return ['click']
326
+ if (st.readonly === true) return []
327
+ if (t === 'range' || t === 'color' || t === 'date' || t === 'time' || t === 'datetime-local' || t === 'month' || t === 'week') return ['fill']
328
+ return ['fill', 'type', 'clear']
329
+ }
330
+ if (tag === 'textarea') return st.readonly === true ? [] : ['fill', 'type', 'clear']
331
+ if (tag === 'select') return ['select']
332
+ if (tag === 'button' || tag === 'summary') return ['click']
333
+ if (kind.kind === 'contenteditable') return ['fill', 'type', 'clear']
334
+ if (role === 'checkbox' || role === 'radio' || role === 'switch' || role === 'menuitemcheckbox' || role === 'menuitemradio') return ['check', 'click']
335
+ if (role === 'textbox' || role === 'searchbox') return ['fill', 'type', 'clear']
336
+ if (role === 'combobox') return ['click', 'type']
337
+ if (role === 'slider' || role === 'spinbutton') return ['press']
338
+ return ['click']
339
+ }
340
+
341
+ // A link whose address carries a secret in its query shows the value as [redacted] and gets no href-based locator.
342
+ const QUERY_PARAM = /([?&#;])([^=&#;]+)=([^&#;]*)/g
343
+ const redactHref = (h) => h.replace(QUERY_PARAM, (m, d, k) => (SENSITIVE.test(camel(k)) ? d + k + '=[redacted]' : m))
344
+ const hrefHasSecret = (h) => { let hit = false; h.replace(QUERY_PARAM, (m, d, k) => { if (SENSITIVE.test(camel(k))) hit = true; return m }); return hit }
345
+
346
+ const candidatesFor = (el, role, name) => {
347
+ const c = []
348
+ const tag = el.localName
349
+ if (role && name && name.length <= 150) c.push({ role, name, exact: true })
350
+ const lt = labelText(el)
351
+ if (lt && lt.length <= 150 && (tag === 'input' || tag === 'select' || tag === 'textarea')) c.push({ label: lt, exact: true })
352
+ const tid = attr(el, 'data-testid'); if (tid) c.push({ testId: tid })
353
+ if (el.id) c.push({ selector: '#' + cssId(el.id) })
354
+ const nm = attr(el, 'name')
355
+ if (nm && (tag === 'input' || tag === 'select' || tag === 'textarea' || tag === 'button')) {
356
+ const t = tag === 'input' ? '[type="' + cssStr(typeOf(el)) + '"]' : ''
357
+ const v = tag === 'input' && typeOf(el) === 'radio' && has(el, 'value') ? '[value="' + cssStr(attr(el, 'value')) + '"]' : ''
358
+ c.push({ selector: tag + t + '[name="' + cssStr(nm) + '"]' + v })
359
+ }
360
+ const ph = attr(el, 'placeholder')
361
+ if (ph && (tag === 'input' || tag === 'textarea')) c.push({ selector: tag + '[placeholder="' + cssStr(ph) + '"]' })
362
+ if (tag === 'a' && has(el, 'href') && !hrefHasSecret(attr(el, 'href'))) c.push({ selector: 'a[href="' + cssStr(attr(el, 'href')) + '"]' })
363
+ return c
364
+ }
365
+
366
+ const optionRows = (el) => {
367
+ const rows = []
368
+ const list = Array.from(el.options || [])
369
+ for (const o of list.slice(0, 100)) {
370
+ const row = { value: cut(o.value, 200), label: cut(norm(o.label || o.textContent), 200), selected: !!o.selected }
371
+ if (o.disabled) row.disabled = true
372
+ rows.push(row)
373
+ }
374
+ return { rows, total: list.length }
375
+ }
376
+
377
+ const buildControl = (item) => {
378
+ const el = item.el
379
+ const frame = item.frame
380
+ const kind = item.kind
381
+ if (item.group) return buildRadioGroup(item)
382
+ const role = explicitRole(el) || implicitRole(el)
383
+ const name = nameOf(el, role)
384
+ const rec = { id: numbered(el, frame.win), source: kind.native ? 'dom' : 'aria', path: frame.path }
385
+ if (role) rec.role = role
386
+ if (name) rec.name = name
387
+ const t = typeOf(el)
388
+ if (el.localName === 'input') rec.type = t
389
+ else if (el.localName === 'select') rec.type = el.multiple ? 'select-multiple' : 'select-one'
390
+ else if (el.localName === 'textarea') rec.type = 'textarea'
391
+ else if (el.localName === 'button') rec.type = String(el.type || 'submit')
392
+ else if (kind.kind === 'contenteditable') rec.type = 'contenteditable'
393
+ const st = stateOf(el, kind)
394
+ Object.assign(rec, st)
395
+ Object.assign(rec, valueOf(el, kind))
396
+ if (el.localName === 'select') { const o = optionRows(el); rec.options = o.rows; if (o.total > o.rows.length) rec.optionsTotal = o.total }
397
+ const c = constraintsOf(el, kind)
398
+ if (Object.keys(c).length) rec.constraints = c
399
+ const v = el.localName === 'button' ? undefined : validityOf(el)
400
+ if (v) rec.validity = v
401
+ rec.actions = actionsFor(el, kind, role, st)
402
+ rec.cands = candidatesFor(el, role, name)
403
+ return rec
404
+ }
405
+
406
+ const buildRadioGroup = (item) => {
407
+ const members = item.members
408
+ const first = members[0]
409
+ const frame = item.frame
410
+ const rec = { source: 'dom', path: frame.path, role: 'radiogroup', type: 'radio' }
411
+ // The group is named by its fieldset legend or role=radiogroup label, else by its name attribute.
412
+ let name = ''
413
+ const fs = first.closest ? first.closest('fieldset') : null
414
+ if (fs && fs.querySelector('legend')) name = norm(fs.querySelector('legend').innerText || fs.querySelector('legend').textContent)
415
+ if (!name) { const rg = first.closest ? first.closest('[role=radiogroup]') : null; if (rg) name = norm(attr(rg, 'aria-label') || (attr(rg, 'aria-labelledby') ? idText(rg, attr(rg, 'aria-labelledby')) : '')) }
416
+ if (!name) name = attr(first, 'name') || ''
417
+ if (name) rec.name = cut(name, 200)
418
+ rec.visible = members.some((m) => isVisible(m))
419
+ const dis = members.every((m) => m.matches(':disabled'))
420
+ rec.disabled = dis
421
+ rec.options = members.map((m) => {
422
+ const mrole = 'radio'
423
+ const mname = nameOf(m, mrole)
424
+ const row = { id: numbered(m, frame.win), label: cut(mname, 200), value: cut(String(m.value), 200), checked: !!m.checked, cands: candidatesFor(m, mrole, mname) }
425
+ if (m.matches(':disabled')) row.disabled = true
426
+ return row
427
+ })
428
+ const c = constraintsOf(first, item.kind)
429
+ if (Object.keys(c).length) rec.constraints = c
430
+ const v = validityOf(first)
431
+ if (v) rec.validity = v
432
+ rec.actions = rec.visible && !dis ? ['check'] : []
433
+ return rec
434
+ }
435
+
436
+ const buildLink = (el, frame) => {
437
+ const rec = { id: numbered(el, frame.win), path: frame.path }
438
+ rec.text = nameOf(el, 'link')
439
+ let href = ''
440
+ try { href = typeof el.href === 'string' ? el.href : String(el.href && el.href.baseVal || '') } catch (e) {}
441
+ rec.href = cut(redactHref(href || attr(el, 'href') || ''), 500)
442
+ rec.visible = isVisible(el)
443
+ if (attr(el, 'target') === '_blank') rec.target = '_blank'
444
+ rec.cands = candidatesFor(el, 'link', rec.text)
445
+ return rec
446
+ }
447
+
448
+ // ---- tables ----
449
+ const cells = (row) => Array.from(row.cells || []).map((c) => cut(norm(c.innerText || c.textContent), 120))
450
+ const ariaCells = (row) => Array.from(row.querySelectorAll('[role=gridcell],[role=cell],[role=rowheader],[role=columnheader]')).map((c) => cut(norm(c.innerText || c.textContent), 120))
451
+ const PAGER = /^(next|next page|older|more|load more|show more|›|»|>|下一页|下页|更多)$/i
452
+ const paginationNear = (t) => {
453
+ const scopes = []
454
+ let cur = t
455
+ // An ancestor counts only while this table is the only one in it; otherwise a pager belongs to some other table.
456
+ const tablesIn = (n) => n.querySelectorAll('table, [role=table], [role=grid], [role=treegrid]').length
457
+ for (let i = 0; i < 3 && cur; i++) { cur = cur.parentElement; if (cur && tablesIn(cur) === 1) scopes.push(cur); else break }
458
+ if (t.nextElementSibling && tablesIn(t.nextElementSibling) === 0) scopes.push(t.nextElementSibling)
459
+ if (t.previousElementSibling && tablesIn(t.previousElementSibling) === 0) scopes.push(t.previousElementSibling)
460
+ for (const s of scopes) {
461
+ let hit = null
462
+ try { hit = s.querySelector('[aria-label*="agination" i], nav.pagination, .pagination, [class*="pagination" i], [data-pagination], [rel="next"]') } catch (e) {}
463
+ if (s.matches && s.matches('[aria-label*="agination" i], .pagination, [class*="pagination" i]')) hit = s
464
+ if (hit && !t.contains(hit)) return 'pagination controls found next to the table'
465
+ for (const b of s.querySelectorAll('a, button, [role=button]')) {
466
+ if (t.contains(b)) continue
467
+ if (PAGER.test(norm(b.innerText || b.textContent || attr(b, 'aria-label') || ''))) return 'a "' + cut(norm(b.innerText || b.textContent || attr(b, 'aria-label')), 30) + '" control is next to the table'
468
+ }
469
+ }
470
+ return ''
471
+ }
472
+ const buildTable = (t, frame) => {
473
+ const native = t.localName === 'table'
474
+ let headers = []
475
+ let body = []
476
+ let headerRows = 0
477
+ let rowsAll = []
478
+ if (native) {
479
+ rowsAll = Array.from(t.rows)
480
+ const head = rowsAll.filter((r) => r.parentNode && r.parentNode.localName === 'thead')
481
+ if (head.length) { headers = cells(head[head.length - 1]); headerRows = head.length; body = rowsAll.filter((r) => head.indexOf(r) < 0) }
482
+ else if (rowsAll.length && rowsAll[0].cells.length && Array.from(rowsAll[0].cells).every((c) => c.localName === 'th')) { headers = cells(rowsAll[0]); headerRows = 1; body = rowsAll.slice(1) }
483
+ else body = rowsAll
484
+ } else {
485
+ rowsAll = Array.from(t.querySelectorAll('[role=row]'))
486
+ const head = rowsAll.filter((r) => r.querySelector('[role=columnheader]') && !r.querySelector('[role=gridcell],[role=cell]'))
487
+ if (head.length) { headers = ariaCells(head[0]); headerRows = head.length }
488
+ body = rowsAll.filter((r) => head.indexOf(r) < 0)
489
+ }
490
+ const rec = { id: numbered(t, frame.win), path: frame.path, headers, totalRows: body.length, coverage: 'complete' }
491
+ const role = native ? 'table' : explicitRole(t)
492
+ const name = nameOf(t, role) || (attr(t, 'aria-label') || '')
493
+ if (name) rec.name = cut(name, 200)
494
+ rec.rows = body.slice(0, MAX).map((r) => (native ? cells(r) : ariaCells(r)))
495
+ const reasons = []
496
+ const declared = has(t, 'aria-rowcount') ? Number(attr(t, 'aria-rowcount')) : undefined
497
+ if (declared === -1) { reasons.push('row count unknown (aria-rowcount=-1): the list is loaded in pieces'); }
498
+ else if (declared !== undefined && Number.isFinite(declared) && declared > body.length + headerRows) { rec.declaredRows = declared - headerRows; reasons.push('virtualized: aria-rowcount says ' + declared + ' rows, ' + (body.length + headerRows) + ' are in the DOM') }
499
+ const idx = body.map((r) => Number(attr(r, 'aria-rowindex'))).filter((n) => Number.isFinite(n))
500
+ if (idx.length && !reasons.length && Math.min.apply(null, idx) > headerRows + 1) reasons.push('virtualized: the first rendered row is aria-rowindex ' + Math.min.apply(null, idx))
501
+ const pager = paginationNear(t)
502
+ if (pager) reasons.push('paginated: ' + pager + '; this is one page of the data')
503
+ if (reasons.length) { rec.coverage = 'partial'; rec.reason = reasons.join('; ') }
504
+ rec.cands = []
505
+ if (name) rec.cands.push({ role: role || 'table', name, exact: true })
506
+ const tid = attr(t, 'data-testid'); if (tid) rec.cands.push({ testId: tid })
507
+ if (t.id) rec.cands.push({ selector: '#' + cssId(t.id) })
508
+ return rec
509
+ }
510
+
511
+ const ARIA_TABLE = new Set(['table', 'grid', 'treegrid'])
512
+
513
+ // ---- the scan ----
514
+ const handle = (el, frame) => {
515
+ const tag = el.localName
516
+ if (tag === 'iframe' || tag === 'frame') { if (opts.controls || opts.links || opts.tables) enterFrame(el, frame); return }
517
+ if (opts.controls) {
518
+ const kind = classify(el)
519
+ if (kind && inRegion(el, frame)) {
520
+ if (kind.kind === 'input' && typeOf(el) === 'radio' && attr(el, 'name')) {
521
+ const rootNode = el.getRootNode()
522
+ let groups = radioGroups.get(rootNode)
523
+ if (!groups) { groups = new Map(); radioGroups.set(rootNode, groups) }
524
+ if (el.form && !formIds.has(el.form)) formIds.set(el.form, nextForm++)
525
+ const key = (el.form ? 'f' + formIds.get(el.form) : '') + '|' + attr(el, 'name') + '|' + frame.path.join('>')
526
+ const existing = groups.get(key)
527
+ if (existing) existing.members.push(el)
528
+ else { const it = { el, kind, frame, group: true, members: [el] }; groups.set(key, it); items.push(it) }
529
+ } else items.push({ el, kind, frame })
530
+ }
531
+ }
532
+ if (opts.links && (tag === 'a' || tag === 'area') && has(el, 'href') && inRegion(el, frame)) {
533
+ out.counts.links++
534
+ if (out.links.length < MAX) out.links.push(buildLink(el, frame))
535
+ }
536
+ if (opts.tables && (tag === 'table' || (explicitRole(el) && ARIA_TABLE.has(explicitRole(el)))) && inRegion(el, frame)) {
537
+ out.counts.tables++
538
+ if (out.tables.length < MAX) out.tables.push(buildTable(el, frame))
539
+ }
540
+ }
541
+ const scan = (start, frame) => {
542
+ const stack = [start]
543
+ while (stack.length) {
544
+ const el = stack.pop()
545
+ if (!isEl(el)) continue
546
+ handle(el, frame)
547
+ const kids = el.children
548
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i])
549
+ if (el.shadowRoot) {
550
+ out.shadowRoots++
551
+ const sk = el.shadowRoot.children
552
+ for (let i = sk.length - 1; i >= 0; i--) stack.push(sk[i])
553
+ }
554
+ }
555
+ }
556
+
557
+ const topDoc = root.nodeType === 9 ? root : root.ownerDocument
558
+ const frame0 = { path: [], doc: topDoc, win: topDoc.defaultView, ox: 0, oy: 0, roots: null }
559
+ if (root.nodeType === 9) { if (root.documentElement) scan(root.documentElement, frame0) } else scan(root, frame0)
560
+
561
+ out.counts.controls = items.length
562
+ for (const it of items.slice(0, MAX)) out.controls.push(buildControl(it))
563
+ return out
564
+ }`;
565
+ //# sourceMappingURL=observe-script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observe-script.js","sourceRoot":"","sources":["../src/observe-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,wGAAwG;AACxG,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA;AAE3C,mFAAmF;AACnF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAA;AAExC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAA;AAEpC,yFAAyF;AACzF,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAA;;;EAGvC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAshBtC,CAAA"}