@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.
- package/README.md +266 -88
- package/assets/skills/dsh-browser/SKILL.md +120 -0
- package/assets/skills/dsh-browser/references/develop.md +48 -0
- package/assets/skills/dsh-browser/references/observe.md +91 -0
- package/assets/skills/dsh-browser/references/opencli.md +25 -0
- package/assets/skills/dsh-browser/references/recipes.md +149 -0
- package/assets/skills/dsh-browser/references/userscripts.md +44 -0
- package/cordis.patch.yml +11 -3
- package/lib/actions/act.d.ts +3 -0
- package/lib/actions/act.js +147 -0
- package/lib/actions/act.js.map +1 -0
- package/lib/actions/automation.d.ts +3 -0
- package/lib/actions/automation.js +356 -0
- package/lib/actions/automation.js.map +1 -0
- package/lib/actions/crawl.d.ts +3 -0
- package/lib/actions/crawl.js +28 -0
- package/lib/actions/crawl.js.map +1 -0
- package/lib/actions/errors.d.ts +53 -0
- package/lib/actions/errors.js +178 -0
- package/lib/actions/errors.js.map +1 -0
- package/lib/actions/index-view.d.ts +42 -0
- package/lib/actions/index-view.js +268 -0
- package/lib/actions/index-view.js.map +1 -0
- package/lib/actions/inspect.d.ts +3 -0
- package/lib/actions/inspect.js +35 -0
- package/lib/actions/inspect.js.map +1 -0
- package/lib/actions/observe.d.ts +3 -0
- package/lib/actions/observe.js +121 -0
- package/lib/actions/observe.js.map +1 -0
- package/lib/actions/opencli.d.ts +3 -0
- package/lib/actions/opencli.js +51 -0
- package/lib/actions/opencli.js.map +1 -0
- package/lib/actions/registry.d.ts +35 -0
- package/lib/actions/registry.js +89 -0
- package/lib/actions/registry.js.map +1 -0
- package/lib/actions/run.d.ts +34 -0
- package/lib/actions/run.js +226 -0
- package/lib/actions/run.js.map +1 -0
- package/lib/actions/runtime.d.ts +3 -0
- package/lib/actions/runtime.js +33 -0
- package/lib/actions/runtime.js.map +1 -0
- package/lib/actions/schema.d.ts +31 -0
- package/lib/actions/schema.js +309 -0
- package/lib/actions/schema.js.map +1 -0
- package/lib/actions/script.d.ts +3 -0
- package/lib/actions/script.js +97 -0
- package/lib/actions/script.js.map +1 -0
- package/lib/actions/shared.d.ts +31 -0
- package/lib/actions/shared.js +45 -0
- package/lib/actions/shared.js.map +1 -0
- package/lib/actions/surface.d.ts +22 -0
- package/lib/actions/surface.js +18 -0
- package/lib/actions/surface.js.map +1 -0
- package/lib/actions/target.d.ts +3 -0
- package/lib/actions/target.js +66 -0
- package/lib/actions/target.js.map +1 -0
- package/lib/actions/types.d.ts +181 -0
- package/lib/actions/types.js +48 -0
- package/lib/actions/types.js.map +1 -0
- package/lib/activation-rules.d.ts +29 -0
- package/lib/activation-rules.js +33 -0
- package/lib/activation-rules.js.map +1 -0
- package/lib/approval-policy.d.ts +40 -2
- package/lib/approval-policy.js +184 -97
- package/lib/approval-policy.js.map +1 -1
- package/lib/automation-assets-rpc.d.ts +8 -2
- package/lib/automation-assets-rpc.js +68 -10
- package/lib/automation-assets-rpc.js.map +1 -1
- package/lib/automation-assets.d.ts +152 -4
- package/lib/automation-assets.js +255 -15
- package/lib/automation-assets.js.map +1 -1
- package/lib/automation-convert.d.ts +23 -0
- package/lib/automation-convert.js +110 -0
- package/lib/automation-convert.js.map +1 -0
- package/lib/automation-development.d.ts +26 -0
- package/lib/automation-development.js +47 -0
- package/lib/automation-development.js.map +1 -1
- package/lib/automation-execution.d.ts +57 -1
- package/lib/automation-execution.js +167 -11
- package/lib/automation-execution.js.map +1 -1
- package/lib/automation-journal.d.ts +92 -0
- package/lib/automation-journal.js +449 -0
- package/lib/automation-journal.js.map +1 -0
- package/lib/automation-v2.d.ts +138 -0
- package/lib/automation-v2.js +631 -0
- package/lib/automation-v2.js.map +1 -0
- package/lib/automation.d.ts +82 -5
- package/lib/automation.js +239 -82
- package/lib/automation.js.map +1 -1
- package/lib/browser-service.d.ts +309 -32
- package/lib/browser-service.js +636 -182
- package/lib/browser-service.js.map +1 -1
- package/lib/client/SettingsCard.js +95 -57
- package/lib/client/SettingsCard.js.map +1 -1
- package/lib/client/asset-editor.d.ts +55 -0
- package/lib/client/asset-editor.js +101 -0
- package/lib/client/asset-editor.js.map +1 -0
- package/lib/client/automation-assets-client.d.ts +99 -2
- package/lib/client/automation-assets-client.js +243 -10
- package/lib/client/automation-assets-client.js.map +1 -1
- package/lib/client/form.d.ts +48 -4
- package/lib/client/form.js +142 -29
- package/lib/client/form.js.map +1 -1
- package/lib/client/index.d.ts +25 -2
- package/lib/client/index.js +5 -1
- package/lib/client/index.js.map +1 -1
- package/lib/client/locales.d.ts +85 -6
- package/lib/client/locales.js +64 -12
- package/lib/client/locales.js.map +1 -1
- package/lib/client/prompts-form.d.ts +67 -0
- package/lib/client/prompts-form.js +140 -0
- package/lib/client/prompts-form.js.map +1 -0
- package/lib/client/prompts-status-client.d.ts +34 -0
- package/lib/client/prompts-status-client.js +53 -0
- package/lib/client/prompts-status-client.js.map +1 -0
- package/lib/client/settings-namespace.d.ts +1 -1
- package/lib/client/settings-namespace.js +1 -1
- package/lib/client/settings-namespace.js.map +1 -1
- package/lib/client.js +1915 -330
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +50 -1
- package/lib/config.js +31 -1
- package/lib/config.js.map +1 -1
- package/lib/dialogs.d.ts +37 -0
- package/lib/dialogs.js +73 -0
- package/lib/dialogs.js.map +1 -0
- package/lib/freedom.d.ts +23 -7
- package/lib/freedom.js +48 -59
- package/lib/freedom.js.map +1 -1
- package/lib/index.d.ts +5 -13
- package/lib/index.js +51 -30
- package/lib/index.js.map +1 -1
- package/lib/journal.d.ts +109 -0
- package/lib/journal.js +238 -0
- package/lib/journal.js.map +1 -0
- package/lib/locator.d.ts +82 -0
- package/lib/locator.js +190 -0
- package/lib/locator.js.map +1 -0
- package/lib/observe-script.d.ts +27 -0
- package/lib/observe-script.js +565 -0
- package/lib/observe-script.js.map +1 -0
- package/lib/observe.d.ts +179 -0
- package/lib/observe.js +368 -0
- package/lib/observe.js.map +1 -0
- package/lib/policy-keys.d.ts +30 -0
- package/lib/policy-keys.js +34 -0
- package/lib/policy-keys.js.map +1 -0
- package/lib/prompt-limits.d.ts +30 -0
- package/lib/prompt-limits.js +31 -0
- package/lib/prompt-limits.js.map +1 -0
- package/lib/prompts.d.ts +131 -0
- package/lib/prompts.js +295 -0
- package/lib/prompts.js.map +1 -0
- package/lib/skill.d.ts +100 -0
- package/lib/skill.js +181 -0
- package/lib/skill.js.map +1 -0
- package/lib/tool-defs.d.ts +72 -0
- package/lib/tool-defs.js +67 -0
- package/lib/tool-defs.js.map +1 -0
- package/lib/tools.d.ts +19 -4
- package/lib/tools.js +124 -922
- package/lib/tools.js.map +1 -1
- package/package.json +15 -11
- package/scripts/check-host-rpc.mjs +11 -4
- package/scripts/dump-prompts.mjs +29 -0
- package/scripts/measure-tool-surface.mjs +190 -0
- package/lib/settings-scope.d.ts +0 -56
- package/lib/settings-scope.js +0 -51
- 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"}
|