1pub struct BridgeCapacities {
3 pub console_logs: usize,
5 pub mutation_log: usize,
7 pub network_log: usize,
9 pub navigation_log: usize,
11 pub dialog_log: usize,
13 pub long_tasks: usize,
15}
16
17impl Default for BridgeCapacities {
18 fn default() -> Self {
19 Self {
20 console_logs: 1000,
21 mutation_log: 500,
22 network_log: 1000,
23 navigation_log: 200,
24 dialog_log: 100,
25 long_tasks: 100,
26 }
27 }
28}
29
30#[must_use]
32pub fn init_script(caps: &BridgeCapacities) -> String {
33 format!(
34 "\n(function() {{\
35 \n if (window.__VICTAURI__) return;\
36 \n\
37 \n var CAP_CONSOLE = {console_logs};\
38 \n var CAP_MUTATION = {mutation_log};\
39 \n var CAP_NETWORK = {network_log};\
40 \n var CAP_NAVIGATION = {navigation_log};\
41 \n var CAP_DIALOG = {dialog_log};\
42 \n var CAP_LONG_TASKS = {long_tasks};\
43 \n",
44 console_logs = caps.console_logs,
45 mutation_log = caps.mutation_log,
46 network_log = caps.network_log,
47 navigation_log = caps.navigation_log,
48 dialog_log = caps.dialog_log,
49 long_tasks = caps.long_tasks,
50 ) + INIT_SCRIPT_BODY
51}
52
53const INIT_SCRIPT_BODY: &str = r#"
56 var refMap = new Map();
57 var refCounter = 0;
58 var weakRefMap = new Map();
59
60 function resolveRef(refId) {
61 var direct = refMap.get(refId);
62 if (direct) {
63 if (direct.isConnected) return direct;
64 refMap.delete(refId);
65 return null;
66 }
67 var weak = weakRefMap.get(refId);
68 if (weak) {
69 var el = weak.deref();
70 if (el && el.isConnected) return el;
71 weakRefMap.delete(refId);
72 return null;
73 }
74 return null;
75 }
76
77 var REF_MAP_LIMIT = 10000;
78
79 function registerRef(node) {
80 var ref_id = 'e' + (refCounter++);
81 if (refMap.size >= REF_MAP_LIMIT) {
82 var oldest = refMap.keys().next().value;
83 refMap.delete(oldest);
84 weakRefMap.delete(oldest);
85 }
86 refMap.set(ref_id, node);
87 if (typeof WeakRef !== 'undefined') {
88 weakRefMap.set(ref_id, new WeakRef(node));
89 }
90 return ref_id;
91 }
92
93 function getStaleRefs() {
94 var stale = [];
95 weakRefMap.forEach(function(weak, refId) {
96 var el = weak.deref();
97 if (!el || !el.isConnected) {
98 stale.push(refId);
99 weakRefMap.delete(refId);
100 refMap.delete(refId);
101 }
102 });
103 return stale;
104 }
105 var consoleLogs = [];
106 var mutationLog = [];
107 var networkLog = [];
108 var networkCounter = 0;
109 var navigationLog = [];
110 var dialogLog = [];
111 var interactionLog = [];
112 var CAP_INTERACTION = 500;
113
114 function checkActionable(el) {
115 if (!el || !el.isConnected) return { error: 'element is detached from DOM', hint: 'RETRY_LATER' };
116 if (el.disabled) return { error: 'element is disabled (disabled attribute)', hint: 'RETRY_LATER' };
117 if (el.getAttribute && el.getAttribute('aria-disabled') === 'true') return { error: 'element is disabled (aria-disabled)', hint: 'RETRY_LATER' };
118 var cs = window.getComputedStyle(el);
119 if (cs.display === 'none') return { error: 'element is not visible (display: none)', hint: 'RETRY_LATER' };
120 if (cs.visibility === 'hidden') return { error: 'element is not visible (visibility: hidden)', hint: 'RETRY_LATER' };
121 if (parseFloat(cs.opacity) < 0.01) return { error: 'element is not visible (opacity: ' + cs.opacity + ')', hint: 'RETRY_LATER' };
122 var rect = el.getBoundingClientRect();
123 if (rect.width === 0 && rect.height === 0) return { error: 'element has zero size', hint: 'RETRY_LATER' };
124 if (cs.pointerEvents === 'none') return { error: 'element has pointer-events: none', hint: 'RETRY_LATER' };
125 var vw = window.innerWidth || document.documentElement.clientWidth;
126 var vh = window.innerHeight || document.documentElement.clientHeight;
127 if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
128 el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
129 rect = el.getBoundingClientRect();
130 if (rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw) {
131 return { error: 'element is outside viewport after scroll attempt', hint: 'CHECK_INPUT' };
132 }
133 }
134 var cx = rect.left + rect.width / 2;
135 var cy = rect.top + rect.height / 2;
136 var topEl = document.elementFromPoint(cx, cy);
137 if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
138 var tag = topEl.tagName ? topEl.tagName.toLowerCase() : 'unknown';
139 var info = tag;
140 if (topEl.id) info += '#' + topEl.id;
141 else if (topEl.className && typeof topEl.className === 'string') {
142 var cls = topEl.className.trim().split(/\s+/)[0];
143 if (cls) info += '.' + cls;
144 }
145 return { error: 'element is covered by ' + info + ' at (' + Math.round(cx) + ',' + Math.round(cy) + ')', hint: 'RETRY_LATER' };
146 }
147 return null;
148 }
149
150 function withAutoWait(refId, timeoutMs, actionFn) {
151 return new Promise(function(resolve) {
152 var deadline = Date.now() + (timeoutMs || 5000);
153 function attempt() {
154 var el = resolveRef(refId);
155 if (!el) {
156 if (Date.now() >= deadline) { resolve({ ok: false, error: 'ref not found: ' + refId, hint: 'CHECK_INPUT' }); return; }
157 setTimeout(attempt, 50); return;
158 }
159 var check = checkActionable(el);
160 if (check) {
161 if (check.hint === 'CHECK_INPUT' || Date.now() >= deadline) {
162 var msg = Date.now() >= deadline ? 'timeout (' + (timeoutMs || 5000) + 'ms): ' + check.error : check.error;
163 resolve({ ok: false, error: msg, hint: check.hint || 'RETRY_LATER' }); return;
164 }
165 setTimeout(attempt, 50); return;
166 }
167 try { var r = actionFn(el); resolve(r || { ok: true }); }
168 catch (e) { resolve({ ok: false, error: 'action threw: ' + e.message, hint: 'CHECK_INPUT' }); }
169 }
170 attempt();
171 });
172 }
173
174 // ── Public API ───────────────────────────────────────────────────────────
175
176 window.__VICTAURI__ = {
177 version: '0.3.0',
178
179 // ── DOM ──────────────────────────────────────────────────────────────
180
181 snapshot: function(format) {
182 var previousRefs = new Set(refMap.keys());
183 refMap.clear();
184 var fmt = format || 'compact';
185 var tree;
186 if (fmt === 'json') {
187 tree = walkDom(document.body);
188 } else {
189 tree = walkDomCompact(document.body, 0);
190 }
191 var currentRefs = new Set(refMap.keys());
192 var stale = [];
193 previousRefs.forEach(function(refId) {
194 if (!currentRefs.has(refId)) {
195 var weak = weakRefMap.get(refId);
196 if (weak) {
197 var el = weak.deref();
198 if (!el || !el.isConnected) {
199 stale.push(refId);
200 weakRefMap.delete(refId);
201 }
202 } else {
203 stale.push(refId);
204 }
205 }
206 });
207 return { tree: tree, stale_refs: stale, format: fmt };
208 },
209
210 getRef: function(refId) {
211 return resolveRef(refId);
212 },
213
214 getStaleRefs: function() {
215 return getStaleRefs();
216 },
217
218 findElements: function(query) {
219 var results = [];
220 var maxResults = query.max_results || 10;
221
222 function matches(el) {
223 if (query.text) {
224 var txt = (el.textContent || '').trim();
225 if (txt.toLowerCase().indexOf(query.text.toLowerCase()) === -1) return false;
226 }
227 if (query.role) {
228 var role = el.getAttribute('role') || inferRole(el);
229 if (role !== query.role) return false;
230 }
231 if (query.test_id) {
232 if (el.getAttribute('data-testid') !== query.test_id) return false;
233 }
234 if (query.css) {
235 try { if (!el.matches(query.css)) return false; } catch(e) { return false; }
236 }
237 if (query.name) {
238 var name = el.getAttribute('aria-label')
239 || el.getAttribute('title')
240 || el.getAttribute('placeholder') || '';
241 if (name.toLowerCase().indexOf(query.name.toLowerCase()) === -1) return false;
242 }
243 return true;
244 }
245
246 function search(node) {
247 if (results.length >= maxResults) return;
248 if (!node || node.nodeType !== 1) return;
249 var style = window.getComputedStyle(node);
250 if (style.display === 'none' || style.visibility === 'hidden') return;
251
252 if (matches(node)) {
253 var existingRef = null;
254 refMap.forEach(function(el, refId) {
255 if (el === node) existingRef = refId;
256 });
257 var ref_id = existingRef || registerRef(node);
258 var role = node.getAttribute('role') || inferRole(node);
259 var rect = node.getBoundingClientRect();
260 results.push({
261 ref_id: ref_id,
262 tag: node.tagName.toLowerCase(),
263 role: role,
264 name: node.getAttribute('aria-label') || node.getAttribute('title') || null,
265 text: (node.textContent || '').trim().substring(0, 100),
266 bounds: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }
267 });
268 }
269
270 for (var c = 0; c < node.children.length; c++) {
271 search(node.children[c]);
272 }
273 if (node.shadowRoot) {
274 for (var s = 0; s < node.shadowRoot.children.length; s++) {
275 search(node.shadowRoot.children[s]);
276 }
277 }
278 }
279
280 search(document.body);
281 return results;
282 },
283
284 // ── Interactions ─────────────────────────────────────────────────────
285
286 click: function(refId, timeoutMs) {
287 return withAutoWait(refId, timeoutMs, function(el) {
288 el.click();
289 return { ok: true };
290 });
291 },
292
293 doubleClick: function(refId, timeoutMs) {
294 return withAutoWait(refId, timeoutMs, function(el) {
295 el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
296 return { ok: true };
297 });
298 },
299
300 hover: function(refId, timeoutMs) {
301 return withAutoWait(refId, timeoutMs, function(el) {
302 el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
303 el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
304 return { ok: true };
305 });
306 },
307
308 fill: function(refId, value, timeoutMs) {
309 return withAutoWait(refId, timeoutMs, function(el) {
310 if (!el.matches('input, textarea, [contenteditable="true"]')) {
311 return { ok: false, error: 'element is not fillable (not input, textarea, or contenteditable): ' + (el.tagName || '').toLowerCase(), hint: 'CHECK_INPUT' };
312 }
313 var proto = el instanceof HTMLTextAreaElement
314 ? HTMLTextAreaElement.prototype
315 : HTMLInputElement.prototype;
316 var desc = Object.getOwnPropertyDescriptor(proto, 'value');
317 if (desc && desc.set) {
318 desc.set.call(el, value);
319 } else {
320 el.value = value;
321 }
322 el.dispatchEvent(new Event('input', { bubbles: true }));
323 el.dispatchEvent(new Event('change', { bubbles: true }));
324 return { ok: true };
325 });
326 },
327
328 type: function(refId, text, timeoutMs) {
329 return withAutoWait(refId, timeoutMs, function(el) {
330 el.focus();
331 var proto = el instanceof HTMLTextAreaElement
332 ? HTMLTextAreaElement.prototype
333 : HTMLInputElement.prototype;
334 var desc = Object.getOwnPropertyDescriptor(proto, 'value');
335 for (var i = 0; i < text.length; i++) {
336 var ch = text[i];
337 el.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true }));
338 el.dispatchEvent(new KeyboardEvent('keypress', { key: ch, bubbles: true }));
339 var current = el.value || '';
340 if (desc && desc.set) {
341 desc.set.call(el, current + ch);
342 } else {
343 el.value = current + ch;
344 }
345 el.dispatchEvent(new InputEvent('input', { bubbles: true, data: ch, inputType: 'insertText' }));
346 el.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true }));
347 }
348 el.dispatchEvent(new Event('change', { bubbles: true }));
349 return { ok: true };
350 });
351 },
352
353 pressKey: function(key) {
354 var target = document.activeElement || document.body;
355 target.dispatchEvent(new KeyboardEvent('keydown', { key: key, bubbles: true }));
356 target.dispatchEvent(new KeyboardEvent('keyup', { key: key, bubbles: true }));
357 return { ok: true };
358 },
359
360 selectOption: function(refId, values, timeoutMs) {
361 return withAutoWait(refId, timeoutMs, function(el) {
362 if (el.tagName !== 'SELECT') {
363 return { ok: false, error: 'element is not a <select>', hint: 'CHECK_INPUT' };
364 }
365 var valSet = new Set(values);
366 for (var i = 0; i < el.options.length; i++) {
367 el.options[i].selected = valSet.has(el.options[i].value);
368 }
369 el.dispatchEvent(new Event('change', { bubbles: true }));
370 return { ok: true };
371 });
372 },
373
374 scrollTo: function(refId, x, y, timeoutMs) {
375 if (refId) {
376 return withAutoWait(refId, timeoutMs, function(el) {
377 el.scrollIntoView({ behavior: 'smooth', block: 'center' });
378 return { ok: true };
379 });
380 } else {
381 window.scrollTo({ left: x || 0, top: y || 0, behavior: 'smooth' });
382 return Promise.resolve({ ok: true });
383 }
384 },
385
386 focusElement: function(refId, timeoutMs) {
387 return withAutoWait(refId, timeoutMs, function(el) {
388 el.focus();
389 return { ok: true, tag: el.tagName.toLowerCase() };
390 });
391 },
392
393 // ── IPC Log ──────────────────────────────────────────────────────────
394
395 getIpcLog: function(limit) {
396 var ipcPrefix = 'http://ipc.localhost/';
397 var victauriPrefix = 'plugin%3Avictauri%7C';
398 var entries = [];
399 for (var i = 0; i < networkLog.length; i++) {
400 var n = networkLog[i];
401 if (n.url.indexOf(ipcPrefix) !== 0) continue;
402 var raw = n.url.substring(ipcPrefix.length);
403 if (raw.indexOf(victauriPrefix) === 0) continue;
404 var command;
405 try { command = decodeURIComponent(raw); } catch(e) { command = raw; }
406 entries.push({
407 id: n.id,
408 command: command,
409 args: n.request_args || {},
410 timestamp: n.timestamp,
411 status: n.status === 200 ? 'ok' : (n.status === 'pending' ? 'pending' : 'error'),
412 duration_ms: n.duration_ms,
413 result: n.response_body || null,
414 error: n.status !== 200 && n.status !== 'pending' ? 'HTTP ' + n.status : null,
415 });
416 }
417 if (limit) return entries.slice(-limit);
418 return entries;
419 },
420
421 clearIpcLog: function() {
422 var ipcPrefix = 'http://ipc.localhost/';
423 for (var i = networkLog.length - 1; i >= 0; i--) {
424 if (networkLog[i].url.indexOf(ipcPrefix) === 0) networkLog.splice(i, 1);
425 }
426 },
427
428 // ── Console ──────────────────────────────────────────────────────────
429
430 getConsoleLogs: function(since) {
431 if (since) return consoleLogs.filter(function(l) { return l.timestamp >= since; });
432 return consoleLogs;
433 },
434
435 clearConsoleLogs: function() {
436 consoleLogs.length = 0;
437 },
438
439 // ── Mutations ────────────────────────────────────────────────────────
440
441 getMutationLog: function(since) {
442 if (since) return mutationLog.filter(function(m) { return m.timestamp >= since; });
443 return mutationLog;
444 },
445
446 clearMutationLog: function() {
447 mutationLog.length = 0;
448 },
449
450 // ── Network ──────────────────────────────────────────────────────────
451
452 getNetworkLog: function(filter, limit) {
453 var log = networkLog;
454 if (filter) {
455 log = log.filter(function(e) { return e.url.indexOf(filter) !== -1; });
456 }
457 if (limit) log = log.slice(-limit);
458 return log;
459 },
460
461 clearNetworkLog: function() {
462 networkLog.length = 0;
463 },
464
465 // ── Storage ──────────────────────────────────────────────────────────
466
467 getLocalStorage: function(key) {
468 if (key !== undefined && key !== null) {
469 var v = localStorage.getItem(key);
470 try { return JSON.parse(v); } catch(e) { return v; }
471 }
472 var obj = {};
473 for (var i = 0; i < localStorage.length; i++) {
474 var k = localStorage.key(i);
475 var val = localStorage.getItem(k);
476 try { obj[k] = JSON.parse(val); } catch(e) { obj[k] = val; }
477 }
478 return obj;
479 },
480
481 setLocalStorage: function(key, value) {
482 localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
483 return { ok: true };
484 },
485
486 deleteLocalStorage: function(key) {
487 localStorage.removeItem(key);
488 return { ok: true };
489 },
490
491 getSessionStorage: function(key) {
492 if (key !== undefined && key !== null) {
493 var v = sessionStorage.getItem(key);
494 try { return JSON.parse(v); } catch(e) { return v; }
495 }
496 var obj = {};
497 for (var i = 0; i < sessionStorage.length; i++) {
498 var k = sessionStorage.key(i);
499 var val = sessionStorage.getItem(k);
500 try { obj[k] = JSON.parse(val); } catch(e) { obj[k] = val; }
501 }
502 return obj;
503 },
504
505 setSessionStorage: function(key, value) {
506 sessionStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
507 return { ok: true };
508 },
509
510 deleteSessionStorage: function(key) {
511 sessionStorage.removeItem(key);
512 return { ok: true };
513 },
514
515 getCookies: function() {
516 if (!document.cookie) return [];
517 return document.cookie.split(';').map(function(c) {
518 var parts = c.trim().split('=');
519 return { name: parts[0], value: parts.slice(1).join('=') };
520 });
521 },
522
523 // ── Navigation ───────────────────────────────────────────────────────
524
525 getNavigationLog: function() {
526 return navigationLog;
527 },
528
529 navigate: function(url) {
530 window.location.href = url;
531 return { ok: true };
532 },
533
534 navigateBack: function() {
535 history.back();
536 return { ok: true };
537 },
538
539 // ── Dialogs ──────────────────────────────────────────────────────────
540
541 getDialogLog: function() {
542 return dialogLog;
543 },
544
545 clearDialogLog: function() {
546 dialogLog.length = 0;
547 },
548
549 setDialogAutoResponse: function(type, action, text) {
550 dialogAutoResponses[type] = { action: action, text: text };
551 return { ok: true };
552 },
553
554 // ── Combined Event Stream ────────────────────────────────────────────
555
556 getEventStream: function(since) {
557 var events = [];
558 var ts = since || 0;
559
560 consoleLogs.forEach(function(l) {
561 if (l.timestamp >= ts) {
562 events.push({ type: 'console', level: l.level, message: l.message, timestamp: l.timestamp });
563 }
564 });
565
566 mutationLog.forEach(function(m) {
567 if (m.timestamp >= ts) {
568 events.push({ type: 'dom_mutation', count: m.count, timestamp: m.timestamp });
569 }
570 });
571
572 var ipcPrefix = 'http://ipc.localhost/';
573 var victauriPrefix = 'plugin%3Avictauri%7C';
574 networkLog.forEach(function(n) {
575 if (n.timestamp >= ts && n.url.indexOf(ipcPrefix) === 0) {
576 var raw = n.url.substring(ipcPrefix.length);
577 if (raw.indexOf(victauriPrefix) === 0) return;
578 var cmd; try { cmd = decodeURIComponent(raw); } catch(e) { cmd = raw; }
579 events.push({ type: 'ipc', command: cmd, status: n.status === 200 ? 'ok' : (n.status === 'pending' ? 'pending' : 'error'), duration_ms: n.duration_ms, timestamp: n.timestamp });
580 }
581 });
582
583 networkLog.forEach(function(n) {
584 if (n.timestamp >= ts) {
585 events.push({ type: 'network', method: n.method, url: n.url, status: n.status, duration_ms: n.duration_ms, timestamp: n.timestamp });
586 }
587 });
588
589 navigationLog.forEach(function(n) {
590 if (n.timestamp >= ts) {
591 events.push({ type: 'navigation', url: n.url, nav_type: n.type, timestamp: n.timestamp });
592 }
593 });
594
595 interactionLog.forEach(function(i) {
596 if (i.timestamp >= ts) {
597 events.push({ type: 'dom_interaction', action: i.action, selector: i.selector, value: i.value, timestamp: i.timestamp });
598 }
599 });
600
601 events.sort(function(a, b) { return a.timestamp - b.timestamp; });
602 return events;
603 },
604
605 // ── Wait ─────────────────────────────────────────────────────────────
606
607 waitFor: function(opts) {
608 return new Promise(function(resolve) {
609 var timeout = opts.timeout_ms || 10000;
610 var poll = opts.poll_ms || 200;
611 var start = Date.now();
612
613 function check() {
614 var elapsed = Date.now() - start;
615 if (elapsed >= timeout) {
616 resolve({ ok: false, error: 'timeout after ' + timeout + 'ms', elapsed_ms: elapsed });
617 return;
618 }
619
620 function getFullText(root) {
621 var text = root.innerText || '';
622 var els = root.querySelectorAll('*');
623 for (var j = 0; j < els.length; j++) {
624 if (els[j].shadowRoot) text += ' ' + getFullText(els[j].shadowRoot);
625 }
626 return text;
627 }
628 var met = false;
629 if (opts.condition === 'text' && opts.value) {
630 met = getFullText(document.body).indexOf(opts.value) !== -1;
631 } else if (opts.condition === 'text_gone' && opts.value) {
632 met = getFullText(document.body).indexOf(opts.value) === -1;
633 } else if (opts.condition === 'selector' && opts.value) {
634 met = !!document.querySelector(opts.value);
635 } else if (opts.condition === 'selector_gone' && opts.value) {
636 met = !document.querySelector(opts.value);
637 } else if (opts.condition === 'url' && opts.value) {
638 met = window.location.href.indexOf(opts.value) !== -1;
639 } else if (opts.condition === 'ipc_idle') {
640 met = networkLog.filter(function(n) { return n.url.indexOf('http://ipc.localhost/') === 0; }).every(function(n) { return n.status !== 'pending'; });
641 } else if (opts.condition === 'network_idle') {
642 met = networkLog.every(function(n) { return n.status !== 'pending'; });
643 }
644
645 if (met) {
646 resolve({ ok: true, elapsed_ms: Date.now() - start });
647 } else {
648 setTimeout(check, poll);
649 }
650 }
651 check();
652 });
653 },
654 // ── CSS / Style Introspection ────────────────────────────────────────
655
656 getStyles: function(refId, properties) {
657 var el = resolveRef(refId);
658 if (!el) return { error: 'ref not found: ' + refId };
659 var computed = window.getComputedStyle(el);
660 var result = {};
661 if (properties && properties.length > 0) {
662 for (var i = 0; i < properties.length; i++) {
663 result[properties[i]] = computed.getPropertyValue(properties[i]);
664 }
665 } else {
666 var important = ['display','position','width','height','margin','padding',
667 'color','background-color','font-size','font-family','font-weight',
668 'border','border-radius','opacity','visibility','overflow','z-index',
669 'flex-direction','justify-content','align-items','gap','grid-template-columns',
670 'box-shadow','transform','transition','cursor','pointer-events','text-align',
671 'line-height','letter-spacing','white-space','text-overflow','max-width',
672 'max-height','min-width','min-height','top','right','bottom','left'];
673 for (var i = 0; i < important.length; i++) {
674 var v = computed.getPropertyValue(important[i]);
675 if (v && v !== '' && v !== 'none' && v !== 'normal' && v !== 'auto' && v !== '0px' && v !== 'rgba(0, 0, 0, 0)') {
676 result[important[i]] = v;
677 }
678 }
679 }
680 return { ref_id: refId, tag: el.tagName.toLowerCase(), styles: result };
681 },
682
683 getBoundingBoxes: function(refIds) {
684 var results = [];
685 for (var i = 0; i < refIds.length; i++) {
686 var el = resolveRef(refIds[i]);
687 if (!el) { results.push({ ref_id: refIds[i], error: 'ref not found' }); continue; }
688 var rect = el.getBoundingClientRect();
689 var computed = window.getComputedStyle(el);
690 results.push({
691 ref_id: refIds[i],
692 tag: el.tagName.toLowerCase(),
693 x: Math.round(rect.x),
694 y: Math.round(rect.y),
695 width: Math.round(rect.width),
696 height: Math.round(rect.height),
697 margin: {
698 top: parseInt(computed.marginTop) || 0,
699 right: parseInt(computed.marginRight) || 0,
700 bottom: parseInt(computed.marginBottom) || 0,
701 left: parseInt(computed.marginLeft) || 0,
702 },
703 padding: {
704 top: parseInt(computed.paddingTop) || 0,
705 right: parseInt(computed.paddingRight) || 0,
706 bottom: parseInt(computed.paddingBottom) || 0,
707 left: parseInt(computed.paddingLeft) || 0,
708 },
709 border: {
710 top: parseInt(computed.borderTopWidth) || 0,
711 right: parseInt(computed.borderRightWidth) || 0,
712 bottom: parseInt(computed.borderBottomWidth) || 0,
713 left: parseInt(computed.borderLeftWidth) || 0,
714 },
715 });
716 }
717 return results;
718 },
719
720 // ── Visual Debug Overlays ────────────────────────────────────────────
721
722 highlightElement: function(refId, color, label) {
723 var el = resolveRef(refId);
724 if (!el) return { error: 'ref not found: ' + refId };
725 var c = color || 'rgba(255, 0, 0, 0.3)';
726 var overlay = document.createElement('div');
727 overlay.className = '__victauri_highlight__';
728 overlay.setAttribute('data-victauri-ref', refId);
729 var rect = el.getBoundingClientRect();
730 overlay.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483647;' +
731 'border:2px solid ' + c + ';background:' + c + ';' +
732 'left:' + rect.left + 'px;top:' + rect.top + 'px;' +
733 'width:' + rect.width + 'px;height:' + rect.height + 'px;' +
734 'transition:all 0.2s ease;';
735 if (label) {
736 var tag = document.createElement('span');
737 tag.textContent = label;
738 tag.style.cssText = 'position:absolute;top:-20px;left:0;background:#222;color:#fff;' +
739 'font-size:11px;padding:2px 6px;border-radius:3px;white-space:nowrap;font-family:monospace;';
740 overlay.appendChild(tag);
741 }
742 document.body.appendChild(overlay);
743 return { ok: true, ref_id: refId };
744 },
745
746 clearHighlights: function() {
747 var overlays = document.querySelectorAll('.__victauri_highlight__');
748 for (var i = 0; i < overlays.length; i++) overlays[i].remove();
749 return { ok: true, removed: overlays.length };
750 },
751
752 // ── CSS Injection ────────────────────────────────────────────────────
753
754 injectCss: function(css) {
755 var existing = document.getElementById('__victauri_injected_css__');
756 if (existing) existing.remove();
757 var style = document.createElement('style');
758 style.id = '__victauri_injected_css__';
759 style.textContent = css;
760 document.head.appendChild(style);
761 return { ok: true, length: css.length };
762 },
763
764 removeInjectedCss: function() {
765 var existing = document.getElementById('__victauri_injected_css__');
766 if (!existing) return { ok: true, removed: false };
767 existing.remove();
768 return { ok: true, removed: true };
769 },
770
771 // ── Accessibility Audit ──────────────────────────────────────────────
772
773 auditAccessibility: function() {
774 var violations = [];
775 var warnings = [];
776
777 // Images without alt text
778 var imgs = document.querySelectorAll('img');
779 for (var i = 0; i < imgs.length; i++) {
780 if (!imgs[i].hasAttribute('alt')) {
781 violations.push({ rule: 'img-alt', severity: 'critical', element: describeEl(imgs[i]),
782 message: 'Image missing alt attribute' });
783 } else if (imgs[i].alt.trim() === '') {
784 warnings.push({ rule: 'img-alt-empty', severity: 'minor', element: describeEl(imgs[i]),
785 message: 'Image has empty alt (ok if decorative)' });
786 }
787 }
788
789 // Form inputs without labels
790 var inputs = document.querySelectorAll('input, select, textarea');
791 for (var i = 0; i < inputs.length; i++) {
792 var inp = inputs[i];
793 if (inp.type === 'hidden') continue;
794 var hasLabel = inp.id && document.querySelector('label[for="' + inp.id + '"]');
795 var hasAria = inp.getAttribute('aria-label') || inp.getAttribute('aria-labelledby');
796 var hasTitle = inp.title;
797 var hasPlaceholder = inp.placeholder;
798 if (!hasLabel && !hasAria && !hasTitle && !hasPlaceholder) {
799 violations.push({ rule: 'input-label', severity: 'serious', element: describeEl(inp),
800 message: 'Form input has no accessible label' });
801 }
802 }
803
804 // Buttons without accessible text
805 var buttons = document.querySelectorAll('button, [role="button"]');
806 for (var i = 0; i < buttons.length; i++) {
807 var btn = buttons[i];
808 var text = (btn.textContent || '').trim();
809 var ariaLabel = btn.getAttribute('aria-label');
810 var ariaLabelledBy = btn.getAttribute('aria-labelledby');
811 if (!text && !ariaLabel && !ariaLabelledBy && !btn.title) {
812 var hasImg = btn.querySelector('img[alt], svg[aria-label]');
813 if (!hasImg) {
814 violations.push({ rule: 'button-name', severity: 'serious', element: describeEl(btn),
815 message: 'Button has no accessible name' });
816 }
817 }
818 }
819
820 // Links without text
821 var links = document.querySelectorAll('a[href]');
822 for (var i = 0; i < links.length; i++) {
823 var link = links[i];
824 var text = (link.textContent || '').trim();
825 var ariaLabel = link.getAttribute('aria-label');
826 if (!text && !ariaLabel && !link.title) {
827 violations.push({ rule: 'link-name', severity: 'serious', element: describeEl(link),
828 message: 'Link has no accessible text' });
829 }
830 }
831
832 // Missing document language
833 if (!document.documentElement.lang) {
834 violations.push({ rule: 'html-lang', severity: 'serious', element: '<html>',
835 message: 'Document missing lang attribute' });
836 }
837
838 // Heading hierarchy
839 var headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
840 var prevLevel = 0;
841 for (var i = 0; i < headings.length; i++) {
842 var level = parseInt(headings[i].tagName.charAt(1));
843 if (level > prevLevel + 1 && prevLevel > 0) {
844 warnings.push({ rule: 'heading-order', severity: 'moderate', element: describeEl(headings[i]),
845 message: 'Heading level skipped from h' + prevLevel + ' to h' + level });
846 }
847 prevLevel = level;
848 }
849
850 // Missing page title
851 if (!document.title || document.title.trim() === '') {
852 violations.push({ rule: 'document-title', severity: 'serious', element: '<head>',
853 message: 'Document has no title' });
854 }
855
856 // Color contrast (simplified — checks text elements against backgrounds)
857 var textEls = document.querySelectorAll('p, span, a, button, h1, h2, h3, h4, h5, h6, li, td, th, label, div');
858 var contrastIssues = 0;
859 for (var i = 0; i < textEls.length && contrastIssues < 10; i++) {
860 var el = textEls[i];
861 if (!el.textContent || el.textContent.trim() === '') continue;
862 if (el.children.length > 0 && el.children[0].textContent === el.textContent) continue;
863 var cs = window.getComputedStyle(el);
864 var fg = parseColor(cs.color);
865 var bg = parseColor(cs.backgroundColor);
866 if (fg && bg && bg.a > 0) {
867 var ratio = contrastRatio(fg, bg);
868 var fontSize = parseFloat(cs.fontSize);
869 var isBold = parseInt(cs.fontWeight) >= 700;
870 var isLarge = fontSize >= 24 || (fontSize >= 18.66 && isBold);
871 var threshold = isLarge ? 3 : 4.5;
872 if (ratio < threshold) {
873 contrastIssues++;
874 warnings.push({ rule: 'color-contrast', severity: 'serious',
875 element: describeEl(el),
876 message: 'Contrast ratio ' + ratio.toFixed(2) + ':1 (needs ' + threshold + ':1)',
877 details: { fg: cs.color, bg: cs.backgroundColor, ratio: ratio.toFixed(2) } });
878 }
879 }
880 }
881
882 // ARIA role validity
883 var ariaEls = document.querySelectorAll('[role]');
884 var validRoles = new Set(['alert','alertdialog','application','article','banner','button',
885 'cell','checkbox','columnheader','combobox','complementary','contentinfo','definition',
886 'dialog','directory','document','feed','figure','form','grid','gridcell','group',
887 'heading','img','link','list','listbox','listitem','log','main','marquee','math',
888 'menu','menubar','menuitem','menuitemcheckbox','menuitemradio','meter','navigation',
889 'none','note','option','presentation','progressbar','radio','radiogroup','region',
890 'row','rowgroup','rowheader','scrollbar','search','searchbox','separator','slider',
891 'spinbutton','status','switch','tab','table','tablist','tabpanel','term','textbox',
892 'timer','toolbar','tooltip','tree','treegrid','treeitem']);
893 for (var i = 0; i < ariaEls.length; i++) {
894 var role = ariaEls[i].getAttribute('role');
895 if (role && !validRoles.has(role)) {
896 warnings.push({ rule: 'aria-role', severity: 'moderate', element: describeEl(ariaEls[i]),
897 message: 'Invalid ARIA role: ' + role });
898 }
899 }
900
901 // Tab index > 0
902 var tabbable = document.querySelectorAll('[tabindex]');
903 for (var i = 0; i < tabbable.length; i++) {
904 var ti = parseInt(tabbable[i].getAttribute('tabindex'));
905 if (ti > 0) {
906 warnings.push({ rule: 'tabindex-positive', severity: 'moderate', element: describeEl(tabbable[i]),
907 message: 'Positive tabindex disrupts natural tab order (tabindex=' + ti + ')' });
908 }
909 }
910
911 return {
912 violations: violations,
913 warnings: warnings,
914 summary: {
915 critical: violations.filter(function(v) { return v.severity === 'critical'; }).length,
916 serious: violations.filter(function(v) { return v.severity === 'serious'; }).length + warnings.filter(function(w) { return w.severity === 'serious'; }).length,
917 moderate: warnings.filter(function(w) { return w.severity === 'moderate'; }).length,
918 minor: warnings.filter(function(w) { return w.severity === 'minor'; }).length,
919 total: violations.length + warnings.length,
920 }
921 };
922 },
923
924 // ── Performance Metrics ──────────────────────────────────────────────
925
926 getPerformanceMetrics: function() {
927 var result = {};
928
929 // Navigation timing
930 var nav = performance.getEntriesByType('navigation')[0];
931 if (nav) {
932 result.navigation = {
933 dns_ms: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
934 connect_ms: Math.round(nav.connectEnd - nav.connectStart),
935 ttfb_ms: Math.round(nav.responseStart - nav.requestStart),
936 response_ms: Math.round(nav.responseEnd - nav.responseStart),
937 dom_interactive_ms: Math.round(nav.domInteractive - nav.startTime),
938 dom_complete_ms: Math.round(nav.domComplete - nav.startTime),
939 load_event_ms: Math.round(nav.loadEventEnd - nav.startTime),
940 transfer_size: nav.transferSize,
941 encoded_body_size: nav.encodedBodySize,
942 decoded_body_size: nav.decodedBodySize,
943 };
944 }
945
946 // Resource summary
947 var resources = performance.getEntriesByType('resource');
948 var byType = {};
949 var totalTransfer = 0;
950 for (var i = 0; i < resources.length; i++) {
951 var r = resources[i];
952 var type = r.initiatorType || 'other';
953 if (!byType[type]) byType[type] = { count: 0, total_ms: 0, total_bytes: 0 };
954 byType[type].count++;
955 byType[type].total_ms += r.duration;
956 byType[type].total_bytes += r.transferSize || 0;
957 totalTransfer += r.transferSize || 0;
958 }
959 result.resources = {
960 total_count: resources.length,
961 total_transfer_bytes: totalTransfer,
962 by_type: byType,
963 slowest: resources.sort(function(a, b) { return b.duration - a.duration; }).slice(0, 5).map(function(r) {
964 return { name: r.name.split('/').pop().split('?')[0], duration_ms: Math.round(r.duration), size: r.transferSize || 0, type: r.initiatorType };
965 }),
966 };
967
968 // Paint timing
969 var paints = performance.getEntriesByType('paint');
970 result.paint = {};
971 for (var i = 0; i < paints.length; i++) {
972 result.paint[paints[i].name] = Math.round(paints[i].startTime);
973 }
974
975 // Memory (Chrome/Edge)
976 if (performance.memory) {
977 result.js_heap = {
978 used_mb: Math.round(performance.memory.usedJSHeapSize / 1048576 * 100) / 100,
979 total_mb: Math.round(performance.memory.totalJSHeapSize / 1048576 * 100) / 100,
980 limit_mb: Math.round(performance.memory.jsHeapSizeLimit / 1048576 * 100) / 100,
981 };
982 }
983
984 // Long tasks (if PerformanceObserver captured any)
985 if (window.__VICTAURI__._longTasks) {
986 result.long_tasks = {
987 count: window.__VICTAURI__._longTasks.length,
988 total_ms: Math.round(window.__VICTAURI__._longTasks.reduce(function(s, t) { return s + t.duration; }, 0)),
989 worst_ms: window.__VICTAURI__._longTasks.length > 0 ? Math.round(Math.max.apply(null, window.__VICTAURI__._longTasks.map(function(t) { return t.duration; }))) : 0,
990 };
991 }
992
993 // DOM stats
994 result.dom = {
995 elements: document.querySelectorAll('*').length,
996 max_depth: (function() { var d = 0; var walk = function(el, depth) { if (depth > d) d = depth; for (var i = 0; i < el.children.length && i < 5; i++) walk(el.children[i], depth + 1); }; walk(document.body, 0); return d; })(),
997 event_listeners: window.__VICTAURI__._listenerCount || 0,
998 };
999
1000 return result;
1001 },
1002 };
1003
1004 // ── Accessibility Helpers ────────────────────────────────────────────────
1005
1006 function describeEl(el) {
1007 var s = '<' + el.tagName.toLowerCase();
1008 if (el.id) s += ' id="' + el.id + '"';
1009 if (el.className && typeof el.className === 'string') {
1010 var cls = el.className.trim();
1011 if (cls) s += ' class="' + cls.substring(0, 50) + '"';
1012 }
1013 s += '>';
1014 return s;
1015 }
1016
1017 function parseColor(str) {
1018 if (!str) return null;
1019 var m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
1020 if (!m) return null;
1021 return { r: parseInt(m[1]), g: parseInt(m[2]), b: parseInt(m[3]), a: m[4] !== undefined ? parseFloat(m[4]) : 1 };
1022 }
1023
1024 function luminance(c) {
1025 var rs = c.r / 255, gs = c.g / 255, bs = c.b / 255;
1026 var r = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
1027 var g = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
1028 var b = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
1029 return 0.2126 * r + 0.7152 * g + 0.0722 * b;
1030 }
1031
1032 function contrastRatio(fg, bg) {
1033 var l1 = luminance(fg), l2 = luminance(bg);
1034 var lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
1035 return (lighter + 0.05) / (darker + 0.05);
1036 }
1037
1038 // ── Long Task Observer ──────────────────────────────────────────────────
1039
1040 try {
1041 window.__VICTAURI__._longTasks = [];
1042 var ltObserver = new PerformanceObserver(function(list) {
1043 var entries = list.getEntries();
1044 for (var i = 0; i < entries.length; i++) {
1045 window.__VICTAURI__._longTasks.push({ duration: entries[i].duration, startTime: entries[i].startTime });
1046 if (window.__VICTAURI__._longTasks.length > CAP_LONG_TASKS) window.__VICTAURI__._longTasks.shift();
1047 }
1048 });
1049 ltObserver.observe({ type: 'longtask', buffered: true });
1050 } catch(e) {}
1051
1052 // ── Event Listener Counter ──────────────────────────────────────────────
1053
1054 (function() {
1055 var count = 0;
1056 var origAdd = EventTarget.prototype.addEventListener;
1057 var origRemove = EventTarget.prototype.removeEventListener;
1058 EventTarget.prototype.addEventListener = function() {
1059 count++;
1060 window.__VICTAURI__._listenerCount = count;
1061 return origAdd.apply(this, arguments);
1062 };
1063 EventTarget.prototype.removeEventListener = function() {
1064 if (count > 0) count--;
1065 window.__VICTAURI__._listenerCount = count;
1066 return origRemove.apply(this, arguments);
1067 };
1068 })();
1069
1070 // ── DOM Walking ──────────────────────────────────────────────────────────
1071
1072 function walkDom(node) {
1073 if (!node || node.nodeType !== 1) return null;
1074
1075 var style = window.getComputedStyle(node);
1076 var visible = style.display !== 'none'
1077 && style.visibility !== 'hidden'
1078 && style.opacity !== '0';
1079
1080 if (!visible) return null;
1081
1082 var ref_id = registerRef(node);
1083
1084 var rect = node.getBoundingClientRect();
1085 var role = node.getAttribute('role') || inferRole(node);
1086 var name = node.getAttribute('aria-label')
1087 || node.getAttribute('title')
1088 || node.getAttribute('placeholder')
1089 || (node.tagName === 'BUTTON' ? node.textContent.trim().substring(0, 80) : null)
1090 || (node.tagName === 'A' ? node.textContent.trim().substring(0, 80) : null);
1091
1092 var element = {
1093 ref_id: ref_id,
1094 tag: node.tagName.toLowerCase(),
1095 role: role,
1096 name: name,
1097 text: getDirectText(node),
1098 value: node.value || null,
1099 enabled: !node.disabled,
1100 visible: true,
1101 focusable: node.tabIndex >= 0 || ['INPUT','BUTTON','SELECT','TEXTAREA','A'].indexOf(node.tagName) !== -1,
1102 bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1103 children: [],
1104 attributes: {}
1105 };
1106
1107 var interestingAttrs = ['data-testid', 'id', 'type', 'href', 'src', 'checked', 'selected'];
1108 for (var a = 0; a < interestingAttrs.length; a++) {
1109 if (node.hasAttribute(interestingAttrs[a])) {
1110 element.attributes[interestingAttrs[a]] = node.getAttribute(interestingAttrs[a]);
1111 }
1112 }
1113
1114 for (var c = 0; c < node.children.length; c++) {
1115 var childEl = walkDom(node.children[c]);
1116 if (childEl) element.children.push(childEl);
1117 }
1118
1119 if (node.shadowRoot) {
1120 for (var s = 0; s < node.shadowRoot.children.length; s++) {
1121 var shadowChild = walkDom(node.shadowRoot.children[s]);
1122 if (shadowChild) element.children.push(shadowChild);
1123 }
1124 }
1125
1126 return element;
1127 }
1128
1129 function walkDomCompact(node, depth) {
1130 if (!node || node.nodeType !== 1) return '';
1131
1132 var style = window.getComputedStyle(node);
1133 var visible = style.display !== 'none'
1134 && style.visibility !== 'hidden'
1135 && style.opacity !== '0';
1136
1137 if (!visible) return '';
1138
1139 var ref_id = registerRef(node);
1140 var indent = '';
1141 for (var d = 0; d < depth; d++) indent += ' ';
1142
1143 var role = node.getAttribute('role') || inferRole(node);
1144 var name = node.getAttribute('aria-label')
1145 || node.getAttribute('title')
1146 || node.getAttribute('placeholder')
1147 || '';
1148 var text = getDirectText(node) || '';
1149 var tag = node.tagName.toLowerCase();
1150
1151 var line = indent + '[' + ref_id + '] ';
1152
1153 if (role && role !== tag) {
1154 line += role;
1155 } else {
1156 line += tag;
1157 }
1158
1159 if (name) {
1160 line += ' "' + name.substring(0, 60) + '"';
1161 } else if (text && text.length <= 60) {
1162 line += ' "' + text + '"';
1163 } else if (text) {
1164 line += ' "' + text.substring(0, 57) + '..."';
1165 }
1166
1167 if (node.disabled) line += ' [disabled]';
1168 if (node.value) line += ' value=' + JSON.stringify(node.value.substring(0, 40));
1169
1170 var testId = node.getAttribute('data-testid');
1171 if (testId) line += ' @' + testId;
1172
1173 var type = node.getAttribute('type');
1174 if (type && tag === 'input') line += ' type=' + type;
1175
1176 var href = node.getAttribute('href');
1177 if (href && tag === 'a') line += ' href=' + href.substring(0, 60);
1178
1179 var result = line + '\n';
1180
1181 for (var c = 0; c < node.children.length; c++) {
1182 result += walkDomCompact(node.children[c], depth + 1);
1183 }
1184
1185 if (node.shadowRoot) {
1186 for (var s = 0; s < node.shadowRoot.children.length; s++) {
1187 result += walkDomCompact(node.shadowRoot.children[s], depth + 1);
1188 }
1189 }
1190
1191 return result;
1192 }
1193
1194 function inferRole(node) {
1195 var tag = node.tagName;
1196 var roles = {
1197 'BUTTON': 'button', 'A': 'link', 'INPUT': 'textbox',
1198 'SELECT': 'combobox', 'TEXTAREA': 'textbox', 'IMG': 'img',
1199 'NAV': 'navigation', 'MAIN': 'main', 'HEADER': 'banner',
1200 'FOOTER': 'contentinfo', 'ASIDE': 'complementary',
1201 'H1': 'heading', 'H2': 'heading', 'H3': 'heading',
1202 'H4': 'heading', 'H5': 'heading', 'H6': 'heading',
1203 'UL': 'list', 'OL': 'list', 'LI': 'listitem',
1204 'TABLE': 'table', 'FORM': 'form', 'DIALOG': 'dialog',
1205 };
1206 if (tag === 'INPUT') {
1207 var type = node.getAttribute('type');
1208 if (type === 'checkbox') return 'checkbox';
1209 if (type === 'radio') return 'radio';
1210 if (type === 'range') return 'slider';
1211 if (type === 'submit' || type === 'button') return 'button';
1212 }
1213 return roles[tag] || null;
1214 }
1215
1216 function getDirectText(node) {
1217 var text = '';
1218 for (var i = 0; i < node.childNodes.length; i++) {
1219 if (node.childNodes[i].nodeType === 3) text += node.childNodes[i].textContent;
1220 }
1221 text = text.trim();
1222 return text.length > 0 ? text.substring(0, 200) : null;
1223 }
1224
1225 // ── Console Hooking ──────────────────────────────────────────────────────
1226
1227 var originalConsole = {
1228 log: console.log, warn: console.warn,
1229 error: console.error, info: console.info, debug: console.debug
1230 };
1231
1232 function hookConsole(level) {
1233 console[level] = function() {
1234 var args = Array.prototype.slice.call(arguments);
1235 consoleLogs.push({ level: level, message: args.map(String).join(' '), timestamp: Date.now() });
1236 if (consoleLogs.length > CAP_CONSOLE) consoleLogs.shift();
1237 originalConsole[level].apply(console, args);
1238 };
1239 }
1240
1241 hookConsole('log');
1242 hookConsole('warn');
1243 hookConsole('error');
1244 hookConsole('info');
1245 hookConsole('debug');
1246
1247 // ── Global Error Capture ────────────────────────────────────────────────
1248
1249 window.addEventListener('error', function(e) {
1250 var msg = e.message || 'Unknown error';
1251 if (e.filename) msg += ' at ' + e.filename + ':' + e.lineno + ':' + e.colno;
1252 consoleLogs.push({ level: 'error', message: '[uncaught] ' + msg, timestamp: Date.now() });
1253 if (consoleLogs.length > CAP_CONSOLE) consoleLogs.shift();
1254 });
1255
1256 window.addEventListener('unhandledrejection', function(e) {
1257 var msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled promise rejection';
1258 consoleLogs.push({ level: 'error', message: '[unhandled rejection] ' + msg, timestamp: Date.now() });
1259 if (consoleLogs.length > CAP_CONSOLE) consoleLogs.shift();
1260 });
1261
1262 // ── Interaction Observer (for record mode) ────────────────────────────────
1263
1264 function bestSelector(el) {
1265 if (el.dataset && el.dataset.testid) return '[data-testid="' + el.dataset.testid + '"]';
1266 if (el.id) return '#' + el.id;
1267 if (el.getAttribute && el.getAttribute('role')) {
1268 var role = el.getAttribute('role');
1269 var text = (el.textContent || '').trim().substring(0, 50);
1270 if (text) return '[role="' + role + '"]:has-text("' + text + '")';
1271 return '[role="' + role + '"]';
1272 }
1273 var tag = (el.tagName || 'div').toLowerCase();
1274 var text = (el.textContent || '').trim().substring(0, 50);
1275 if (text && ['button', 'a', 'label', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'].indexOf(tag) !== -1) {
1276 return tag + ':has-text("' + text + '")';
1277 }
1278 if (el.name) return tag + '[name="' + el.name + '"]';
1279 if (el.className && typeof el.className === 'string') {
1280 var cls = el.className.trim().split(/\s+/).slice(0, 2).join('.');
1281 if (cls) return tag + '.' + cls;
1282 }
1283 return tag;
1284 }
1285
1286 function pushInteraction(action, el, value) {
1287 interactionLog.push({
1288 type: 'dom_interaction',
1289 action: action,
1290 selector: bestSelector(el),
1291 value: value || null,
1292 timestamp: Date.now()
1293 });
1294 if (interactionLog.length > CAP_INTERACTION) interactionLog.shift();
1295 }
1296
1297 document.addEventListener('click', function(e) {
1298 if (e.isTrusted && e.target) pushInteraction('click', e.target, null);
1299 }, true);
1300
1301 document.addEventListener('dblclick', function(e) {
1302 if (e.isTrusted && e.target) pushInteraction('double_click', e.target, null);
1303 }, true);
1304
1305 document.addEventListener('change', function(e) {
1306 if (!e.isTrusted || !e.target) return;
1307 var el = e.target;
1308 var tag = (el.tagName || '').toLowerCase();
1309 if (tag === 'select') {
1310 pushInteraction('select', el, el.value);
1311 } else if (tag === 'input' || tag === 'textarea') {
1312 pushInteraction('fill', el, el.value);
1313 }
1314 }, true);
1315
1316 document.addEventListener('keydown', function(e) {
1317 if (!e.isTrusted) return;
1318 if (['Enter', 'Escape', 'Tab', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].indexOf(e.key) !== -1) {
1319 pushInteraction('key_press', e.target || document.body, e.key);
1320 }
1321 }, true);
1322
1323 // ── Mutation Observer (deferred) ─────────────────────────────────────────
1324
1325 var mutationBatchCount = 0;
1326 var mutationBatchTimer = null;
1327 var __mutationObserver = null;
1328
1329 function startMutationObserver() {
1330 if (!document.documentElement) return false;
1331 __mutationObserver = new MutationObserver(function(mutations) {
1332 mutationBatchCount += mutations.length;
1333 if (!mutationBatchTimer) {
1334 mutationBatchTimer = setTimeout(function() {
1335 mutationLog.push({ count: mutationBatchCount, timestamp: Date.now() });
1336 if (mutationLog.length > CAP_MUTATION) mutationLog.shift();
1337 mutationBatchCount = 0;
1338 mutationBatchTimer = null;
1339 }, 100);
1340 }
1341 });
1342 __mutationObserver.observe(document.documentElement, {
1343 childList: true, subtree: true, attributes: true, characterData: true,
1344 });
1345 return true;
1346 }
1347
1348 if (!startMutationObserver()) {
1349 document.addEventListener('DOMContentLoaded', startMutationObserver);
1350 }
1351
1352 // IPC logging is derived from the network log: Tauri 2.0 sends all IPC
1353 // via fetch to http://ipc.localhost/<command>. The fetch interceptor below
1354 // captures these, and getIpcLog() filters them from networkLog. This avoids
1355 // the need to patch __TAURI_INTERNALS__.invoke, which Tauri freezes with
1356 // configurable:false, writable:false.
1357
1358 // ── Network Interception ─────────────────────────────────────────────────
1359
1360 (function interceptNetwork() {
1361 // fetch
1362 var origFetch = window.fetch;
1363 if (origFetch) {
1364 window.fetch = function(input, init) {
1365 var id = ++networkCounter;
1366 var url = typeof input === 'string' ? input : (input && input.url ? input.url : String(input));
1367 var method = (init && init.method) || (input && input.method) || 'GET';
1368 var isIpc = url.indexOf('http://ipc.localhost/') === 0;
1369 var entry = { id: id, method: method.toUpperCase(), url: url, timestamp: Date.now(), status: 'pending', duration_ms: null };
1370
1371 if (isIpc && init && init.body) {
1372 try {
1373 var bodyStr = typeof init.body === 'string' ? init.body : null;
1374 if (bodyStr) {
1375 var parsed = JSON.parse(bodyStr);
1376 entry.request_args = parsed;
1377 }
1378 } catch(e) {}
1379 }
1380
1381 networkLog.push(entry);
1382 if (networkLog.length > CAP_NETWORK) networkLog.shift();
1383
1384 return origFetch.call(this, input, init).then(function(response) {
1385 entry.status = response.status;
1386 entry.status_text = response.statusText;
1387 entry.duration_ms = Date.now() - entry.timestamp;
1388
1389 if (isIpc) {
1390 var cloned = response.clone();
1391 cloned.text().then(function(text) {
1392 try { entry.response_body = JSON.parse(text); } catch(e) { entry.response_body = text; }
1393 }).catch(function() {});
1394 }
1395
1396 return response;
1397 }, function(err) {
1398 entry.status = 'error';
1399 entry.error = String(err);
1400 entry.duration_ms = Date.now() - entry.timestamp;
1401 throw err;
1402 });
1403 };
1404 }
1405
1406 // XMLHttpRequest
1407 var origOpen = XMLHttpRequest.prototype.open;
1408 var origSend = XMLHttpRequest.prototype.send;
1409 XMLHttpRequest.prototype.open = function(method, url) {
1410 this.__victauri_net = { method: method, url: url };
1411 return origOpen.apply(this, arguments);
1412 };
1413 XMLHttpRequest.prototype.send = function() {
1414 if (this.__victauri_net) {
1415 var id = ++networkCounter;
1416 var entry = {
1417 id: id,
1418 method: this.__victauri_net.method.toUpperCase(),
1419 url: this.__victauri_net.url,
1420 timestamp: Date.now(),
1421 status: 'pending',
1422 duration_ms: null,
1423 };
1424 networkLog.push(entry);
1425 if (networkLog.length > CAP_NETWORK) networkLog.shift();
1426 var self = this;
1427 this.addEventListener('load', function() {
1428 entry.status = self.status;
1429 entry.status_text = self.statusText;
1430 entry.duration_ms = Date.now() - entry.timestamp;
1431 });
1432 this.addEventListener('error', function() {
1433 entry.status = 'error';
1434 entry.duration_ms = Date.now() - entry.timestamp;
1435 });
1436 }
1437 return origSend.apply(this, arguments);
1438 };
1439 })();
1440
1441 // ── Navigation Tracking ──────────────────────────────────────────────────
1442
1443 (function trackNavigation() {
1444 navigationLog.push({ url: window.location.href, timestamp: Date.now(), type: 'initial' });
1445
1446 var origPushState = history.pushState;
1447 var origReplaceState = history.replaceState;
1448 history.pushState = function() {
1449 var result = origPushState.apply(this, arguments);
1450 navigationLog.push({ url: window.location.href, timestamp: Date.now(), type: 'pushState' });
1451 if (navigationLog.length > CAP_NAVIGATION) navigationLog.shift();
1452 return result;
1453 };
1454 history.replaceState = function() {
1455 var result = origReplaceState.apply(this, arguments);
1456 navigationLog.push({ url: window.location.href, timestamp: Date.now(), type: 'replaceState' });
1457 if (navigationLog.length > CAP_NAVIGATION) navigationLog.shift();
1458 return result;
1459 };
1460 window.addEventListener('popstate', function() {
1461 navigationLog.push({ url: window.location.href, timestamp: Date.now(), type: 'popstate' });
1462 });
1463 window.addEventListener('hashchange', function(e) {
1464 navigationLog.push({ url: window.location.href, timestamp: Date.now(), type: 'hashchange', old_url: e.oldURL });
1465 });
1466 })();
1467
1468 // ── Dialog Capture ───────────────────────────────────────────────────────
1469
1470 var dialogAutoResponses = { alert: { action: 'accept' }, confirm: { action: 'accept' }, prompt: { action: 'accept', text: '' } };
1471
1472 // ── Resource Cleanup ────────────────────────────────────────────────────
1473
1474 window.addEventListener('pagehide', function() {
1475 if (__mutationObserver) { __mutationObserver.disconnect(); __mutationObserver = null; }
1476 if (mutationBatchTimer) { clearTimeout(mutationBatchTimer); mutationBatchTimer = null; }
1477 console.log = originalConsole.log;
1478 console.warn = originalConsole.warn;
1479 console.error = originalConsole.error;
1480 console.info = originalConsole.info;
1481 console.debug = originalConsole.debug;
1482 consoleLogs.length = 0;
1483 mutationLog.length = 0;
1484 networkLog.length = 0;
1485 navigationLog.length = 0;
1486 dialogLog.length = 0;
1487 interactionLog.length = 0;
1488 refMap.clear();
1489 weakRefMap.clear();
1490 refCounter = 0;
1491 });
1492
1493 (function captureDialogs() {
1494 window.alert = function(msg) {
1495 dialogLog.push({ type: 'alert', message: String(msg || ''), timestamp: Date.now() });
1496 if (dialogLog.length > CAP_DIALOG) dialogLog.shift();
1497 };
1498 window.confirm = function(msg) {
1499 var resp = dialogAutoResponses.confirm;
1500 var result = resp.action === 'accept';
1501 dialogLog.push({ type: 'confirm', message: String(msg || ''), timestamp: Date.now(), result: result });
1502 if (dialogLog.length > CAP_DIALOG) dialogLog.shift();
1503 return result;
1504 };
1505 window.prompt = function(msg, defaultValue) {
1506 var resp = dialogAutoResponses.prompt;
1507 var result = resp.action === 'accept' ? (resp.text || defaultValue || '') : null;
1508 dialogLog.push({ type: 'prompt', message: String(msg || ''), timestamp: Date.now(), result: result });
1509 if (dialogLog.length > CAP_DIALOG) dialogLog.shift();
1510 return result;
1511 };
1512 })();
1513})();
1514"#;