@getsigil/core 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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/sigil.cjs +549 -0
- package/dist/sigil.d.cts +83 -0
- package/dist/sigil.d.ts +83 -0
- package/dist/sigil.js +524 -0
- package/package.json +53 -0
package/dist/sigil.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @getsigil/core - Visual markers for automated UI testing
|
|
3
|
+
* https://usesigil.dev
|
|
4
|
+
*/
|
|
5
|
+
interface SigilConfig {
|
|
6
|
+
/** Enable/disable Sigil markers */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Marker position: 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right' */
|
|
9
|
+
position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
10
|
+
/** Z-index for markers */
|
|
11
|
+
zIndex?: number;
|
|
12
|
+
/** Marker opacity (0-1) */
|
|
13
|
+
opacity?: number;
|
|
14
|
+
/** WebSocket port for executor communication */
|
|
15
|
+
wsPort?: number;
|
|
16
|
+
}
|
|
17
|
+
interface MarkerEncoding {
|
|
18
|
+
borderColor: number;
|
|
19
|
+
cellColors: number[];
|
|
20
|
+
}
|
|
21
|
+
declare class SigilCore {
|
|
22
|
+
private config;
|
|
23
|
+
private observer;
|
|
24
|
+
private wsConnection;
|
|
25
|
+
private wsReconnectTimer;
|
|
26
|
+
private markedElements;
|
|
27
|
+
private initialized;
|
|
28
|
+
/**
|
|
29
|
+
* Initialize Sigil with configuration
|
|
30
|
+
*/
|
|
31
|
+
init(options?: SigilConfig): void;
|
|
32
|
+
/**
|
|
33
|
+
* Update configuration
|
|
34
|
+
*/
|
|
35
|
+
configure(options: Partial<SigilConfig>): void;
|
|
36
|
+
/**
|
|
37
|
+
* Scan DOM and add markers to elements with data-sigil-id
|
|
38
|
+
*/
|
|
39
|
+
scan(root?: Document | Element): void;
|
|
40
|
+
/**
|
|
41
|
+
* Auto-discover interactive elements and add data-sigil-id attributes
|
|
42
|
+
*/
|
|
43
|
+
autoDiscover(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Show all markers
|
|
46
|
+
*/
|
|
47
|
+
show(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Hide all markers
|
|
50
|
+
*/
|
|
51
|
+
hide(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Clean up and remove all markers
|
|
54
|
+
*/
|
|
55
|
+
dispose(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Check if Sigil is enabled
|
|
58
|
+
*/
|
|
59
|
+
isEnabled(): boolean;
|
|
60
|
+
private injectStyles;
|
|
61
|
+
private addMarker;
|
|
62
|
+
private getPositionStyles;
|
|
63
|
+
private encode;
|
|
64
|
+
private sha256;
|
|
65
|
+
private createMarkerSvg;
|
|
66
|
+
private generateElementId;
|
|
67
|
+
private startObserver;
|
|
68
|
+
private stopObserver;
|
|
69
|
+
private removeAllMarkers;
|
|
70
|
+
private connectWebSocket;
|
|
71
|
+
private handleWebSocketMessage;
|
|
72
|
+
private searchForMarker;
|
|
73
|
+
private getDirection;
|
|
74
|
+
private scrollToMarker;
|
|
75
|
+
private readTextContent;
|
|
76
|
+
private readInputValue;
|
|
77
|
+
private selectOption;
|
|
78
|
+
private setCheckboxState;
|
|
79
|
+
private sendResult;
|
|
80
|
+
}
|
|
81
|
+
declare const sigil: SigilCore;
|
|
82
|
+
|
|
83
|
+
export { type MarkerEncoding, sigil as Sigil, type SigilConfig, sigil as default };
|
package/dist/sigil.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// src/sigil.ts
|
|
2
|
+
var HEX_COLORS = [
|
|
3
|
+
"#FF0000",
|
|
4
|
+
"#FFFF00",
|
|
5
|
+
"#00FF00",
|
|
6
|
+
"#00FFFF",
|
|
7
|
+
"#0000FF",
|
|
8
|
+
"#FF00FF",
|
|
9
|
+
"#FFFFFF",
|
|
10
|
+
"#000000"
|
|
11
|
+
];
|
|
12
|
+
var NO_CHILD_ELEMENTS = [
|
|
13
|
+
"INPUT",
|
|
14
|
+
"TEXTAREA",
|
|
15
|
+
"SELECT",
|
|
16
|
+
"IMG",
|
|
17
|
+
"BR",
|
|
18
|
+
"HR",
|
|
19
|
+
"META",
|
|
20
|
+
"LINK",
|
|
21
|
+
"AREA",
|
|
22
|
+
"BASE",
|
|
23
|
+
"COL",
|
|
24
|
+
"EMBED",
|
|
25
|
+
"PARAM",
|
|
26
|
+
"SOURCE",
|
|
27
|
+
"TRACK",
|
|
28
|
+
"WBR"
|
|
29
|
+
];
|
|
30
|
+
var SigilCore = class {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.config = {
|
|
33
|
+
enabled: true,
|
|
34
|
+
position: "center",
|
|
35
|
+
zIndex: 9999,
|
|
36
|
+
opacity: 1,
|
|
37
|
+
wsPort: 5050
|
|
38
|
+
};
|
|
39
|
+
this.observer = null;
|
|
40
|
+
this.wsConnection = null;
|
|
41
|
+
this.wsReconnectTimer = null;
|
|
42
|
+
this.markedElements = /* @__PURE__ */ new WeakSet();
|
|
43
|
+
this.initialized = false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Initialize Sigil with configuration
|
|
47
|
+
*/
|
|
48
|
+
init(options = {}) {
|
|
49
|
+
if (typeof window !== "undefined") {
|
|
50
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
51
|
+
const urlWsPort = urlParams.get("sigilWsPort");
|
|
52
|
+
if (urlWsPort) {
|
|
53
|
+
options.wsPort = parseInt(urlWsPort, 10);
|
|
54
|
+
options.opacity = options.opacity ?? 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.config = { ...this.config, ...options };
|
|
58
|
+
if (!this.config.enabled) return;
|
|
59
|
+
this.injectStyles();
|
|
60
|
+
this.scan();
|
|
61
|
+
this.startObserver();
|
|
62
|
+
this.connectWebSocket();
|
|
63
|
+
this.initialized = true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Update configuration
|
|
67
|
+
*/
|
|
68
|
+
configure(options) {
|
|
69
|
+
this.config = { ...this.config, ...options };
|
|
70
|
+
if (!this.config.enabled) {
|
|
71
|
+
this.stopObserver();
|
|
72
|
+
this.removeAllMarkers();
|
|
73
|
+
} else {
|
|
74
|
+
this.scan();
|
|
75
|
+
this.startObserver();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Scan DOM and add markers to elements with data-sigil-id
|
|
80
|
+
*/
|
|
81
|
+
scan(root = document) {
|
|
82
|
+
if (!this.config.enabled) return;
|
|
83
|
+
const elements = root.querySelectorAll("[data-sigil-id]");
|
|
84
|
+
elements.forEach((el) => this.addMarker(el));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Auto-discover interactive elements and add data-sigil-id attributes
|
|
88
|
+
*/
|
|
89
|
+
autoDiscover() {
|
|
90
|
+
const selectors = [
|
|
91
|
+
"button",
|
|
92
|
+
"a[href]",
|
|
93
|
+
'input:not([type="hidden"])',
|
|
94
|
+
"textarea",
|
|
95
|
+
"select",
|
|
96
|
+
'[role="button"]',
|
|
97
|
+
'[role="checkbox"]',
|
|
98
|
+
'[role="textbox"]',
|
|
99
|
+
'[role="combobox"]',
|
|
100
|
+
'[role="switch"]'
|
|
101
|
+
];
|
|
102
|
+
const allElements = [];
|
|
103
|
+
selectors.forEach((sel) => {
|
|
104
|
+
try {
|
|
105
|
+
document.querySelectorAll(sel).forEach((el) => allElements.push(el));
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const uniqueElements = [...new Set(allElements)].filter((el) => !el.hasAttribute("data-sigil-id"));
|
|
110
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
111
|
+
document.querySelectorAll("[data-sigil-id]").forEach((el) => {
|
|
112
|
+
usedIds.add(el.getAttribute("data-sigil-id"));
|
|
113
|
+
});
|
|
114
|
+
let counter = 0;
|
|
115
|
+
uniqueElements.forEach((el) => {
|
|
116
|
+
const id = this.generateElementId(el, counter++, usedIds);
|
|
117
|
+
if (id) {
|
|
118
|
+
el.setAttribute("data-sigil-id", id);
|
|
119
|
+
usedIds.add(id);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
console.log(`Sigil: Auto-discovered ${counter} interactive elements`);
|
|
123
|
+
this.scan();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Show all markers
|
|
127
|
+
*/
|
|
128
|
+
show() {
|
|
129
|
+
document.querySelectorAll(".sigil-marker").forEach((m) => {
|
|
130
|
+
m.style.opacity = "1";
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Hide all markers
|
|
135
|
+
*/
|
|
136
|
+
hide() {
|
|
137
|
+
document.querySelectorAll(".sigil-marker").forEach((m) => {
|
|
138
|
+
m.style.opacity = "0";
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Clean up and remove all markers
|
|
143
|
+
*/
|
|
144
|
+
dispose() {
|
|
145
|
+
this.stopObserver();
|
|
146
|
+
if (this.wsConnection) {
|
|
147
|
+
this.wsConnection.close();
|
|
148
|
+
this.wsConnection = null;
|
|
149
|
+
}
|
|
150
|
+
if (this.wsReconnectTimer) {
|
|
151
|
+
clearInterval(this.wsReconnectTimer);
|
|
152
|
+
this.wsReconnectTimer = null;
|
|
153
|
+
}
|
|
154
|
+
this.removeAllMarkers();
|
|
155
|
+
this.initialized = false;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if Sigil is enabled
|
|
159
|
+
*/
|
|
160
|
+
isEnabled() {
|
|
161
|
+
return this.config.enabled;
|
|
162
|
+
}
|
|
163
|
+
// ========== Private Methods ==========
|
|
164
|
+
injectStyles() {
|
|
165
|
+
if (document.getElementById("sigil-styles")) return;
|
|
166
|
+
const style = document.createElement("style");
|
|
167
|
+
style.id = "sigil-styles";
|
|
168
|
+
style.textContent = `
|
|
169
|
+
.mud-dialog, .mud-dialog-content, .mud-dialog-actions,
|
|
170
|
+
.modal-content, .modal-body, .modal-footer, .modal-header,
|
|
171
|
+
.rz-dialog, .rz-dialog-content,
|
|
172
|
+
[class*="dialog-content"], [class*="modal-content"], [class*="popup-content"] {
|
|
173
|
+
overflow: visible !important;
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
document.head.appendChild(style);
|
|
177
|
+
}
|
|
178
|
+
async addMarker(element) {
|
|
179
|
+
if (this.markedElements.has(element)) return;
|
|
180
|
+
if (!this.config.enabled) return;
|
|
181
|
+
const markerId = element.getAttribute("data-sigil-id");
|
|
182
|
+
if (!markerId) return;
|
|
183
|
+
this.markedElements.add(element);
|
|
184
|
+
const encoding = await this.encode(markerId);
|
|
185
|
+
const svgContent = this.createMarkerSvg(encoding);
|
|
186
|
+
const marker = document.createElement("div");
|
|
187
|
+
marker.className = "sigil-marker";
|
|
188
|
+
marker.innerHTML = svgContent;
|
|
189
|
+
marker.setAttribute("data-sigil-for", markerId);
|
|
190
|
+
const canHaveChildren = !NO_CHILD_ELEMENTS.includes(element.tagName);
|
|
191
|
+
if (canHaveChildren) {
|
|
192
|
+
const computedStyle = window.getComputedStyle(element);
|
|
193
|
+
if (computedStyle.position === "static") {
|
|
194
|
+
element.style.position = "relative";
|
|
195
|
+
}
|
|
196
|
+
element.style.overflow = "visible";
|
|
197
|
+
const pos = this.getPositionStyles();
|
|
198
|
+
Object.assign(marker.style, {
|
|
199
|
+
position: "absolute",
|
|
200
|
+
...pos,
|
|
201
|
+
zIndex: this.config.zIndex.toString(),
|
|
202
|
+
opacity: this.config.opacity.toString(),
|
|
203
|
+
pointerEvents: "none",
|
|
204
|
+
lineHeight: "0"
|
|
205
|
+
});
|
|
206
|
+
element.appendChild(marker);
|
|
207
|
+
} else {
|
|
208
|
+
const updatePosition = () => {
|
|
209
|
+
const rect = element.getBoundingClientRect();
|
|
210
|
+
Object.assign(marker.style, {
|
|
211
|
+
position: "fixed",
|
|
212
|
+
top: rect.top + rect.height / 2 - 8 + "px",
|
|
213
|
+
left: rect.left + rect.width / 2 - 8 + "px",
|
|
214
|
+
zIndex: this.config.zIndex.toString(),
|
|
215
|
+
opacity: this.config.opacity.toString(),
|
|
216
|
+
pointerEvents: "none",
|
|
217
|
+
lineHeight: "0"
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
updatePosition();
|
|
221
|
+
document.body.appendChild(marker);
|
|
222
|
+
window.addEventListener("scroll", updatePosition, { passive: true });
|
|
223
|
+
window.addEventListener("resize", updatePosition, { passive: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
getPositionStyles() {
|
|
227
|
+
const positions = {
|
|
228
|
+
"center": { top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
|
|
229
|
+
"top-left": { top: "0", left: "0" },
|
|
230
|
+
"top-right": { top: "0", right: "0" },
|
|
231
|
+
"bottom-left": { bottom: "0", left: "0" },
|
|
232
|
+
"bottom-right": { bottom: "0", right: "0" }
|
|
233
|
+
};
|
|
234
|
+
return positions[this.config.position] || positions["center"];
|
|
235
|
+
}
|
|
236
|
+
async encode(markerId) {
|
|
237
|
+
const hash = await this.sha256(markerId);
|
|
238
|
+
const borderColor = hash[0] & 7;
|
|
239
|
+
const cellColors = [];
|
|
240
|
+
for (let i = 0; i < 9; i++) {
|
|
241
|
+
const byteIndex = 1 + Math.floor(i * 3 / 8);
|
|
242
|
+
const bitOffset = i * 3 % 8;
|
|
243
|
+
let value;
|
|
244
|
+
if (bitOffset <= 5) {
|
|
245
|
+
value = hash[byteIndex] >> bitOffset & 7;
|
|
246
|
+
} else {
|
|
247
|
+
value = (hash[byteIndex] >> bitOffset | hash[byteIndex + 1] << 8 - bitOffset) & 7;
|
|
248
|
+
}
|
|
249
|
+
cellColors.push(value);
|
|
250
|
+
}
|
|
251
|
+
return { borderColor, cellColors };
|
|
252
|
+
}
|
|
253
|
+
async sha256(message) {
|
|
254
|
+
const msgBuffer = new TextEncoder().encode(message);
|
|
255
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
|
|
256
|
+
return new Uint8Array(hashBuffer);
|
|
257
|
+
}
|
|
258
|
+
createMarkerSvg(encoding) {
|
|
259
|
+
const borderHex = HEX_COLORS[encoding.borderColor];
|
|
260
|
+
let svg = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" style="image-rendering: pixelated; shape-rendering: crispEdges;">`;
|
|
261
|
+
svg += `<rect x="0" y="0" width="1" height="1" fill="#FF00FF"/>`;
|
|
262
|
+
svg += `<rect x="1" y="0" width="1" height="1" fill="#00FFFF"/>`;
|
|
263
|
+
svg += `<rect x="0" y="1" width="1" height="1" fill="#00FFFF"/>`;
|
|
264
|
+
svg += `<rect x="1" y="1" width="1" height="1" fill="#FF00FF"/>`;
|
|
265
|
+
svg += `<rect x="2" y="0" width="14" height="2" fill="${borderHex}"/>`;
|
|
266
|
+
svg += `<rect x="0" y="2" width="2" height="12" fill="${borderHex}"/>`;
|
|
267
|
+
svg += `<rect x="14" y="2" width="2" height="12" fill="${borderHex}"/>`;
|
|
268
|
+
svg += `<rect x="0" y="14" width="16" height="2" fill="${borderHex}"/>`;
|
|
269
|
+
for (let row = 0; row < 3; row++) {
|
|
270
|
+
for (let col = 0; col < 3; col++) {
|
|
271
|
+
const idx = row * 3 + col;
|
|
272
|
+
const x = 2 + col * 4;
|
|
273
|
+
const y = 2 + row * 4;
|
|
274
|
+
const cellHex = HEX_COLORS[encoding.cellColors[idx]];
|
|
275
|
+
svg += `<rect x="${x}" y="${y}" width="4" height="4" fill="${cellHex}"/>`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
svg += `</svg>`;
|
|
279
|
+
return svg;
|
|
280
|
+
}
|
|
281
|
+
generateElementId(el, index, usedIds) {
|
|
282
|
+
const tag = el.tagName.toLowerCase();
|
|
283
|
+
const isFormElement = ["input", "textarea", "select", "button"].includes(tag);
|
|
284
|
+
const sanitize = (str) => str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 30);
|
|
285
|
+
if (isFormElement) {
|
|
286
|
+
let id2 = el.id;
|
|
287
|
+
if (id2) {
|
|
288
|
+
id2 = sanitize(id2);
|
|
289
|
+
if (!usedIds.has(id2)) return id2;
|
|
290
|
+
id2 = `${tag}-${id2}`;
|
|
291
|
+
if (!usedIds.has(id2)) return id2;
|
|
292
|
+
}
|
|
293
|
+
id2 = el.getAttribute("name") || "";
|
|
294
|
+
if (id2) {
|
|
295
|
+
id2 = sanitize(id2);
|
|
296
|
+
if (!usedIds.has(id2)) return id2;
|
|
297
|
+
id2 = `${tag}-${id2}`;
|
|
298
|
+
if (!usedIds.has(id2)) return id2;
|
|
299
|
+
}
|
|
300
|
+
id2 = el.getAttribute("aria-label") || "";
|
|
301
|
+
if (id2) {
|
|
302
|
+
id2 = sanitize(id2);
|
|
303
|
+
if (!usedIds.has(id2)) return id2;
|
|
304
|
+
}
|
|
305
|
+
id2 = el.getAttribute("placeholder") || "";
|
|
306
|
+
if (id2) {
|
|
307
|
+
id2 = sanitize(id2);
|
|
308
|
+
if (!usedIds.has(id2)) return id2;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
let id2 = el.getAttribute("aria-label") || "";
|
|
312
|
+
if (id2) {
|
|
313
|
+
id2 = sanitize(id2);
|
|
314
|
+
if (!usedIds.has(id2)) return id2;
|
|
315
|
+
}
|
|
316
|
+
const text = el.textContent?.trim() || "";
|
|
317
|
+
if (text && text.length <= 30) {
|
|
318
|
+
id2 = sanitize(text);
|
|
319
|
+
if (!usedIds.has(id2)) return id2;
|
|
320
|
+
}
|
|
321
|
+
if (tag === "a") {
|
|
322
|
+
const href = el.getAttribute("href") || "";
|
|
323
|
+
if (href && href !== "#") {
|
|
324
|
+
id2 = sanitize(href.replace(/^[#/]+/, ""));
|
|
325
|
+
if (id2 && !usedIds.has(id2)) return id2;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
let id = `${tag}-${index}`;
|
|
330
|
+
while (usedIds.has(id)) {
|
|
331
|
+
id = `${tag}-${++index}`;
|
|
332
|
+
}
|
|
333
|
+
return id;
|
|
334
|
+
}
|
|
335
|
+
startObserver() {
|
|
336
|
+
if (this.observer) return;
|
|
337
|
+
this.observer = new MutationObserver((mutations) => {
|
|
338
|
+
mutations.forEach((mutation) => {
|
|
339
|
+
mutation.addedNodes.forEach((node) => {
|
|
340
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
341
|
+
const el = node;
|
|
342
|
+
if (el.hasAttribute?.("data-sigil-id")) {
|
|
343
|
+
this.addMarker(el);
|
|
344
|
+
}
|
|
345
|
+
this.scan(el);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
this.observer.observe(document.body, {
|
|
351
|
+
childList: true,
|
|
352
|
+
subtree: true
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
stopObserver() {
|
|
356
|
+
if (this.observer) {
|
|
357
|
+
this.observer.disconnect();
|
|
358
|
+
this.observer = null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
removeAllMarkers() {
|
|
362
|
+
document.querySelectorAll(".sigil-marker").forEach((m) => m.remove());
|
|
363
|
+
}
|
|
364
|
+
connectWebSocket() {
|
|
365
|
+
if (typeof WebSocket === "undefined") return;
|
|
366
|
+
if (this.wsConnection?.readyState === WebSocket.OPEN) return;
|
|
367
|
+
try {
|
|
368
|
+
this.wsConnection = new WebSocket(`ws://127.0.0.1:${this.config.wsPort}`);
|
|
369
|
+
this.wsConnection.onopen = () => {
|
|
370
|
+
console.log("Sigil: Connected to executor");
|
|
371
|
+
if (this.wsReconnectTimer) {
|
|
372
|
+
clearInterval(this.wsReconnectTimer);
|
|
373
|
+
this.wsReconnectTimer = null;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
this.wsConnection.onmessage = (event) => {
|
|
377
|
+
this.handleWebSocketMessage(event.data);
|
|
378
|
+
};
|
|
379
|
+
this.wsConnection.onclose = () => {
|
|
380
|
+
if (!this.wsReconnectTimer) {
|
|
381
|
+
this.wsReconnectTimer = setInterval(() => this.connectWebSocket(), 2e3);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
this.wsConnection.onerror = () => {
|
|
385
|
+
};
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
handleWebSocketMessage(data) {
|
|
390
|
+
const cmd = data.trim();
|
|
391
|
+
const cmdLower = cmd.toLowerCase();
|
|
392
|
+
if (cmdLower === "show") {
|
|
393
|
+
this.show();
|
|
394
|
+
} else if (cmdLower === "hide") {
|
|
395
|
+
this.hide();
|
|
396
|
+
} else if (cmdLower.startsWith("search:")) {
|
|
397
|
+
const markerId = cmd.substring(7);
|
|
398
|
+
const result = this.searchForMarker(markerId);
|
|
399
|
+
this.sendResult(result);
|
|
400
|
+
} else if (cmdLower.startsWith("scrollto:")) {
|
|
401
|
+
const markerId = cmd.substring(9);
|
|
402
|
+
this.scrollToMarker(markerId);
|
|
403
|
+
} else if (cmdLower.startsWith("read:text:")) {
|
|
404
|
+
const markerId = cmd.substring(10);
|
|
405
|
+
const result = this.readTextContent(markerId);
|
|
406
|
+
this.sendResult(result);
|
|
407
|
+
} else if (cmdLower.startsWith("read:value:")) {
|
|
408
|
+
const markerId = cmd.substring(11);
|
|
409
|
+
const result = this.readInputValue(markerId);
|
|
410
|
+
this.sendResult(result);
|
|
411
|
+
} else if (cmdLower.startsWith("select:")) {
|
|
412
|
+
const parts = cmd.substring(7).split(":");
|
|
413
|
+
const markerId = parts[0];
|
|
414
|
+
const optionValue = parts.slice(1).join(":");
|
|
415
|
+
Promise.resolve(this.selectOption(markerId, optionValue)).then((result) => {
|
|
416
|
+
this.sendResult(result);
|
|
417
|
+
});
|
|
418
|
+
} else if (cmdLower.startsWith("check:")) {
|
|
419
|
+
const markerId = cmd.substring(6);
|
|
420
|
+
const result = this.setCheckboxState(markerId, true);
|
|
421
|
+
this.sendResult(result);
|
|
422
|
+
} else if (cmdLower.startsWith("uncheck:")) {
|
|
423
|
+
const markerId = cmd.substring(8);
|
|
424
|
+
const result = this.setCheckboxState(markerId, false);
|
|
425
|
+
this.sendResult(result);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
searchForMarker(markerId) {
|
|
429
|
+
const element = document.querySelector(`[data-sigil-id="${markerId}"]`);
|
|
430
|
+
if (!element) {
|
|
431
|
+
return { found: false, visible: false, markerId };
|
|
432
|
+
}
|
|
433
|
+
const rect = element.getBoundingClientRect();
|
|
434
|
+
const viewportHeight = window.innerHeight;
|
|
435
|
+
const viewportWidth = window.innerWidth;
|
|
436
|
+
const markerBottom = rect.bottom + 16;
|
|
437
|
+
const inViewport = rect.bottom > 0 && markerBottom < viewportHeight && rect.left < viewportWidth && rect.right > 0;
|
|
438
|
+
return {
|
|
439
|
+
found: true,
|
|
440
|
+
visible: inViewport,
|
|
441
|
+
markerId,
|
|
442
|
+
direction: inViewport ? null : this.getDirection(rect, viewportWidth, viewportHeight),
|
|
443
|
+
offsetX: 0,
|
|
444
|
+
offsetY: 0
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
getDirection(rect, vw, vh) {
|
|
448
|
+
let dir = "";
|
|
449
|
+
if (rect.bottom < 0) dir = "up";
|
|
450
|
+
else if (rect.bottom + 16 > vh) dir = "down";
|
|
451
|
+
if (rect.right < 0) dir += dir ? "-left" : "left";
|
|
452
|
+
else if (rect.left > vw) dir += dir ? "-right" : "right";
|
|
453
|
+
return dir || "down";
|
|
454
|
+
}
|
|
455
|
+
scrollToMarker(markerId) {
|
|
456
|
+
const element = document.querySelector(`[data-sigil-id="${markerId}"]`);
|
|
457
|
+
if (element) {
|
|
458
|
+
element.scrollIntoView({ behavior: "instant", block: "center" });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
readTextContent(markerId) {
|
|
462
|
+
const element = document.querySelector(`[data-sigil-id="${markerId}"]`);
|
|
463
|
+
if (!element) return { success: false, value: "", error: "Element not found" };
|
|
464
|
+
return { success: true, value: element.textContent?.trim() || "" };
|
|
465
|
+
}
|
|
466
|
+
readInputValue(markerId) {
|
|
467
|
+
const element = document.querySelector(`[data-sigil-id="${markerId}"]`);
|
|
468
|
+
if (!element) return { success: false, value: "", error: "Element not found" };
|
|
469
|
+
const input = element.querySelector("input, textarea, select") || element;
|
|
470
|
+
return { success: true, value: input.value || "" };
|
|
471
|
+
}
|
|
472
|
+
selectOption(markerId, optionValue) {
|
|
473
|
+
const element = document.querySelector(`[data-sigil-id="${markerId}"]`);
|
|
474
|
+
if (!element) return { success: false, error: "Element not found" };
|
|
475
|
+
const select = element.querySelector("select") || element;
|
|
476
|
+
if (select.tagName === "SELECT") {
|
|
477
|
+
const option = Array.from(select.options).find(
|
|
478
|
+
(opt) => opt.value === optionValue || opt.text.trim() === optionValue
|
|
479
|
+
);
|
|
480
|
+
if (option) {
|
|
481
|
+
select.value = option.value;
|
|
482
|
+
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
483
|
+
return { success: true };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
element.click();
|
|
487
|
+
return new Promise((resolve) => {
|
|
488
|
+
setTimeout(() => {
|
|
489
|
+
const options = document.querySelectorAll('[role="option"], .dropdown-item, li');
|
|
490
|
+
for (const opt of options) {
|
|
491
|
+
if (opt.textContent?.trim() === optionValue) {
|
|
492
|
+
opt.click();
|
|
493
|
+
resolve({ success: true });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
resolve({ success: false, error: "Option not found" });
|
|
498
|
+
}, 200);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
setCheckboxState(markerId, shouldCheck) {
|
|
502
|
+
const element = document.querySelector(`[data-sigil-id="${markerId}"]`);
|
|
503
|
+
if (!element) return { success: false, error: "Element not found" };
|
|
504
|
+
const checkbox = element.querySelector('input[type="checkbox"]') || element;
|
|
505
|
+
if (checkbox.type === "checkbox" && checkbox.checked !== shouldCheck) {
|
|
506
|
+
checkbox.click();
|
|
507
|
+
}
|
|
508
|
+
return { success: true };
|
|
509
|
+
}
|
|
510
|
+
sendResult(result) {
|
|
511
|
+
if (this.wsConnection?.readyState === WebSocket.OPEN) {
|
|
512
|
+
this.wsConnection.send(JSON.stringify(result));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
var sigil = new SigilCore();
|
|
517
|
+
var sigil_default = sigil;
|
|
518
|
+
if (typeof window !== "undefined") {
|
|
519
|
+
window.Sigil = sigil;
|
|
520
|
+
}
|
|
521
|
+
export {
|
|
522
|
+
sigil as Sigil,
|
|
523
|
+
sigil_default as default
|
|
524
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getsigil/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual markers for automated UI testing - zero-config browser automation",
|
|
5
|
+
"author": "Iridium Softworks",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://usesigil.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/iridium-softworks/sigil.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/iridium-softworks/sigil/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"testing",
|
|
17
|
+
"ui-testing",
|
|
18
|
+
"automation",
|
|
19
|
+
"browser-automation",
|
|
20
|
+
"visual-testing",
|
|
21
|
+
"markers",
|
|
22
|
+
"e2e",
|
|
23
|
+
"end-to-end"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/sigil.cjs",
|
|
27
|
+
"module": "./dist/sigil.js",
|
|
28
|
+
"types": "./dist/sigil.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/sigil.js",
|
|
32
|
+
"require": "./dist/sigil.cjs",
|
|
33
|
+
"types": "./dist/sigil.d.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup src/sigil.ts --format cjs,esm --dts --clean",
|
|
42
|
+
"dev": "tsup src/sigil.ts --format cjs,esm --dts --watch",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsup": "^8.0.0",
|
|
48
|
+
"typescript": "^5.3.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|