haltija 1.3.0-beta.8 → 1.3.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/README.md +30 -3
- package/apps/desktop/main.js +14 -0
- package/apps/desktop/package.json +2 -1
- package/apps/desktop/resources/component.js +138 -57
- package/bin/cli-subcommand.mjs +205 -17
- package/bin/hints.json +1 -1
- package/bin/hj.mjs +102 -2
- package/dist/agent-message-format.d.ts +56 -0
- package/dist/agent-shell.d.ts +161 -0
- package/dist/api-handlers.d.ts +107 -0
- package/dist/api-router.d.ts +29 -0
- package/dist/api-schema.d.ts +894 -0
- package/dist/bookmarklet.d.ts +21 -0
- package/dist/browser-tests.d.ts +49 -0
- package/dist/client.d.ts +168 -0
- package/dist/codemirror-bundle.d.ts +16 -0
- package/dist/component.d.ts +403 -0
- package/dist/component.esm.js +7545 -0
- package/dist/component.js +138 -57
- package/dist/embedded-assets.d.ts +18 -0
- package/dist/hj.js +275 -17
- package/dist/index.d.ts +46 -0
- package/dist/index.js +7948 -227
- package/dist/key-codes.d.ts +12 -0
- package/dist/mcp-config.d.ts +71 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.js +7948 -227
- package/dist/sessions.d.ts +49 -0
- package/dist/task-board.d.ts +45 -0
- package/dist/tasks.d.ts +77 -0
- package/dist/terminal.d.ts +154 -0
- package/dist/test-data.d.ts +37 -0
- package/dist/test-formatters.d.ts +68 -0
- package/dist/test-generator.d.ts +37 -0
- package/dist/test-page.d.ts +8 -0
- package/dist/test.d.ts +136 -0
- package/dist/text-selector.d.ts +30 -0
- package/dist/types.d.ts +1054 -0
- package/dist/version.d.ts +11 -0
- package/llms.txt +60 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -76,6 +76,26 @@ Haltija connects to the browser you're already using. The one with the bug, the
|
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
|
79
|
+
## Haltija vs. Claude in Chrome
|
|
80
|
+
|
|
81
|
+
Both let an agent drive a browser — they're built for different jobs. Because Haltija can either **spawn its own isolated browser** *or* **attach to your real one** (via a script tag / bookmarklet on a port you choose), it covers the axes a live-session-only tool can't.
|
|
82
|
+
|
|
83
|
+
| Axis | Haltija | Claude in Chrome |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| Setup / auth | ✅ `bunx haltija` — zero auth, self-spawns | ⚠️ Extension + claude.ai login + per-site permissions |
|
|
86
|
+
| How the agent invokes it | ✅ Plain `hj` CLI / REST — always available in a shell | ⚠️ MCP tools; must be loaded + extension connected |
|
|
87
|
+
| Project integration | ✅ *Is* your test harness — JSON tests (`hj test-suite`), record→replay, CI | ❌ None |
|
|
88
|
+
| Determinism / cost | ✅ DOM + `eval` with stable ref IDs — cheap text, scriptable, headless-CI-able | ⚠️ Has structured reads (page text, find-by-text) *and* vision/coordinate actions; the vision path is token-heavy and coordinate clicks less deterministic |
|
|
89
|
+
| Isolation | ✅ Own instance on its own port — never touches your browsing | ❌ Drives tabs in your live session |
|
|
90
|
+
| Real-browser fidelity (WebGL/3D, pixels) | ✅ Attach to a real GPU Chrome or the desktop app *(headless GPU is the one weak spot)* | ✅ Full real Chrome GPU |
|
|
91
|
+
| Authenticated / external sites | ✅ Inject into your logged-in browser on a chosen port *(one-time setup step)* | ✅ Native — already your logged-in profile |
|
|
92
|
+
| Flakiness / contention | ✅ A private port per project = dedicated instance *(a shared instance can still flake under contention)* | ⚠️ Depends on live browser + extension state |
|
|
93
|
+
| Screenshots | ✅ Native (desktop) or WebRTC `getDisplayMedia` (browser) | ✅ Vision-native |
|
|
94
|
+
|
|
95
|
+
**The short version:** reach for Claude in Chrome to glance at the tab you're already looking at; reach for Haltija when you want a browser your agent *controls* — reproducibly, cheaply, in CI, and as the regression harness your project already has.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
79
99
|
## How It Works
|
|
80
100
|
|
|
81
101
|
```
|
|
@@ -246,18 +266,25 @@ The server rejects every REST/WebSocket request without a matching `X-Haltija-To
|
|
|
246
266
|
## Installation
|
|
247
267
|
|
|
248
268
|
```bash
|
|
249
|
-
bunx haltija #
|
|
250
|
-
bunx haltija --server # Server only
|
|
269
|
+
bunx haltija # Bundled browser + embedded server (easiest start)
|
|
270
|
+
bunx haltija --server # Server only — your browser, per-project dev/debug
|
|
251
271
|
npm install -g haltija # Install globally
|
|
252
272
|
|
|
253
273
|
# Server options
|
|
254
274
|
haltija --https # HTTPS mode
|
|
255
275
|
haltija --port 3000 # Custom port
|
|
276
|
+
haltija --name <proj> # Register a per-project instance (hj --name <proj>)
|
|
256
277
|
haltija --token <secret> # Require X-Haltija-Token on every request
|
|
257
|
-
haltija --headless # For CI pipelines
|
|
278
|
+
haltija --headless # For CI pipelines (no desktop app needed)
|
|
258
279
|
haltija --setup-mcp # Configure Claude Desktop
|
|
259
280
|
```
|
|
260
281
|
|
|
282
|
+
**Which mode?** For day-to-day dev/debugging, run a per-project server
|
|
283
|
+
(`haltija --server --name <proj>`) and drive it with the `hj` CLI or your coding
|
|
284
|
+
agent — that's the paved path. For CI, `haltija --headless` runs a deterministic
|
|
285
|
+
headless Chromium with no desktop app required. A downloadable notarized desktop
|
|
286
|
+
app also exists but isn't required for either.
|
|
287
|
+
|
|
261
288
|
---
|
|
262
289
|
|
|
263
290
|
## Security
|
package/apps/desktop/main.js
CHANGED
|
@@ -1387,6 +1387,13 @@ if (!gotTheLock) {
|
|
|
1387
1387
|
// App lifecycle
|
|
1388
1388
|
app.whenReady().then(async () => {
|
|
1389
1389
|
console.log('[Haltija Desktop] App ready, starting initialization...')
|
|
1390
|
+
|
|
1391
|
+
// Clear the "manually quit" marker — user is explicitly starting Haltija,
|
|
1392
|
+
// so hj should resume auto-launching when needed.
|
|
1393
|
+
try {
|
|
1394
|
+
const quitMarker = path.join(os.homedir(), '.haltija', 'last-quit')
|
|
1395
|
+
if (fs.existsSync(quitMarker)) fs.rmSync(quitMarker, { force: true })
|
|
1396
|
+
} catch {}
|
|
1390
1397
|
|
|
1391
1398
|
try {
|
|
1392
1399
|
// Start or connect to server first
|
|
@@ -1444,6 +1451,13 @@ if (!gotTheLock) {
|
|
|
1444
1451
|
}
|
|
1445
1452
|
embeddedServers.length = 0
|
|
1446
1453
|
}
|
|
1454
|
+
// Drop a marker so hj's auto-launch knows the user explicitly quit.
|
|
1455
|
+
// Cleared next time the user manually starts Haltija.
|
|
1456
|
+
try {
|
|
1457
|
+
const dir = path.join(os.homedir(), '.haltija')
|
|
1458
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
1459
|
+
fs.writeFileSync(path.join(dir, 'last-quit'), String(Date.now()))
|
|
1460
|
+
} catch {}
|
|
1447
1461
|
})
|
|
1448
1462
|
|
|
1449
1463
|
// Handle certificate errors (for self-signed certs in dev)
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.3.0
|
|
49
|
+
var VERSION = "1.3.0";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -98,6 +98,47 @@
|
|
|
98
98
|
return lower.includes(parsed.searchText);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// src/key-codes.ts
|
|
102
|
+
function keyToCode(key) {
|
|
103
|
+
const specialKeys = {
|
|
104
|
+
Enter: "Enter",
|
|
105
|
+
Escape: "Escape",
|
|
106
|
+
Tab: "Tab",
|
|
107
|
+
Backspace: "Backspace",
|
|
108
|
+
Delete: "Delete",
|
|
109
|
+
ArrowUp: "ArrowUp",
|
|
110
|
+
ArrowDown: "ArrowDown",
|
|
111
|
+
ArrowLeft: "ArrowLeft",
|
|
112
|
+
ArrowRight: "ArrowRight",
|
|
113
|
+
Home: "Home",
|
|
114
|
+
End: "End",
|
|
115
|
+
PageUp: "PageUp",
|
|
116
|
+
PageDown: "PageDown",
|
|
117
|
+
" ": "Space",
|
|
118
|
+
Space: "Space",
|
|
119
|
+
".": "Period",
|
|
120
|
+
",": "Comma",
|
|
121
|
+
"/": "Slash",
|
|
122
|
+
";": "Semicolon",
|
|
123
|
+
"'": "Quote",
|
|
124
|
+
"[": "BracketLeft",
|
|
125
|
+
"]": "BracketRight",
|
|
126
|
+
"\\": "Backslash",
|
|
127
|
+
"-": "Minus",
|
|
128
|
+
"=": "Equal",
|
|
129
|
+
"`": "Backquote"
|
|
130
|
+
};
|
|
131
|
+
if (specialKeys[key])
|
|
132
|
+
return specialKeys[key];
|
|
133
|
+
if (/^F\d{1,2}$/.test(key))
|
|
134
|
+
return key;
|
|
135
|
+
if (/^[a-zA-Z]$/.test(key))
|
|
136
|
+
return `Key${key.toUpperCase()}`;
|
|
137
|
+
if (/^[0-9]$/.test(key))
|
|
138
|
+
return `Digit${key}`;
|
|
139
|
+
return key;
|
|
140
|
+
}
|
|
141
|
+
|
|
101
142
|
// src/component.ts
|
|
102
143
|
var VERSION2 = VERSION;
|
|
103
144
|
var PRODUCT_NAME = "Haltija";
|
|
@@ -1435,7 +1476,7 @@
|
|
|
1435
1476
|
if (htmlEl.disabled) {
|
|
1436
1477
|
flags.disabled = true;
|
|
1437
1478
|
}
|
|
1438
|
-
if (htmlEl.readOnly) {
|
|
1479
|
+
if ("readOnly" in htmlEl && htmlEl.readOnly) {
|
|
1439
1480
|
flags.readOnly = true;
|
|
1440
1481
|
}
|
|
1441
1482
|
if (!htmlEl.validity.valid) {
|
|
@@ -1717,6 +1758,8 @@
|
|
|
1717
1758
|
isElectron = false;
|
|
1718
1759
|
browserId = uid();
|
|
1719
1760
|
killed = false;
|
|
1761
|
+
displayStream = null;
|
|
1762
|
+
displayVideo = null;
|
|
1720
1763
|
isActive = true;
|
|
1721
1764
|
homeLeft = 0;
|
|
1722
1765
|
homeBottom = 16;
|
|
@@ -1918,6 +1961,7 @@
|
|
|
1918
1961
|
this.restoreDialogs();
|
|
1919
1962
|
this.clearEventWatchers();
|
|
1920
1963
|
this.stopMutationWatch();
|
|
1964
|
+
this.stopScreenCapture();
|
|
1921
1965
|
}
|
|
1922
1966
|
attributeChangedCallback(name, _old, value) {
|
|
1923
1967
|
if (name === "server") {
|
|
@@ -2085,6 +2129,10 @@
|
|
|
2085
2129
|
color: white;
|
|
2086
2130
|
animation: pulse 1s infinite;
|
|
2087
2131
|
}
|
|
2132
|
+
.btn[data-action="screen"].sharing {
|
|
2133
|
+
background: #3b82f6;
|
|
2134
|
+
color: white;
|
|
2135
|
+
}
|
|
2088
2136
|
.btn[data-action="logs"] {
|
|
2089
2137
|
font-size: 10px;
|
|
2090
2138
|
font-weight: 500;
|
|
@@ -2485,6 +2533,7 @@
|
|
|
2485
2533
|
<div class="controls">
|
|
2486
2534
|
<button class="btn" data-action="select" title="Click or drag to select elements" aria-label="Select elements">\uD83D\uDC46</button>
|
|
2487
2535
|
<button class="btn" data-action="record" title="Record test (click to start/stop)" aria-label="Record test">REC</button>
|
|
2536
|
+
<button class="btn" data-action="screen" title="Share screen so the agent can take screenshots (browser only; not needed in Haltija desktop app)" aria-label="Share screen for screenshots">\uD83D\uDDA5</button>
|
|
2488
2537
|
<button class="btn" data-action="logs" title="Show event log panel" aria-label="Toggle event log">LOG</button>
|
|
2489
2538
|
<button class="btn info-btn" data-action="stats" title="Copy stats to clipboard" aria-label="Copy stats">i</button>
|
|
2490
2539
|
<button class="btn" data-action="minimize" title="Minimize widget (⌥Tab)" aria-label="Minimize">─</button>
|
|
@@ -2566,6 +2615,8 @@
|
|
|
2566
2615
|
this.toggleRecording();
|
|
2567
2616
|
if (action2 === "select")
|
|
2568
2617
|
this.startSelection();
|
|
2618
|
+
if (action2 === "screen")
|
|
2619
|
+
this.toggleScreenCapture();
|
|
2569
2620
|
if (action2 === "stats")
|
|
2570
2621
|
this.copyStatsToClipboard();
|
|
2571
2622
|
if (action2 === "close-modal")
|
|
@@ -3016,7 +3067,7 @@
|
|
|
3016
3067
|
if (options.addAssertions && inputValue) {
|
|
3017
3068
|
steps.push({
|
|
3018
3069
|
action: "assert",
|
|
3019
|
-
assertion: { type: "value", selector,
|
|
3070
|
+
assertion: { type: "value", selector, value: inputValue },
|
|
3020
3071
|
description: `Verify ${inputLabel} is "${inputValue}"`
|
|
3021
3072
|
});
|
|
3022
3073
|
}
|
|
@@ -3845,6 +3896,55 @@ ${elementSummary}${moreText}`;
|
|
|
3845
3896
|
this.hide();
|
|
3846
3897
|
}
|
|
3847
3898
|
}
|
|
3899
|
+
async toggleScreenCapture() {
|
|
3900
|
+
if (this.displayStream) {
|
|
3901
|
+
this.stopScreenCapture();
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
|
|
3905
|
+
console.warn(`${LOG_PREFIX} Screen capture not supported in this browser.`);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
try {
|
|
3909
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
3910
|
+
video: { frameRate: { ideal: 5 } },
|
|
3911
|
+
audio: false
|
|
3912
|
+
});
|
|
3913
|
+
this.displayStream = stream;
|
|
3914
|
+
const video = document.createElement("video");
|
|
3915
|
+
video.srcObject = stream;
|
|
3916
|
+
video.muted = true;
|
|
3917
|
+
video.playsInline = true;
|
|
3918
|
+
await video.play().catch(() => {});
|
|
3919
|
+
this.displayVideo = video;
|
|
3920
|
+
stream.getVideoTracks()[0]?.addEventListener("ended", () => {
|
|
3921
|
+
this.stopScreenCapture();
|
|
3922
|
+
});
|
|
3923
|
+
const btn = this.shadowRoot?.querySelector('.btn[data-action="screen"]');
|
|
3924
|
+
btn?.classList.add("sharing");
|
|
3925
|
+
} catch (err) {
|
|
3926
|
+
console.log(`${LOG_PREFIX} Screen capture not started: ${err.message}`);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
stopScreenCapture() {
|
|
3930
|
+
if (this.displayStream) {
|
|
3931
|
+
for (const track of this.displayStream.getTracks()) {
|
|
3932
|
+
try {
|
|
3933
|
+
track.stop();
|
|
3934
|
+
} catch {}
|
|
3935
|
+
}
|
|
3936
|
+
this.displayStream = null;
|
|
3937
|
+
}
|
|
3938
|
+
if (this.displayVideo) {
|
|
3939
|
+
try {
|
|
3940
|
+
this.displayVideo.pause();
|
|
3941
|
+
} catch {}
|
|
3942
|
+
this.displayVideo.srcObject = null;
|
|
3943
|
+
this.displayVideo = null;
|
|
3944
|
+
}
|
|
3945
|
+
const btn = this.shadowRoot?.querySelector('.btn[data-action="screen"]');
|
|
3946
|
+
btn?.classList.remove("sharing");
|
|
3947
|
+
}
|
|
3848
3948
|
toggleMinimize() {
|
|
3849
3949
|
if (this.headless)
|
|
3850
3950
|
return;
|
|
@@ -5336,7 +5436,8 @@ ${elementSummary}${moreText}`;
|
|
|
5336
5436
|
scroll: 0,
|
|
5337
5437
|
mutation: 0,
|
|
5338
5438
|
console: 0,
|
|
5339
|
-
focus: 0
|
|
5439
|
+
focus: 0,
|
|
5440
|
+
recording: 0
|
|
5340
5441
|
};
|
|
5341
5442
|
this.statsStartTime = Date.now();
|
|
5342
5443
|
this.startSemanticEvents();
|
|
@@ -5781,7 +5882,37 @@ ${elementSummary}${moreText}`;
|
|
|
5781
5882
|
return;
|
|
5782
5883
|
}
|
|
5783
5884
|
}
|
|
5784
|
-
this.
|
|
5885
|
+
if (this.displayVideo && this.displayStream) {
|
|
5886
|
+
try {
|
|
5887
|
+
const video = this.displayVideo;
|
|
5888
|
+
const w = video.videoWidth;
|
|
5889
|
+
const h = video.videoHeight;
|
|
5890
|
+
if (!w || !h) {
|
|
5891
|
+
this.respond(msg2.id, false, null, "Screen capture stream has no frame data yet — try again in a moment.");
|
|
5892
|
+
return;
|
|
5893
|
+
}
|
|
5894
|
+
const canvas = document.createElement("canvas");
|
|
5895
|
+
canvas.width = w;
|
|
5896
|
+
canvas.height = h;
|
|
5897
|
+
const ctx = canvas.getContext("2d");
|
|
5898
|
+
ctx.drawImage(video, 0, 0, w, h);
|
|
5899
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
5900
|
+
const converted = await convertFormat(dataUrl);
|
|
5901
|
+
this.respond(msg2.id, true, {
|
|
5902
|
+
image: converted.image,
|
|
5903
|
+
viewport,
|
|
5904
|
+
format,
|
|
5905
|
+
width: converted.width,
|
|
5906
|
+
height: converted.height,
|
|
5907
|
+
source: "getDisplayMedia"
|
|
5908
|
+
});
|
|
5909
|
+
return;
|
|
5910
|
+
} catch (err) {
|
|
5911
|
+
this.respond(msg2.id, false, null, `Screen capture frame failed: ${err.message}`);
|
|
5912
|
+
return;
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
this.respond(msg2.id, false, null, "No screenshot capture available. Either run the Haltija Desktop app (npx haltija@latest -f) or click the \uD83D\uDDA5 button in the Haltija widget to share your screen.");
|
|
5785
5916
|
} catch (err) {
|
|
5786
5917
|
this.respond(msg2.id, false, null, err.message);
|
|
5787
5918
|
}
|
|
@@ -6113,7 +6244,7 @@ ${elementSummary}${moreText}`;
|
|
|
6113
6244
|
}
|
|
6114
6245
|
} else if (isContentEditable) {
|
|
6115
6246
|
document.execCommand("insertText", false, char);
|
|
6116
|
-
}
|
|
6247
|
+
}
|
|
6117
6248
|
el.dispatchEvent(new InputEvent("input", {
|
|
6118
6249
|
bubbles: true,
|
|
6119
6250
|
inputType: "insertText",
|
|
@@ -6193,31 +6324,6 @@ ${elementSummary}${moreText}`;
|
|
|
6193
6324
|
el.value = value;
|
|
6194
6325
|
}
|
|
6195
6326
|
}
|
|
6196
|
-
getKeyCode(char) {
|
|
6197
|
-
const upper = char.toUpperCase();
|
|
6198
|
-
if (upper >= "A" && upper <= "Z")
|
|
6199
|
-
return `Key${upper}`;
|
|
6200
|
-
if (char >= "0" && char <= "9")
|
|
6201
|
-
return `Digit${char}`;
|
|
6202
|
-
const specialKeys = {
|
|
6203
|
-
" ": "Space",
|
|
6204
|
-
".": "Period",
|
|
6205
|
-
",": "Comma",
|
|
6206
|
-
"/": "Slash",
|
|
6207
|
-
";": "Semicolon",
|
|
6208
|
-
"'": "Quote",
|
|
6209
|
-
"[": "BracketLeft",
|
|
6210
|
-
"]": "BracketRight",
|
|
6211
|
-
"\\": "Backslash",
|
|
6212
|
-
"-": "Minus",
|
|
6213
|
-
"=": "Equal",
|
|
6214
|
-
"`": "Backquote",
|
|
6215
|
-
Enter: "Enter",
|
|
6216
|
-
Tab: "Tab",
|
|
6217
|
-
Backspace: "Backspace"
|
|
6218
|
-
};
|
|
6219
|
-
return specialKeys[char] || `Key${upper}`;
|
|
6220
|
-
}
|
|
6221
6327
|
getAdjacentKeys() {
|
|
6222
6328
|
return {
|
|
6223
6329
|
a: ["s", "q", "w", "z"],
|
|
@@ -6464,32 +6570,7 @@ ${elementSummary}${moreText}`;
|
|
|
6464
6570
|
}
|
|
6465
6571
|
}
|
|
6466
6572
|
getKeyCode(key) {
|
|
6467
|
-
|
|
6468
|
-
Enter: "Enter",
|
|
6469
|
-
Escape: "Escape",
|
|
6470
|
-
Tab: "Tab",
|
|
6471
|
-
Backspace: "Backspace",
|
|
6472
|
-
Delete: "Delete",
|
|
6473
|
-
ArrowUp: "ArrowUp",
|
|
6474
|
-
ArrowDown: "ArrowDown",
|
|
6475
|
-
ArrowLeft: "ArrowLeft",
|
|
6476
|
-
ArrowRight: "ArrowRight",
|
|
6477
|
-
Home: "Home",
|
|
6478
|
-
End: "End",
|
|
6479
|
-
PageUp: "PageUp",
|
|
6480
|
-
PageDown: "PageDown",
|
|
6481
|
-
" ": "Space",
|
|
6482
|
-
Space: "Space"
|
|
6483
|
-
};
|
|
6484
|
-
if (specialKeys[key])
|
|
6485
|
-
return specialKeys[key];
|
|
6486
|
-
if (/^F\d{1,2}$/.test(key))
|
|
6487
|
-
return key;
|
|
6488
|
-
if (/^[a-zA-Z]$/.test(key))
|
|
6489
|
-
return `Key${key.toUpperCase()}`;
|
|
6490
|
-
if (/^[0-9]$/.test(key))
|
|
6491
|
-
return `Digit${key}`;
|
|
6492
|
-
return key;
|
|
6573
|
+
return keyToCode(key);
|
|
6493
6574
|
}
|
|
6494
6575
|
getElementLabel(el) {
|
|
6495
6576
|
const text = el.textContent?.trim().slice(0, 30);
|