@ranganathmk/trq 0.1.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,708 @@
1
+ "use strict";
2
+ (() => {
3
+ // src/selectors.ts
4
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
5
+ "button",
6
+ "link",
7
+ "option",
8
+ "menuitem",
9
+ "menuitemcheckbox",
10
+ "menuitemradio",
11
+ "tab",
12
+ "checkbox",
13
+ "radio",
14
+ "switch",
15
+ "treeitem"
16
+ ]);
17
+ function hasInteractiveRole(el) {
18
+ const role = el.getAttribute("role");
19
+ return !!role && INTERACTIVE_ROLES.has(role);
20
+ }
21
+ function isClickableElement(el) {
22
+ return el.tagName === "BUTTON" || el.tagName === "A" || hasInteractiveRole(el) || el.tagName.includes("-");
23
+ }
24
+ function buildSelectors(el) {
25
+ const list = [];
26
+ const ariaName = computeAccessibleName(el);
27
+ if (ariaName) list.push([`aria/${ariaName}`]);
28
+ for (const attr of ["data-testid", "data-test", "data-cy", "data-qa"]) {
29
+ const v = el.getAttribute(attr);
30
+ if (v) list.push([`[${attr}='${cssEscape(v)}']`]);
31
+ }
32
+ if (isClickableElement(el)) {
33
+ const text = el.textContent?.trim();
34
+ if (text && text.length > 0 && text.length < 80) {
35
+ list.push([`text/${text}`]);
36
+ }
37
+ }
38
+ list.push([buildCssPath(el)]);
39
+ list.push([`xpath=${buildXPath(el)}`]);
40
+ return list;
41
+ }
42
+ function computeAccessibleName(el) {
43
+ const ariaLabel = el.getAttribute("aria-label")?.trim();
44
+ if (ariaLabel) return ariaLabel;
45
+ const labelledBy = el.getAttribute("aria-labelledby");
46
+ if (labelledBy) {
47
+ const ref = document.getElementById(labelledBy);
48
+ if (ref) {
49
+ const t = ref.textContent?.trim();
50
+ if (t) return t;
51
+ }
52
+ }
53
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT") {
54
+ if (el.id) {
55
+ const label = document.querySelector(`label[for='${cssEscape(el.id)}']`);
56
+ const t = label?.textContent?.trim();
57
+ if (t) return t;
58
+ }
59
+ const placeholder = el.getAttribute("placeholder")?.trim();
60
+ if (placeholder) return placeholder;
61
+ const name = el.getAttribute("name")?.trim();
62
+ if (name) return name;
63
+ }
64
+ if (el.tagName === "BUTTON" || el.tagName === "A" || hasInteractiveRole(el) || el.tagName.includes("-")) {
65
+ const t = el.textContent?.trim();
66
+ if (t && t.length < 80) return t;
67
+ }
68
+ const title = el.getAttribute("title")?.trim();
69
+ if (title) return title;
70
+ return null;
71
+ }
72
+ function isDynamicId(id) {
73
+ if (/^(mat|cdk|ng|md|rc|react-select|radix-)/i.test(id) && /\d/.test(id)) return true;
74
+ if (/^:r[a-z0-9]+:?$/.test(id)) return true;
75
+ if (/^[a-z]+-\d{3,}$/i.test(id)) return true;
76
+ return false;
77
+ }
78
+ function buildCssPath(el) {
79
+ const parts = [];
80
+ let cur = el;
81
+ let depth = 0;
82
+ while (cur && cur.nodeType === 1 && depth < 8) {
83
+ let part = cur.tagName.toLowerCase();
84
+ if (cur.id && !isDynamicId(cur.id)) {
85
+ parts.unshift(`#${cssEscape(cur.id)}`);
86
+ break;
87
+ }
88
+ const classAttr = cur.getAttribute("class");
89
+ const classes = classAttr ? classAttr.trim().split(/\s+/).filter((c) => c && !/^\d/.test(c)).slice(0, 2) : [];
90
+ if (classes.length) part += "." + classes.map(cssEscape).join(".");
91
+ const parent = cur.parentElement;
92
+ if (parent) {
93
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
94
+ if (sameTag.length > 1) {
95
+ part += `:nth-of-type(${sameTag.indexOf(cur) + 1})`;
96
+ }
97
+ }
98
+ parts.unshift(part);
99
+ if (cur === document.body) break;
100
+ cur = cur.parentElement;
101
+ depth++;
102
+ }
103
+ return parts.join(" > ");
104
+ }
105
+ function buildXPath(el) {
106
+ const parts = [];
107
+ let cur = el;
108
+ while (cur && cur.nodeType === 1 && cur !== document.documentElement) {
109
+ const parent = cur.parentElement;
110
+ if (!parent) break;
111
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
112
+ const idx = sameTag.indexOf(cur) + 1;
113
+ parts.unshift(`${cur.tagName.toLowerCase()}[${idx}]`);
114
+ cur = parent;
115
+ }
116
+ return "/html/" + parts.join("/");
117
+ }
118
+ function cssEscape(s) {
119
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(s);
120
+ return s.replace(/(['"\\])/g, "\\$1");
121
+ }
122
+
123
+ // src/bootstrap.ts
124
+ (() => {
125
+ if (window.__trqInstalled) return;
126
+ window.__trqInstalled = true;
127
+ window.__trqMode = window.__trqMode || "recording";
128
+ let stepCount = 0;
129
+ let renderOverlay = null;
130
+ window.__trqSetStep = (n) => {
131
+ stepCount = n;
132
+ renderOverlay?.();
133
+ };
134
+ function emit(event) {
135
+ if (window.__trqMode !== "recording") return;
136
+ if (typeof window.__trqEmit !== "function") return;
137
+ try {
138
+ window.__trqEmit(JSON.stringify(event));
139
+ } catch {
140
+ }
141
+ }
142
+ function emitMeta(event) {
143
+ if (typeof window.__trqEmit !== "function") return;
144
+ try {
145
+ window.__trqEmit(JSON.stringify(event));
146
+ } catch {
147
+ }
148
+ }
149
+ function getViewport() {
150
+ return {
151
+ width: window.innerWidth,
152
+ height: window.innerHeight,
153
+ devicePixelRatio: window.devicePixelRatio
154
+ };
155
+ }
156
+ function targetSnapshot(el) {
157
+ const attrs = {};
158
+ for (const a of Array.from(el.attributes)) attrs[a.name] = a.value;
159
+ const rect = el.getBoundingClientRect();
160
+ const text = el.textContent?.trim().slice(0, 200) ?? null;
161
+ return {
162
+ tagName: el.tagName,
163
+ textContent: text,
164
+ attributes: attrs,
165
+ boundingBox: {
166
+ x: Math.round(rect.x),
167
+ y: Math.round(rect.y),
168
+ width: Math.round(rect.width),
169
+ height: Math.round(rect.height)
170
+ }
171
+ };
172
+ }
173
+ function isInteractive(el) {
174
+ const tag = el.tagName;
175
+ if (tag === "BUTTON" || tag === "A" || tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return true;
176
+ const role = el.getAttribute("role");
177
+ if (role && ["button", "link", "checkbox", "radio", "tab", "menuitem", "option", "switch"].includes(role)) return true;
178
+ if (el.hasAttribute("onclick")) return true;
179
+ const tabIndex = el.tabIndex;
180
+ if (typeof tabIndex === "number" && tabIndex >= 0 && el.hasAttribute("tabindex")) return true;
181
+ return false;
182
+ }
183
+ function mouseModifiers(e) {
184
+ const m = [];
185
+ if (e.altKey) m.push("Alt");
186
+ if (e.ctrlKey) m.push("Control");
187
+ if (e.metaKey) m.push("Meta");
188
+ if (e.shiftKey) m.push("Shift");
189
+ return m;
190
+ }
191
+ function keyModifiers(e) {
192
+ const m = [];
193
+ if (e.altKey) m.push("Alt");
194
+ if (e.ctrlKey) m.push("Control");
195
+ if (e.metaKey) m.push("Meta");
196
+ if (e.shiftKey) m.push("Shift");
197
+ return m;
198
+ }
199
+ function isInputLike(el) {
200
+ return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT";
201
+ }
202
+ function getInputKind(el) {
203
+ if (el.tagName === "TEXTAREA") return "textarea";
204
+ if (el.tagName === "SELECT") {
205
+ return el.multiple ? "select-multiple" : "select-one";
206
+ }
207
+ if (el.tagName === "INPUT") {
208
+ const type = el.type;
209
+ if (type === "checkbox") return "checkbox";
210
+ if (type === "radio") return "radio";
211
+ return "text";
212
+ }
213
+ return "unknown";
214
+ }
215
+ function isPassword(el) {
216
+ return el.tagName === "INPUT" && el.type === "password";
217
+ }
218
+ function readValue(el) {
219
+ if (el.tagName === "INPUT") {
220
+ const inp = el;
221
+ if (inp.type === "checkbox" || inp.type === "radio") return String(inp.checked);
222
+ return inp.value;
223
+ }
224
+ if (el.tagName === "TEXTAREA") return el.value;
225
+ if (el.tagName === "SELECT") {
226
+ const sel = el;
227
+ if (sel.multiple) {
228
+ return JSON.stringify(Array.from(sel.selectedOptions).map((o) => o.value));
229
+ }
230
+ return sel.value;
231
+ }
232
+ return "";
233
+ }
234
+ const lastEmitted = /* @__PURE__ */ new WeakMap();
235
+ function flushInput(el) {
236
+ if (!isInputLike(el)) return;
237
+ const value = readValue(el);
238
+ if (value === "[object Object]") return;
239
+ if (!lastEmitted.has(el)) {
240
+ lastEmitted.set(el, value);
241
+ return;
242
+ }
243
+ if (lastEmitted.get(el) === value) return;
244
+ lastEmitted.set(el, value);
245
+ emit({
246
+ type: "input",
247
+ timestamp: Date.now(),
248
+ url: location.href,
249
+ frame: "main",
250
+ target: targetSnapshot(el),
251
+ selectors: buildSelectors(el),
252
+ inputKind: getInputKind(el),
253
+ value,
254
+ isPassword: isPassword(el),
255
+ viewport: getViewport()
256
+ });
257
+ }
258
+ document.addEventListener(
259
+ "change",
260
+ (e) => {
261
+ if (!e.isTrusted) return;
262
+ if (e.target instanceof Element) flushInput(e.target);
263
+ },
264
+ true
265
+ );
266
+ document.addEventListener(
267
+ "focusout",
268
+ (e) => {
269
+ if (!e.isTrusted) return;
270
+ if (e.target instanceof Element) flushInput(e.target);
271
+ },
272
+ true
273
+ );
274
+ document.addEventListener(
275
+ "focusin",
276
+ (e) => {
277
+ if (!e.isTrusted) return;
278
+ if (e.target instanceof Element && isInputLike(e.target)) {
279
+ if (!lastEmitted.has(e.target)) {
280
+ lastEmitted.set(e.target, readValue(e.target));
281
+ }
282
+ }
283
+ },
284
+ true
285
+ );
286
+ const SPECIAL_KEYS = /* @__PURE__ */ new Set([
287
+ "Enter",
288
+ "Tab",
289
+ "Escape",
290
+ "ArrowUp",
291
+ "ArrowDown",
292
+ "ArrowLeft",
293
+ "ArrowRight"
294
+ ]);
295
+ document.addEventListener(
296
+ "keydown",
297
+ (e) => {
298
+ if (!e.isTrusted) return;
299
+ if (!SPECIAL_KEYS.has(e.key)) return;
300
+ const target = e.target;
301
+ if (!(target instanceof Element)) return;
302
+ if ((e.key === "Enter" || e.key === "Tab") && isInputLike(target)) {
303
+ flushInput(target);
304
+ }
305
+ emit({
306
+ type: "key",
307
+ timestamp: Date.now(),
308
+ url: location.href,
309
+ frame: "main",
310
+ target: targetSnapshot(target),
311
+ selectors: buildSelectors(target),
312
+ key: e.key,
313
+ modifiers: keyModifiers(e),
314
+ viewport: getViewport()
315
+ });
316
+ },
317
+ true
318
+ );
319
+ let assertMode = null;
320
+ let onAssertModeExit = null;
321
+ let highlightHost = null;
322
+ let highlightedEl = null;
323
+ function ensureHighlightHost() {
324
+ if (highlightHost) return highlightHost;
325
+ highlightHost = document.createElement("div");
326
+ highlightHost.id = "__trq_highlight";
327
+ highlightHost.style.cssText = [
328
+ "position:fixed !important",
329
+ "left:0 !important",
330
+ "top:0 !important",
331
+ "width:0 !important",
332
+ "height:0 !important",
333
+ "pointer-events:none !important",
334
+ "z-index:2147483646 !important",
335
+ "border:2px solid #3b82f6 !important",
336
+ "background:rgba(59,130,246,.10) !important",
337
+ "border-radius:3px !important",
338
+ "box-shadow:0 0 0 9999px rgba(0,0,0,.18) !important",
339
+ "display:none !important",
340
+ "transition:left .04s, top .04s, width .04s, height .04s !important"
341
+ ].join(";");
342
+ document.documentElement.appendChild(highlightHost);
343
+ return highlightHost;
344
+ }
345
+ function showHighlight(el) {
346
+ if (highlightedEl === el) return;
347
+ highlightedEl = el;
348
+ const host = ensureHighlightHost();
349
+ const r = el.getBoundingClientRect();
350
+ host.style.setProperty("display", "block", "important");
351
+ host.style.setProperty("left", `${r.left - 2}px`, "important");
352
+ host.style.setProperty("top", `${r.top - 2}px`, "important");
353
+ host.style.setProperty("width", `${r.width}px`, "important");
354
+ host.style.setProperty("height", `${r.height}px`, "important");
355
+ }
356
+ function clearHighlight() {
357
+ highlightedEl = null;
358
+ if (highlightHost) highlightHost.style.setProperty("display", "none", "important");
359
+ }
360
+ let menuHost = null;
361
+ function unmountMenu() {
362
+ if (menuHost) {
363
+ menuHost.remove();
364
+ menuHost = null;
365
+ }
366
+ }
367
+ function isInOverlay(target) {
368
+ if (!(target instanceof Element)) return false;
369
+ return target.id === "__trq_overlay" || target.id === "__trq_assert_menu";
370
+ }
371
+ function enterAssertPicking() {
372
+ if (assertMode !== null) return;
373
+ assertMode = "picking";
374
+ document.documentElement.style.setProperty("cursor", "crosshair", "important");
375
+ ensureHighlightHost();
376
+ }
377
+ function exitAssertMode() {
378
+ assertMode = null;
379
+ document.documentElement.style.removeProperty("cursor");
380
+ clearHighlight();
381
+ unmountMenu();
382
+ onAssertModeExit?.();
383
+ }
384
+ function onElementPicked(el) {
385
+ if (assertMode !== "picking") return;
386
+ assertMode = "editing";
387
+ clearHighlight();
388
+ mountMenu(el);
389
+ }
390
+ function emitAssertion(el, spec) {
391
+ emit({
392
+ type: "assert",
393
+ timestamp: Date.now(),
394
+ url: location.href,
395
+ frame: "main",
396
+ target: targetSnapshot(el),
397
+ selectors: buildSelectors(el),
398
+ assertion: spec,
399
+ viewport: getViewport()
400
+ });
401
+ }
402
+ function mountMenu(el) {
403
+ unmountMenu();
404
+ const rect = el.getBoundingClientRect();
405
+ menuHost = document.createElement("div");
406
+ menuHost.id = "__trq_assert_menu";
407
+ const below = window.innerHeight - rect.bottom > 220;
408
+ const top = below ? rect.bottom + 8 : Math.max(8, rect.top - 220);
409
+ const left = Math.min(
410
+ Math.max(8, rect.left),
411
+ window.innerWidth - 360
412
+ );
413
+ menuHost.style.cssText = [
414
+ "position:fixed !important",
415
+ `top:${top}px !important`,
416
+ `left:${left}px !important`,
417
+ "z-index:2147483647 !important",
418
+ "pointer-events:auto !important",
419
+ "margin:0 !important",
420
+ "padding:0 !important"
421
+ ].join(";");
422
+ const shadow = menuHost.attachShadow({ mode: "closed" });
423
+ const text = el.textContent?.trim() ?? "";
424
+ const hasText = text.length > 0 && text.length < 200;
425
+ const isInputEl = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
426
+ const value = isInputEl ? readValue(el) : "";
427
+ shadow.innerHTML = `
428
+ <style>
429
+ :host { all: initial; }
430
+ .menu {
431
+ background:#1a1a1a; color:#fff;
432
+ font: 13px/1.4 system-ui, sans-serif;
433
+ padding:14px 16px; border-radius:10px;
434
+ box-shadow:0 8px 32px rgba(0,0,0,.4);
435
+ width:340px; user-select:none;
436
+ }
437
+ .head { color:#a3a3a3; font-size:11px; margin-bottom:10px; text-transform:uppercase; letter-spacing:.5px; }
438
+ .head b { color:#fff; font-weight:600; text-transform:none; letter-spacing:0; }
439
+ .row { display:flex; align-items:center; gap:8px; padding:6px 0; }
440
+ .row label { display:flex; align-items:center; gap:8px; cursor:pointer; flex:1; }
441
+ .row input[type=radio] { accent-color:#3b82f6; }
442
+ .row input[type=text] {
443
+ flex:1; background:#0e0e0e; color:#fff; border:1px solid #2a2a2a;
444
+ border-radius:5px; padding:5px 8px; font: 12px/1.2 ui-monospace, monospace;
445
+ min-width:0;
446
+ }
447
+ .row input[type=text]:focus { outline:none; border-color:#3b82f6; }
448
+ .row input[type=text]:disabled { opacity:.4; }
449
+ .actions { display:flex; justify-content:flex-end; gap:8px; margin-top:12px; }
450
+ button.b {
451
+ background:#2a2a2a; color:#fff; border:0; border-radius:6px;
452
+ padding:6px 14px; font: 12px/1 system-ui, sans-serif; cursor:pointer;
453
+ }
454
+ button.b:hover { background:#3a3a3a; }
455
+ button.b.primary { background:#3b82f6; }
456
+ button.b.primary:hover { background:#2563eb; }
457
+ </style>
458
+ <div class="menu">
459
+ <div class="head">Assert about <b>&lt;${el.tagName.toLowerCase()}&gt;</b></div>
460
+ <div class="row">
461
+ <label><input type="radio" name="kind" value="visible" ${!hasText && !isInputEl ? "checked" : ""}/> Is visible</label>
462
+ </div>
463
+ ${hasText ? `<div class="row">
464
+ <label><input type="radio" name="kind" value="text-equals" checked/> Has text</label>
465
+ <input type="text" id="text-input" value="${escapeAttr(text)}"/>
466
+ </div>` : ""}
467
+ ${isInputEl ? `<div class="row">
468
+ <label><input type="radio" name="kind" value="value-equals" ${!hasText ? "checked" : ""}/> Has value</label>
469
+ <input type="text" id="value-input" value="${escapeAttr(value)}"/>
470
+ </div>` : ""}
471
+ <div class="actions">
472
+ <button class="b" id="cancel" type="button">Cancel</button>
473
+ <button class="b primary" id="confirm" type="button">Confirm</button>
474
+ </div>
475
+ </div>
476
+ `;
477
+ document.documentElement.appendChild(menuHost);
478
+ const confirmBtn = shadow.getElementById("confirm");
479
+ const cancelBtn = shadow.getElementById("cancel");
480
+ cancelBtn?.addEventListener("click", (ev) => {
481
+ ev.stopPropagation();
482
+ exitAssertMode();
483
+ });
484
+ confirmBtn?.addEventListener("click", (ev) => {
485
+ ev.stopPropagation();
486
+ const checked = shadow.querySelector('input[name="kind"]:checked');
487
+ const kind = checked?.value ?? "visible";
488
+ let spec = { kind: "visible" };
489
+ if (kind === "text-equals") {
490
+ const inp = shadow.getElementById("text-input");
491
+ spec = { kind: "text-equals", text: inp?.value ?? "" };
492
+ } else if (kind === "value-equals") {
493
+ const inp = shadow.getElementById("value-input");
494
+ spec = { kind: "value-equals", value: inp?.value ?? "" };
495
+ }
496
+ emitAssertion(el, spec);
497
+ exitAssertMode();
498
+ });
499
+ }
500
+ function escapeAttr(s) {
501
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
502
+ }
503
+ let lastHoverEmit = 0;
504
+ const HOVER_THROTTLE_MS = 33;
505
+ document.addEventListener(
506
+ "mousemove",
507
+ (e) => {
508
+ const t = e.target;
509
+ if (!(t instanceof Element)) return;
510
+ if (isInOverlay(t)) return;
511
+ if (assertMode === "picking") {
512
+ showHighlight(t);
513
+ return;
514
+ }
515
+ if (window.__trqInspect) {
516
+ showHighlight(t);
517
+ const now = Date.now();
518
+ if (now - lastHoverEmit < HOVER_THROTTLE_MS) return;
519
+ lastHoverEmit = now;
520
+ let el = t;
521
+ let walked = 0;
522
+ while (!isInteractive(el) && el.parentElement && walked < 5) {
523
+ el = el.parentElement;
524
+ walked++;
525
+ }
526
+ if (el.tagName === "HTML" || el.tagName === "BODY") return;
527
+ emitMeta({
528
+ __trq: "hover",
529
+ timestamp: now,
530
+ target: targetSnapshot(el),
531
+ selectors: buildSelectors(el)
532
+ });
533
+ }
534
+ },
535
+ true
536
+ );
537
+ let highlightTimer = null;
538
+ window.__trqHighlight = (selectors) => {
539
+ if (typeof window.__trqResolveEl !== "function") return false;
540
+ const el = window.__trqResolveEl(selectors);
541
+ if (!el) return false;
542
+ el.scrollIntoView({ block: "center", behavior: "smooth" });
543
+ showHighlight(el);
544
+ if (highlightTimer !== null) clearTimeout(highlightTimer);
545
+ highlightTimer = window.setTimeout(() => {
546
+ clearHighlight();
547
+ highlightTimer = null;
548
+ }, 2e3);
549
+ return true;
550
+ };
551
+ document.addEventListener(
552
+ "keydown",
553
+ (e) => {
554
+ if (e.key !== "Escape") return;
555
+ if (assertMode !== null) {
556
+ e.preventDefault();
557
+ e.stopImmediatePropagation();
558
+ exitAssertMode();
559
+ }
560
+ },
561
+ true
562
+ );
563
+ document.addEventListener(
564
+ "click",
565
+ (e) => {
566
+ if (!e.isTrusted) return;
567
+ const target = e.target;
568
+ if (!(target instanceof Element)) return;
569
+ if (assertMode === "picking" && !isInOverlay(target)) {
570
+ e.preventDefault();
571
+ e.stopImmediatePropagation();
572
+ onElementPicked(target);
573
+ return;
574
+ }
575
+ if (assertMode === "editing" && !isInOverlay(target)) {
576
+ return;
577
+ }
578
+ const active = document.activeElement;
579
+ if (active instanceof Element && isInputLike(active)) flushInput(active);
580
+ let el = target;
581
+ let walked = 0;
582
+ let foundInteractive = isInteractive(el);
583
+ while (!foundInteractive && el.parentElement && walked < 5) {
584
+ el = el.parentElement;
585
+ walked++;
586
+ foundInteractive = isInteractive(el);
587
+ }
588
+ if (!foundInteractive) return;
589
+ if (el.tagName === "HTML" || el.tagName === "BODY") return;
590
+ const rect = el.getBoundingClientRect();
591
+ emit({
592
+ type: "click",
593
+ timestamp: Date.now(),
594
+ url: location.href,
595
+ frame: "main",
596
+ target: targetSnapshot(el),
597
+ selectors: buildSelectors(el),
598
+ click: {
599
+ button: ["left", "middle", "right"][e.button] ?? "left",
600
+ clickCount: e.detail || 1,
601
+ modifiers: mouseModifiers(e),
602
+ offsetX: Math.round(e.clientX - rect.x),
603
+ offsetY: Math.round(e.clientY - rect.y)
604
+ },
605
+ viewport: getViewport()
606
+ });
607
+ },
608
+ true
609
+ );
610
+ if (window === window.top) installOverlay();
611
+ if (typeof window.__trqEmit === "function") {
612
+ try {
613
+ window.__trqEmit(JSON.stringify({ __trq: "ready" }));
614
+ } catch {
615
+ }
616
+ }
617
+ function installOverlay() {
618
+ const mount = () => {
619
+ if (window.__trqInspect) return;
620
+ if (document.getElementById("__trq_overlay")) return;
621
+ const host = document.createElement("div");
622
+ host.id = "__trq_overlay";
623
+ host.style.cssText = [
624
+ "position:fixed !important",
625
+ "top:12px !important",
626
+ "left:50% !important",
627
+ "transform:translateX(-50%) !important",
628
+ "z-index:2147483647 !important",
629
+ "margin:0 !important",
630
+ "padding:0 !important",
631
+ "border:0 !important",
632
+ "display:inline-block !important",
633
+ "width:auto !important",
634
+ "height:auto !important",
635
+ "max-width:360px !important",
636
+ // Host receives pointer events so the Assert button is clickable.
637
+ // Inner spans default to pointer-events:none via the shadow CSS,
638
+ // so only the button captures clicks.
639
+ "pointer-events:auto !important",
640
+ "user-select:none !important"
641
+ ].join(";");
642
+ const shadow = host.attachShadow({ mode: "closed" });
643
+ shadow.innerHTML = `
644
+ <style>
645
+ /* Default everything inside the overlay to non-interactive so it
646
+ can't intercept the page's hover/click. Buttons opt back in. */
647
+ * { pointer-events: none !important; user-select: none !important; }
648
+ .btn { pointer-events: auto !important; cursor: pointer !important; }
649
+ .panel {
650
+ display: flex; align-items: center; gap: 10px;
651
+ background: #1a1a1a; color: #fff; padding: 6px 14px;
652
+ border-radius: 999px; box-shadow: 0 4px 16px rgba(0,0,0,.25);
653
+ font: 12px/1 system-ui, sans-serif;
654
+ backdrop-filter: blur(6px);
655
+ }
656
+ .dot { width:8px; height:8px; border-radius:50%; background:#ef4444; flex:none; animation: pulse 1.5s ease-in-out infinite; }
657
+ @keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.35 } }
658
+ .name { font-weight:600; letter-spacing:.2px; }
659
+ .sep { width:1px; height:10px; background:#3a3a3a; }
660
+ .stats { color:#a3a3a3; }
661
+ .btn {
662
+ background:#2a2a2a; color:#fff; border:0; border-radius:999px;
663
+ padding:4px 10px; font: 11px/1 system-ui, sans-serif;
664
+ }
665
+ .btn:hover { background:#3a3a3a; }
666
+ .btn.active { background:#3b82f6; }
667
+ .mode-label { color:#3b82f6; font-weight:600; }
668
+ </style>
669
+ <div class="panel">
670
+ <span class="dot"></span>
671
+ <span class="name">trq</span>
672
+ <span class="sep"></span>
673
+ <span class="stats" id="stats">0 steps</span>
674
+ <span class="sep"></span>
675
+ <button class="btn" id="assert-btn" type="button">\u2713 Assert</button>
676
+ </div>
677
+ `;
678
+ (document.body || document.documentElement).appendChild(host);
679
+ const stats = shadow.getElementById("stats");
680
+ renderOverlay = () => {
681
+ if (stats) stats.textContent = `${stepCount} step${stepCount === 1 ? "" : "s"}`;
682
+ };
683
+ renderOverlay();
684
+ const assertBtn = shadow.getElementById("assert-btn");
685
+ assertBtn?.addEventListener("click", (ev) => {
686
+ ev.stopPropagation();
687
+ if (assertMode === null) {
688
+ enterAssertPicking();
689
+ assertBtn.classList.add("active");
690
+ assertBtn.textContent = "Click target\u2026 (esc)";
691
+ } else {
692
+ exitAssertMode();
693
+ }
694
+ });
695
+ onAssertModeExit = () => {
696
+ if (!assertBtn) return;
697
+ assertBtn.classList.remove("active");
698
+ assertBtn.textContent = "\u2713 Assert";
699
+ };
700
+ };
701
+ if (document.readyState === "loading") {
702
+ document.addEventListener("DOMContentLoaded", mount, { once: true });
703
+ } else {
704
+ mount();
705
+ }
706
+ }
707
+ })();
708
+ })();