@hypothesi/tauri-mcp-server 0.7.0 → 0.8.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.
@@ -0,0 +1,536 @@
1
+ /**
2
+ * DOM Snapshot - Generates structured DOM representations for AI consumption
3
+ *
4
+ * Supports two snapshot types:
5
+ *
6
+ * 'accessibility' - Uses aria-api for comprehensive, spec-compliant accessibility computation:
7
+ * - WAI-ARIA 1.3 role computation
8
+ * - HTML-AAM 1.0 implicit role mappings
9
+ * - Accessible Name and Description Computation 1.2
10
+ * - aria-owns relationship handling
11
+ * - Shadow DOM traversal
12
+ *
13
+ * 'structure' - DOM structure tree with:
14
+ * - Element tag names
15
+ * - Element IDs (if present)
16
+ * - CSS classes (if present)
17
+ * - data-testid attribute (if present)
18
+ *
19
+ * @param {Object} params
20
+ * @param {string} params.type - Snapshot type ('accessibility' or 'structure')
21
+ * @param {string|null} params.selector - Optional CSS selector to scope snapshot
22
+ */
23
+ (function(params) {
24
+ 'use strict';
25
+
26
+ const { type, selector } = params;
27
+
28
+ // ARIA states to include in snapshot (used by accessibility type)
29
+ const ARIA_STATES = [
30
+ 'checked', 'disabled', 'expanded', 'pressed', 'selected',
31
+ 'hidden', 'invalid', 'required', 'readonly', 'busy',
32
+ 'current', 'grabbed', 'haspopup', 'live', 'modal',
33
+ 'multiline', 'multiselectable', 'orientation', 'sort'
34
+ ];
35
+
36
+ // Roles that should include value
37
+ const VALUE_ROLES = new Set([
38
+ 'textbox', 'searchbox', 'spinbutton', 'slider',
39
+ 'scrollbar', 'progressbar', 'meter', 'combobox'
40
+ ]);
41
+
42
+ // ========================================================================
43
+ // Ref ID System
44
+ // ========================================================================
45
+
46
+ let refCounter = 0;
47
+ const refMap = new Map(),
48
+ reverseRefMap = new Map();
49
+
50
+ function getOrCreateRef(element) {
51
+ if (!refMap.has(element)) {
52
+ const ref = 'e' + refCounter++;
53
+ refMap.set(element, ref);
54
+ reverseRefMap.set(ref, element);
55
+ }
56
+ return refMap.get(element);
57
+ }
58
+
59
+ window.__MCP_ARIA_REFS__ = refMap;
60
+ window.__MCP_ARIA_REFS_REVERSE__ = reverseRefMap;
61
+
62
+ // ========================================================================
63
+ // Visibility (using aria-api for correct aria-hidden inheritance)
64
+ // ========================================================================
65
+
66
+ function isAccessibilityVisible(element) {
67
+ // Use aria-api for aria-hidden (handles inheritance)
68
+ try {
69
+ var ariaHidden = ariaApi.getAttribute(element, 'hidden');
70
+ if (ariaHidden === true || ariaHidden === 'true') return false;
71
+ } catch (e) {
72
+ // Ignore errors from aria-api
73
+ }
74
+
75
+ if (element.hidden) return false;
76
+ if (element.inert || element.closest('[inert]')) return false;
77
+
78
+ try {
79
+ const style = window.getComputedStyle(element);
80
+ if (style.display === 'none') return false;
81
+ if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
82
+ } catch (e) {
83
+ return false;
84
+ }
85
+
86
+ return true;
87
+ }
88
+
89
+ function isPresentational(element) {
90
+ const role = element.getAttribute('role');
91
+ return role === 'presentation' || role === 'none';
92
+ }
93
+
94
+ // ========================================================================
95
+ // Accessibility Properties (using aria-api)
96
+ // ========================================================================
97
+
98
+ function getRole(element) {
99
+ return ariaApi.getRole(element);
100
+ }
101
+
102
+ function getName(element) {
103
+ return ariaApi.getName(element) || '';
104
+ }
105
+
106
+ function getDescription(element) {
107
+ return ariaApi.getDescription(element) || '';
108
+ }
109
+
110
+ function safeGetAttribute(element, attr) {
111
+ try {
112
+ return ariaApi.getAttribute(element, attr);
113
+ } catch (e) {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function getStates(element) {
119
+ const states = {};
120
+ const role = getRole(element);
121
+
122
+ for (const attr of ARIA_STATES) {
123
+ var value = safeGetAttribute(element, attr);
124
+ if (value !== null && value !== undefined && value !== '') {
125
+ if (value === true || value === 'true') {
126
+ states[attr] = true;
127
+ } else if (value === false || value === 'false') {
128
+ if (['expanded', 'pressed', 'checked', 'selected'].includes(attr)) {
129
+ states[attr] = false;
130
+ }
131
+ } else if (value === 'mixed') {
132
+ states[attr] = 'mixed';
133
+ } else {
134
+ states[attr] = value;
135
+ }
136
+ }
137
+ }
138
+
139
+ // Heading level
140
+ if (role === 'heading') {
141
+ var ariaLevel = safeGetAttribute(element, 'level');
142
+ if (ariaLevel) {
143
+ states.level = parseInt(ariaLevel, 10);
144
+ } else if (/^H[1-6]$/i.test(element.tagName)) {
145
+ states.level = parseInt(element.tagName[1], 10);
146
+ }
147
+ }
148
+
149
+ // Value for value-bearing roles
150
+ if (VALUE_ROLES.has(role)) {
151
+ var valueNow = safeGetAttribute(element, 'valuenow');
152
+ var valueText = safeGetAttribute(element, 'valuetext');
153
+ if (valueText) states.valuetext = valueText;
154
+ else if (valueNow !== null) states.valuenow = valueNow;
155
+
156
+ if ((role === 'textbox' || role === 'searchbox') && element.value) {
157
+ states.value = element.value;
158
+ }
159
+ }
160
+
161
+ return states;
162
+ }
163
+
164
+ // ========================================================================
165
+ // Tree Building (using aria-api for aria-owns aware traversal)
166
+ // ========================================================================
167
+
168
+ // Roles to skip (filter out like Chrome DevTools does)
169
+ const SKIP_ROLES = new Set(['generic', 'none', 'presentation']);
170
+
171
+ // Roles that are structural landmarks (keep even without name)
172
+ const LANDMARK_ROLES = new Set([
173
+ 'banner', 'main', 'contentinfo', 'navigation', 'complementary',
174
+ 'region', 'search', 'form', 'application', 'document'
175
+ ]);
176
+
177
+ function buildAriaTree(element, depth, maxDepth) {
178
+ depth = depth || 0;
179
+ maxDepth = maxDepth || 100;
180
+
181
+ if (depth > maxDepth) return { type: 'error', message: 'Max depth exceeded' };
182
+ if (!isAccessibilityVisible(element)) return null;
183
+
184
+ // Handle presentational elements
185
+ if (isPresentational(element)) {
186
+ const children = getAccessibleChildren(element, depth, maxDepth);
187
+ if (children.length === 0) return null;
188
+ if (children.length === 1) return children[0];
189
+ return { type: 'fragment', children: children };
190
+ }
191
+
192
+ const role = getRole(element);
193
+ const name = getName(element);
194
+ const description = getDescription(element);
195
+ const states = getStates(element);
196
+ const ref = getOrCreateRef(element);
197
+ const children = getAccessibleChildren(element, depth, maxDepth);
198
+
199
+ // Skip generic/none roles (like Chrome DevTools) - just return children
200
+ if (SKIP_ROLES.has(role) && !name) {
201
+ if (children.length === 0) return null;
202
+ if (children.length === 1) return children[0];
203
+ return { type: 'fragment', children: children };
204
+ }
205
+
206
+ if (!role && !name && children.length === 0) return null;
207
+
208
+ // Get URL for links (like Playwright)
209
+ var url;
210
+ if (role === 'link' && element.href) {
211
+ url = element.href;
212
+ }
213
+
214
+ // Check cursor style for interactive elements (like Playwright's [cursor=pointer])
215
+ var cursor;
216
+ try {
217
+ var computedStyle = window.getComputedStyle(element);
218
+ if (computedStyle.cursor === 'pointer') {
219
+ cursor = 'pointer';
220
+ }
221
+ } catch (e) {
222
+ // Ignore style errors
223
+ }
224
+
225
+ return {
226
+ type: 'element',
227
+ role: role || 'generic',
228
+ name: name || undefined,
229
+ description: description || undefined,
230
+ states: Object.keys(states).length > 0 ? states : undefined,
231
+ url: url,
232
+ cursor: cursor,
233
+ ref: ref,
234
+ children: children.length > 0 ? children : undefined
235
+ };
236
+ }
237
+
238
+ function getAccessibleChildren(element, depth, maxDepth) {
239
+ const children = [];
240
+ // Use aria-api's getChildNodes for aria-owns support
241
+ const childNodes = ariaApi.getChildNodes(element);
242
+
243
+ for (const child of childNodes) {
244
+ if (child.nodeType === Node.ELEMENT_NODE) {
245
+ const childTree = buildAriaTree(child, depth + 1, maxDepth);
246
+ if (childTree) {
247
+ if (childTree.type === 'fragment') {
248
+ children.push.apply(children, childTree.children);
249
+ } else {
250
+ children.push(childTree);
251
+ }
252
+ }
253
+ } else if (child.nodeType === Node.TEXT_NODE) {
254
+ const text = child.textContent ? child.textContent.trim() : '';
255
+ // Use text type like Playwright
256
+ if (text) children.push({ type: 'text', content: text });
257
+ }
258
+ }
259
+
260
+ return children;
261
+ }
262
+
263
+ // ========================================================================
264
+ // Playwright-compatible YAML Rendering
265
+ // ========================================================================
266
+
267
+ function yamlEscape(str) {
268
+ if (!str) return '""';
269
+ var needsQuotes = /[\n\r\t:#{}\[\],&*?|<>=!%@`]/.test(str) ||
270
+ str.startsWith(' ') || str.endsWith(' ') ||
271
+ str.includes('"') || str.includes("'");
272
+ if (!needsQuotes && !/^[\d.+-]/.test(str)) return str;
273
+ return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
274
+ .replace(/\n/g, '\\n').replace(/\r/g, '\\r') + '"';
275
+ }
276
+
277
+ function renderYaml(node, indent) {
278
+ indent = indent || 0;
279
+ var prefix = ' '.repeat(indent);
280
+ if (!node) return '';
281
+
282
+ // Text nodes (like Playwright)
283
+ if (node.type === 'text') {
284
+ return prefix + '- text: ' + yamlEscape(node.content) + '\n';
285
+ }
286
+
287
+ if (node.type === 'fragment') {
288
+ return node.children.map(function(c) { return renderYaml(c, indent); }).join('');
289
+ }
290
+
291
+ if (node.type === 'error') {
292
+ return prefix + '# ERROR: ' + node.message + '\n';
293
+ }
294
+
295
+ // Build line: - role "name" [attrs] [ref=X]
296
+ var line = prefix + '- ' + node.role;
297
+ if (node.name) line += ' ' + yamlEscape(node.name);
298
+
299
+ // Build attributes in brackets like Playwright
300
+ var attrs = [];
301
+ if (node.states) {
302
+ for (var key in node.states) {
303
+ if (Object.prototype.hasOwnProperty.call(node.states, key)) {
304
+ var value = node.states[key];
305
+ // Skip false values (cleaner output)
306
+ if (value === false) continue;
307
+ if (value === true) {
308
+ attrs.push(key);
309
+ } else if (typeof value === 'number') {
310
+ attrs.push(key + '=' + value);
311
+ } else {
312
+ attrs.push(key + '=' + yamlEscape(String(value)));
313
+ }
314
+ }
315
+ }
316
+ }
317
+ attrs.push('ref=' + node.ref);
318
+
319
+ // Add cursor style like Playwright
320
+ if (node.cursor) {
321
+ attrs.push('cursor=' + node.cursor);
322
+ }
323
+
324
+ if (attrs.length > 0) {
325
+ line += ' [' + attrs.join('] [') + ']';
326
+ }
327
+
328
+ // Check if we have children or URL/description to add
329
+ var hasChildren = node.children && node.children.length > 0;
330
+ var hasUrl = !!node.url;
331
+ var hasDescription = !!node.description;
332
+
333
+ if (hasChildren || hasUrl || hasDescription) {
334
+ line += ':\n';
335
+ // Add URL as child like Playwright
336
+ if (hasUrl) {
337
+ line += prefix + ' - /url: ' + node.url + '\n';
338
+ }
339
+ // Add description as child if present
340
+ if (hasDescription) {
341
+ line += prefix + ' - /description: ' + yamlEscape(node.description) + '\n';
342
+ }
343
+ // Render children
344
+ if (hasChildren) {
345
+ line += node.children.map(function(c) { return renderYaml(c, indent + 1); }).join('');
346
+ }
347
+ } else {
348
+ line += '\n';
349
+ }
350
+
351
+ return line;
352
+ }
353
+
354
+ // ========================================================================
355
+ // Main Execution
356
+ // ========================================================================
357
+
358
+ if (type !== 'accessibility' && type !== 'structure') {
359
+ throw new Error('Unsupported snapshot type: "' + type + '". Supported: \'accessibility\', \'structure\'');
360
+ }
361
+
362
+ // ========================================================================
363
+ // Structure Snapshot (DOM structure tree)
364
+ // ========================================================================
365
+
366
+ function isStructureVisible(element) {
367
+ if (element.hidden) return false;
368
+ try {
369
+ var style = window.getComputedStyle(element);
370
+ if (style.display === 'none') return false;
371
+ if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
372
+ } catch (e) {
373
+ return false;
374
+ }
375
+ return true;
376
+ }
377
+
378
+ function buildStructureTree(element, depth, maxDepth) {
379
+ depth = depth || 0;
380
+ maxDepth = maxDepth || 100;
381
+
382
+ if (depth > maxDepth) return { type: 'error', message: 'Max depth exceeded' };
383
+ if (!isStructureVisible(element)) return null;
384
+
385
+ var tag = element.tagName.toLowerCase();
386
+ var id = element.id || undefined;
387
+ var classes = element.classList.length > 0
388
+ ? Array.from(element.classList)
389
+ : undefined;
390
+ var testId = element.getAttribute('data-testid') || undefined;
391
+ var ref = getOrCreateRef(element);
392
+
393
+ var children = [];
394
+ for (var i = 0; i < element.children.length; i++) {
395
+ var childTree = buildStructureTree(element.children[i], depth + 1, maxDepth);
396
+ if (childTree) children.push(childTree);
397
+ }
398
+
399
+ return {
400
+ type: 'element',
401
+ tag: tag,
402
+ id: id,
403
+ classes: classes,
404
+ testId: testId,
405
+ ref: ref,
406
+ children: children.length > 0 ? children : undefined
407
+ };
408
+ }
409
+
410
+ function renderStructureYaml(node, indent) {
411
+ indent = indent || 0;
412
+ var prefix = ' '.repeat(indent);
413
+ if (!node) return '';
414
+
415
+ if (node.type === 'error') {
416
+ return prefix + '# ERROR: ' + node.message + '\n';
417
+ }
418
+
419
+ // Build element descriptor: tag#id.class1.class2
420
+ var descriptor = node.tag;
421
+ if (node.id) descriptor += '#' + node.id;
422
+ if (node.classes) descriptor += '.' + node.classes.join('.');
423
+
424
+ var attrs = ['ref=' + node.ref];
425
+ if (node.testId) attrs.push('data-testid=' + yamlEscape(node.testId));
426
+
427
+ var line = prefix + '- ' + descriptor + ' [' + attrs.join(' ') + ']';
428
+
429
+ if (node.children && node.children.length > 0) {
430
+ line += ':\n' + node.children.map(function(c) {
431
+ return renderStructureYaml(c, indent + 1);
432
+ }).join('');
433
+ } else {
434
+ line += '\n';
435
+ }
436
+
437
+ return line;
438
+ }
439
+
440
+ // For structure type, we don't need aria-api
441
+ if (type === 'structure') {
442
+ var structureRoots = [];
443
+ var structureScopeInfo = '';
444
+
445
+ if (selector) {
446
+ try {
447
+ document.querySelector(selector);
448
+ } catch (e) {
449
+ return 'Error: Invalid CSS selector "' + selector + '": ' + e.message;
450
+ }
451
+
452
+ var structureElements = document.querySelectorAll(selector);
453
+ if (structureElements.length === 0) {
454
+ return 'Error: No elements found matching selector "' + selector + '"';
455
+ }
456
+
457
+ structureRoots = Array.from(structureElements);
458
+ structureScopeInfo = '# Scoped to: ' + selector + '\n';
459
+ if (structureRoots.length > 1) structureScopeInfo += '# ' + structureRoots.length + ' elements matched\n';
460
+ } else {
461
+ structureRoots = [document.body];
462
+ }
463
+
464
+ var structureOutput = structureScopeInfo;
465
+
466
+ structureRoots.forEach(function(root, index) {
467
+ if (structureRoots.length > 1) structureOutput += '\n# ─── Match ' + (index + 1) + ' of ' + structureRoots.length + ' ───\n';
468
+
469
+ try {
470
+ var tree = buildStructureTree(root);
471
+ structureOutput += tree ? renderStructureYaml(tree) : '# (empty or hidden)\n';
472
+ } catch (e) {
473
+ structureOutput += '# ERROR: ' + e.message + '\n';
474
+ }
475
+ });
476
+
477
+ structureOutput += '\n# ───────────────────────────────────────\n';
478
+ structureOutput += '# Generated: ' + new Date().toISOString() + '\n';
479
+ structureOutput += '# Elements indexed: ' + refCounter + '\n';
480
+ structureOutput += '# Use [ref=eN] with other webview tools\n';
481
+
482
+ return structureOutput.trim();
483
+ }
484
+
485
+ // ========================================================================
486
+ // Accessibility Snapshot (requires aria-api)
487
+ // ========================================================================
488
+
489
+ // Validate aria-api is available
490
+ var ariaApi = window.ariaApi;
491
+ if (!ariaApi) {
492
+ throw new Error('aria-api library not loaded');
493
+ }
494
+
495
+ var roots = [];
496
+ var scopeInfo = '';
497
+
498
+ if (selector) {
499
+ try {
500
+ document.querySelector(selector);
501
+ } catch (e) {
502
+ return 'Error: Invalid CSS selector "' + selector + '": ' + e.message;
503
+ }
504
+
505
+ var elements = document.querySelectorAll(selector);
506
+ if (elements.length === 0) {
507
+ return 'Error: No elements found matching selector "' + selector + '"';
508
+ }
509
+
510
+ roots = Array.from(elements);
511
+ scopeInfo = '# Scoped to: ' + selector + '\n';
512
+ if (roots.length > 1) scopeInfo += '# ' + roots.length + ' elements matched\n';
513
+ } else {
514
+ roots = [document.body];
515
+ }
516
+
517
+ var output = scopeInfo;
518
+
519
+ roots.forEach(function(root, index) {
520
+ if (roots.length > 1) output += '\n# ─── Match ' + (index + 1) + ' of ' + roots.length + ' ───\n';
521
+
522
+ try {
523
+ var tree = buildAriaTree(root);
524
+ output += tree ? renderYaml(tree) : '# (empty or hidden)\n';
525
+ } catch (e) {
526
+ output += '# ERROR: ' + e.message + '\n';
527
+ }
528
+ });
529
+
530
+ output += '\n# ───────────────────────────────────────\n';
531
+ output += '# Generated: ' + new Date().toISOString() + '\n';
532
+ output += '# Elements indexed: ' + refCounter + '\n';
533
+ output += '# Use [ref=eN] with other webview tools\n';
534
+
535
+ return output.trim();
536
+ })
@@ -2,14 +2,22 @@
2
2
  * Find an element using various selector strategies
3
3
  *
4
4
  * @param {Object} params
5
- * @param {string} params.selector - Element selector
5
+ * @param {string} params.selector - Element selector, ref ID (e.g., "ref=e3"), or text
6
6
  * @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
7
7
  */
8
8
  (function(params) {
9
9
  const { selector, strategy } = params;
10
10
  let element;
11
11
 
12
- if (strategy === 'text') {
12
+ // Check if it's a ref ID first (works with any strategy)
13
+ const refMatch = selector.match(/^(?:ref=)?(e\d+)$/);
14
+ if (refMatch) {
15
+ const refId = refMatch[1],
16
+ refMap = window.__MCP_ARIA_REFS_REVERSE__;
17
+ if (refMap) {
18
+ element = refMap.get(refId);
19
+ }
20
+ } else if (strategy === 'text') {
13
21
  // Find element containing text
14
22
  const xpath = "//*[contains(text(), '" + selector + "')]";
15
23
  const result = document.evaluate(
@@ -2,16 +2,29 @@
2
2
  * Focus an element
3
3
  *
4
4
  * @param {Object} params
5
- * @param {string} params.selector - CSS selector for element to focus
5
+ * @param {string} params.selector - CSS selector or ref ID (e.g., "ref=e3") for element to focus
6
6
  */
7
7
  (function(params) {
8
8
  const { selector } = params;
9
9
 
10
- const element = document.querySelector(selector);
11
- if (!element) {
12
- throw new Error(`Element not found: ${selector}`);
10
+ // Resolve element from CSS selector or ref ID (e.g., "ref=e3" or "e3")
11
+ function resolveElement(selectorOrRef) {
12
+ if (!selectorOrRef) return null;
13
+ var refMatch = selectorOrRef.match(/^(?:ref=)?(e\d+)$/);
14
+ if (refMatch) {
15
+ var refId = refMatch[1],
16
+ refMap = window.__MCP_ARIA_REFS_REVERSE__;
17
+ if (!refMap) throw new Error('Ref "' + refId + '" not found. Run webview_dom_snapshot first to index elements.');
18
+ var el = refMap.get(refId);
19
+ if (!el) throw new Error('Ref "' + refId + '" not found. The DOM may have changed since the snapshot.');
20
+ return el;
21
+ }
22
+ var el = document.querySelector(selectorOrRef);
23
+ if (!el) throw new Error('Element not found: ' + selectorOrRef);
24
+ return el;
13
25
  }
14
26
 
27
+ const element = resolveElement(selector);
15
28
  element.focus();
16
29
  return `Focused element: ${selector}`;
17
30
  })
@@ -2,16 +2,35 @@
2
2
  * Get computed CSS styles for elements
3
3
  *
4
4
  * @param {Object} params
5
- * @param {string} params.selector - CSS selector for element(s)
5
+ * @param {string} params.selector - CSS selector or ref ID (e.g., "ref=e3") for element(s)
6
6
  * @param {string[]} params.properties - Specific CSS properties to retrieve
7
7
  * @param {boolean} params.multiple - Whether to get styles for all matching elements
8
8
  */
9
9
  (function(params) {
10
10
  const { selector, properties, multiple } = params;
11
11
 
12
- const elements = multiple
13
- ? Array.from(document.querySelectorAll(selector))
14
- : [document.querySelector(selector)];
12
+ // Resolve element from CSS selector or ref ID (e.g., "ref=e3" or "e3")
13
+ function resolveElement(selectorOrRef) {
14
+ if (!selectorOrRef) return null;
15
+ var refMatch = selectorOrRef.match(/^(?:ref=)?(e\d+)$/);
16
+ if (refMatch) {
17
+ var refId = refMatch[1],
18
+ refMap = window.__MCP_ARIA_REFS_REVERSE__;
19
+ if (!refMap) throw new Error('Ref "' + refId + '" not found. Run webview_dom_snapshot first to index elements.');
20
+ var el = refMap.get(refId);
21
+ if (!el) throw new Error('Ref "' + refId + '" not found. The DOM may have changed since the snapshot.');
22
+ return el;
23
+ }
24
+ var el = document.querySelector(selectorOrRef);
25
+ if (!el) throw new Error('Element not found: ' + selectorOrRef);
26
+ return el;
27
+ }
28
+
29
+ // Check if selector is a ref ID - if so, multiple doesn't apply
30
+ const isRef = /^(?:ref=)?(e\d+)$/.test(selector);
31
+ const elements = isRef
32
+ ? [resolveElement(selector)]
33
+ : (multiple ? Array.from(document.querySelectorAll(selector)) : [document.querySelector(selector)]);
15
34
 
16
35
  if (!elements[0]) {
17
36
  throw new Error(`Element not found: ${selector}`);
@@ -20,6 +20,7 @@ export const SCRIPTS = {
20
20
  getStyles: loadScript('get-styles'),
21
21
  focus: loadScript('focus'),
22
22
  findElement: loadScript('find-element'),
23
+ domSnapshot: loadScript('dom-snapshot'),
23
24
  };
24
25
  /**
25
26
  * Build a script invocation with parameters