@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,542 @@
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/replay-helpers.ts
124
+ (() => {
125
+ if (window.__trqReplayInstalled) return;
126
+ window.__trqReplayInstalled = true;
127
+ function cssEscape2(s) {
128
+ if (typeof CSS !== "undefined" && CSS.escape) {
129
+ return CSS.escape(s);
130
+ }
131
+ return s.replace(/(['"\\])/g, "\\$1");
132
+ }
133
+ function isVisible(el) {
134
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT") {
135
+ return true;
136
+ }
137
+ const rect = el.getBoundingClientRect();
138
+ if (rect.width === 0 && rect.height === 0) return false;
139
+ const style = window.getComputedStyle(el);
140
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
141
+ return true;
142
+ }
143
+ function clickableRectOf(el) {
144
+ let cur = el;
145
+ let depth = 0;
146
+ while (cur && depth < 6) {
147
+ const r = cur.getBoundingClientRect();
148
+ if (r.width > 0 && r.height > 0) {
149
+ return { x: r.x, y: r.y, w: r.width, h: r.height };
150
+ }
151
+ cur = cur.parentElement;
152
+ depth++;
153
+ }
154
+ return null;
155
+ }
156
+ function resolveAria(name) {
157
+ try {
158
+ const ariaMatches = document.querySelectorAll(`[aria-label="${cssEscape2(name)}"]`);
159
+ for (const el of Array.from(ariaMatches)) if (isVisible(el)) return el;
160
+ } catch {
161
+ }
162
+ const labels = document.querySelectorAll("label");
163
+ for (const label of Array.from(labels)) {
164
+ const txt = label.textContent?.trim();
165
+ if (txt !== name) continue;
166
+ const forId = label.getAttribute("for");
167
+ if (forId) {
168
+ const t = document.getElementById(forId);
169
+ if (t && isVisible(t)) return t;
170
+ }
171
+ const inner = label.querySelector("input, textarea, select");
172
+ if (inner && isVisible(inner)) return inner;
173
+ }
174
+ const candidates = document.querySelectorAll('button, a, [role="button"], [role="link"], [role="menuitem"], [role="option"]');
175
+ for (const el of Array.from(candidates)) {
176
+ if (el.textContent?.trim() === name && isVisible(el)) return el;
177
+ }
178
+ try {
179
+ const titleMatches = document.querySelectorAll(`[title="${cssEscape2(name)}"]`);
180
+ for (const el of Array.from(titleMatches)) if (isVisible(el)) return el;
181
+ } catch {
182
+ }
183
+ try {
184
+ const phMatches = document.querySelectorAll(`[placeholder="${cssEscape2(name)}"]`);
185
+ for (const el of Array.from(phMatches)) if (isVisible(el)) return el;
186
+ } catch {
187
+ }
188
+ return null;
189
+ }
190
+ function resolveText(text) {
191
+ const candidates = document.querySelectorAll('button, a, [role="button"], [role="link"]');
192
+ for (const el of Array.from(candidates)) {
193
+ if (el.textContent?.trim() === text && isVisible(el)) return el;
194
+ }
195
+ return null;
196
+ }
197
+ function resolveXPath(expr) {
198
+ try {
199
+ const result = document.evaluate(expr, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
200
+ const node = result.singleNodeValue;
201
+ if (node && node.getBoundingClientRect && isVisible(node)) {
202
+ return node;
203
+ }
204
+ } catch {
205
+ }
206
+ return null;
207
+ }
208
+ function resolveCss(sel) {
209
+ try {
210
+ const matches = document.querySelectorAll(sel);
211
+ for (const el of Array.from(matches)) if (isVisible(el)) return el;
212
+ } catch {
213
+ }
214
+ return null;
215
+ }
216
+ function resolveSingle(sel) {
217
+ const s = sel[0];
218
+ if (!s) return null;
219
+ if (s.startsWith("aria/")) return resolveAria(s.slice(5));
220
+ if (s.startsWith("text/")) return resolveText(s.slice(5));
221
+ if (s.startsWith("xpath=")) return resolveXPath(s.slice(6));
222
+ return resolveCss(s);
223
+ }
224
+ function resolveAny(selectors) {
225
+ for (const sel of selectors) {
226
+ const el = resolveSingle(sel);
227
+ if (el) return el;
228
+ }
229
+ return null;
230
+ }
231
+ function diagnoseFailure(selectors) {
232
+ const lines = [`[trq-resolve] FAIL url=${location.href}`];
233
+ for (const sel of selectors) {
234
+ const s = sel[0];
235
+ if (!s) {
236
+ lines.push(" (empty selector)");
237
+ continue;
238
+ }
239
+ if (s.startsWith("aria/")) {
240
+ const name = s.slice(5);
241
+ const ariaHits = document.querySelectorAll(`[aria-label]`);
242
+ const labels = Array.from(document.querySelectorAll("label")).map((l) => `"${l.textContent?.trim().slice(0, 80)}"`);
243
+ lines.push(` aria "${name}": labels-on-page=${labels.length} sample=${labels.slice(0, 8).join(" | ")}`);
244
+ } else if (s.startsWith("text/")) {
245
+ lines.push(` text "${s.slice(5)}": no match`);
246
+ } else if (s.startsWith("xpath=")) {
247
+ const x = s.slice(6);
248
+ try {
249
+ const r = document.evaluate(x, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
250
+ const n = r.singleNodeValue;
251
+ if (!n) lines.push(` xpath: no node`);
252
+ else lines.push(` xpath: matched <${n.tagName.toLowerCase()}> but isVisible=${isVisible(n)}`);
253
+ } catch (err) {
254
+ lines.push(` xpath: error ${err.message}`);
255
+ }
256
+ } else {
257
+ try {
258
+ const nodes = document.querySelectorAll(s);
259
+ if (nodes.length === 0) lines.push(` css: 0 matches for ${s.slice(0, 100)}`);
260
+ else lines.push(` css: ${nodes.length} match(es), first.isVisible=${isVisible(nodes[0])}`);
261
+ } catch (err) {
262
+ lines.push(` css: error ${err.message}`);
263
+ }
264
+ }
265
+ }
266
+ return lines.join("\n");
267
+ }
268
+ window.__trqResolve = (selectors) => {
269
+ const el = resolveAny(selectors);
270
+ if (!el) {
271
+ const w = window;
272
+ if (w.__trqDebugResolve) {
273
+ console.log(diagnoseFailure(selectors));
274
+ }
275
+ return { found: false };
276
+ }
277
+ const rect = clickableRectOf(el) ?? { x: 0, y: 0, w: 0, h: 0 };
278
+ return { found: true, rect, tag: el.tagName };
279
+ };
280
+ window.__trqResolveEl = (selectors) => resolveAny(selectors);
281
+ window.__trqClick = (selectors) => {
282
+ const el = resolveAny(selectors);
283
+ if (!el) return { error: "no element matched any selector" };
284
+ el.scrollIntoView({ block: "center", inline: "center" });
285
+ const rect = clickableRectOf(el);
286
+ if (!rect) return { error: "resolved element has no clickable bounds" };
287
+ if (el instanceof HTMLElement) {
288
+ try {
289
+ el.click();
290
+ } catch {
291
+ }
292
+ }
293
+ return { rect };
294
+ };
295
+ window.__trqSetInput = (selectors, value) => {
296
+ const el = resolveAny(selectors);
297
+ if (!el) return { error: "no element matched" };
298
+ if (el instanceof HTMLInputElement) {
299
+ if (el.type === "checkbox" || el.type === "radio") {
300
+ const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked");
301
+ desc?.set?.call(el, value === "true");
302
+ el.dispatchEvent(new Event("change", { bubbles: true }));
303
+ } else {
304
+ el.focus();
305
+ const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
306
+ desc?.set?.call(el, value);
307
+ el.dispatchEvent(new Event("input", { bubbles: true }));
308
+ el.dispatchEvent(new Event("change", { bubbles: true }));
309
+ el.blur();
310
+ }
311
+ return { ok: true };
312
+ }
313
+ if (el instanceof HTMLTextAreaElement) {
314
+ el.focus();
315
+ const desc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
316
+ desc?.set?.call(el, value);
317
+ el.dispatchEvent(new Event("input", { bubbles: true }));
318
+ el.dispatchEvent(new Event("change", { bubbles: true }));
319
+ el.blur();
320
+ return { ok: true };
321
+ }
322
+ if (el instanceof HTMLSelectElement) {
323
+ el.value = value;
324
+ el.dispatchEvent(new Event("input", { bubbles: true }));
325
+ el.dispatchEvent(new Event("change", { bubbles: true }));
326
+ el.blur();
327
+ return { ok: true };
328
+ }
329
+ return { error: `element <${el.tagName.toLowerCase()}> is not an input` };
330
+ };
331
+ window.__trqFocus = (selectors) => {
332
+ const el = resolveAny(selectors);
333
+ if (!el) return { error: "no element matched" };
334
+ if (el instanceof HTMLElement) el.focus();
335
+ return { ok: true };
336
+ };
337
+ window.__trqAssert = (selectors, spec) => {
338
+ const el = resolveAny(selectors);
339
+ if (!el) return { ok: false, reason: "no element matched any selector" };
340
+ switch (spec.kind) {
341
+ case "visible":
342
+ return isVisible(el) ? { ok: true } : { ok: false, reason: "element resolved but is not visible" };
343
+ case "text-equals": {
344
+ const actual = el.textContent?.trim() ?? "";
345
+ if (actual === spec.text) return { ok: true };
346
+ return {
347
+ ok: false,
348
+ reason: `expected text "${spec.text}" but found "${actual.slice(0, 120)}"`
349
+ };
350
+ }
351
+ case "value-equals": {
352
+ let actual = "";
353
+ if (el instanceof HTMLInputElement) {
354
+ actual = el.type === "checkbox" || el.type === "radio" ? String(el.checked) : el.value;
355
+ } else if (el instanceof HTMLTextAreaElement) {
356
+ actual = el.value;
357
+ } else if (el instanceof HTMLSelectElement) {
358
+ actual = el.value;
359
+ } else {
360
+ return {
361
+ ok: false,
362
+ reason: `element <${el.tagName.toLowerCase()}> is not an input`
363
+ };
364
+ }
365
+ if (actual === spec.value) return { ok: true };
366
+ return {
367
+ ok: false,
368
+ reason: `expected value "${spec.value}" but found "${actual.slice(0, 120)}"`
369
+ };
370
+ }
371
+ }
372
+ };
373
+ function isDynamicId2(id) {
374
+ if (/^(mat|cdk|ng|md|rc|react-select|radix-)/i.test(id) && /\d/.test(id)) return true;
375
+ if (/^:r[a-z0-9]+:?$/.test(id)) return true;
376
+ if (/^[a-z]+-\d{3,}$/i.test(id)) return true;
377
+ return false;
378
+ }
379
+ function isStableAttr(name) {
380
+ if (name.startsWith("_ng")) return false;
381
+ if (name.startsWith("data-react")) return false;
382
+ if (name === "class" || name === "style") return false;
383
+ return true;
384
+ }
385
+ function attrWeight(name) {
386
+ if (name === "data-testid" || name.startsWith("data-test") || name === "data-cy" || name === "data-qa") return 5;
387
+ if (name === "name" || name === "role") return 3;
388
+ if (name === "value" || name === "type" || name === "placeholder") return 2;
389
+ if (name === "href" || name === "aria-label" || name === "aria-labelledby") return 2;
390
+ return 1;
391
+ }
392
+ function scoreCandidate(el, target) {
393
+ if (el.tagName !== target.tagName) return null;
394
+ if (!isVisible(el)) return null;
395
+ let score = 0;
396
+ const matched = [];
397
+ const elId = el.id;
398
+ const tgtId = target.attributes.id;
399
+ if (elId && tgtId && !isDynamicId2(elId) && !isDynamicId2(tgtId)) {
400
+ if (elId === tgtId) {
401
+ score += 6;
402
+ matched.push("id");
403
+ }
404
+ }
405
+ for (const [name, value] of Object.entries(target.attributes)) {
406
+ if (name === "id") continue;
407
+ if (!isStableAttr(name)) continue;
408
+ const actual = el.getAttribute(name);
409
+ if (actual == null) continue;
410
+ if (actual === value) {
411
+ score += attrWeight(name);
412
+ matched.push(name);
413
+ }
414
+ }
415
+ const tgtClass = (target.attributes.class || "").split(/\s+/).filter(Boolean);
416
+ const elClass = (el.getAttribute("class") || "").split(/\s+/).filter(Boolean);
417
+ if (tgtClass.length > 0 && elClass.length > 0) {
418
+ const tgtSet = new Set(tgtClass.filter((c) => !/^ng-(touched|untouched|dirty|pristine|valid|invalid|focused)$/.test(c)));
419
+ let classHits = 0;
420
+ for (const c of elClass) if (tgtSet.has(c)) classHits++;
421
+ if (classHits > 0) {
422
+ score += Math.min(3, classHits * 0.5);
423
+ matched.push(`class\xD7${classHits}`);
424
+ }
425
+ }
426
+ const elText = (el.textContent || "").trim();
427
+ const tgtText = (target.textContent || "").trim();
428
+ if (tgtText && elText === tgtText) {
429
+ score += 3;
430
+ matched.push("text");
431
+ }
432
+ return score > 0 ? { score, matched } : null;
433
+ }
434
+ function snapshotOf(el) {
435
+ const r = el.getBoundingClientRect();
436
+ const attrs = {};
437
+ for (const a of Array.from(el.attributes)) attrs[a.name] = a.value;
438
+ return {
439
+ tagName: el.tagName,
440
+ textContent: (el.textContent || "").trim().slice(0, 200) || null,
441
+ attributes: attrs,
442
+ boundingBox: {
443
+ x: Math.round(r.x),
444
+ y: Math.round(r.y),
445
+ width: Math.round(r.width),
446
+ height: Math.round(r.height)
447
+ }
448
+ };
449
+ }
450
+ window.__trqSuggest = (target, max = 3) => {
451
+ const all = document.querySelectorAll(target.tagName);
452
+ const scored = [];
453
+ const limit = Math.min(all.length, 5e3);
454
+ for (let i = 0; i < limit; i++) {
455
+ const el = all[i];
456
+ const s = scoreCandidate(el, target);
457
+ if (!s) continue;
458
+ scored.push({
459
+ score: s.score,
460
+ matched: s.matched,
461
+ selectors: buildSelectors(el),
462
+ target: snapshotOf(el)
463
+ });
464
+ }
465
+ scored.sort((a, b) => b.score - a.score);
466
+ return scored.slice(0, max);
467
+ };
468
+ let pickActive = false;
469
+ let pickHovered = null;
470
+ const HOVER_OUTLINE = "2px solid #f48771";
471
+ const HOVER_BG = "rgba(244, 135, 113, 0.15)";
472
+ const pickOriginal = /* @__PURE__ */ new WeakMap();
473
+ function unhighlight(el) {
474
+ if (!el || !(el instanceof HTMLElement)) return;
475
+ const o = pickOriginal.get(el);
476
+ if (o) {
477
+ el.style.outline = o.outline;
478
+ el.style.backgroundColor = o.bg;
479
+ pickOriginal.delete(el);
480
+ }
481
+ }
482
+ function highlight(el) {
483
+ if (!(el instanceof HTMLElement)) return;
484
+ if (pickOriginal.has(el)) return;
485
+ pickOriginal.set(el, { outline: el.style.outline, bg: el.style.backgroundColor });
486
+ el.style.outline = HOVER_OUTLINE;
487
+ el.style.backgroundColor = HOVER_BG;
488
+ }
489
+ function onPickMove(e) {
490
+ if (!pickActive) return;
491
+ const el = e.target;
492
+ if (!el || el === pickHovered) return;
493
+ unhighlight(pickHovered);
494
+ pickHovered = el;
495
+ highlight(el);
496
+ }
497
+ function onPickClick(e) {
498
+ if (!pickActive) return;
499
+ e.preventDefault();
500
+ e.stopPropagation();
501
+ const el = e.target;
502
+ if (!el) return;
503
+ const payload = {
504
+ __trq: "pick",
505
+ selectors: buildSelectors(el),
506
+ target: snapshotOf(el)
507
+ };
508
+ cleanupPick();
509
+ try {
510
+ window.__trqEmit?.(JSON.stringify(payload));
511
+ } catch {
512
+ }
513
+ }
514
+ function onPickKey(e) {
515
+ if (!pickActive) return;
516
+ if (e.key === "Escape") {
517
+ e.preventDefault();
518
+ cleanupPick();
519
+ try {
520
+ window.__trqEmit?.(JSON.stringify({ __trq: "pick-cancel" }));
521
+ } catch {
522
+ }
523
+ }
524
+ }
525
+ function cleanupPick() {
526
+ pickActive = false;
527
+ unhighlight(pickHovered);
528
+ pickHovered = null;
529
+ document.removeEventListener("mousemove", onPickMove, true);
530
+ document.removeEventListener("click", onPickClick, true);
531
+ document.removeEventListener("keydown", onPickKey, true);
532
+ }
533
+ window.__trqStartPick = () => {
534
+ if (pickActive) return;
535
+ pickActive = true;
536
+ document.addEventListener("mousemove", onPickMove, true);
537
+ document.addEventListener("click", onPickClick, true);
538
+ document.addEventListener("keydown", onPickKey, true);
539
+ };
540
+ window.__trqStopPick = () => cleanupPick();
541
+ })();
542
+ })();
@@ -0,0 +1,13 @@
1
+ // src/preload/preload.ts
2
+ var import_electron = require("electron");
3
+ var bridge = {
4
+ invoke(channel, args) {
5
+ return import_electron.ipcRenderer.invoke(channel, args);
6
+ },
7
+ on(channel, listener) {
8
+ const wrapped = (_evt, payload) => listener(payload);
9
+ import_electron.ipcRenderer.on(channel, wrapped);
10
+ return () => import_electron.ipcRenderer.off(channel, wrapped);
11
+ }
12
+ };
13
+ import_electron.contextBridge.exposeInMainWorld("trq", bridge);