haltija 1.1.21 → 1.2.2
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/apps/desktop/index.html +1 -1
- package/apps/desktop/main.js +264 -64
- package/apps/desktop/package.json +11 -3
- package/apps/desktop/preload.js +17 -0
- package/apps/desktop/renderer/agent-status.js +210 -0
- package/apps/desktop/renderer/settings.js +55 -0
- package/apps/desktop/renderer/state.js +98 -0
- package/apps/desktop/renderer/status.js +38 -0
- package/apps/desktop/renderer/tabs.js +374 -0
- package/apps/desktop/renderer/ui-utils.js +180 -0
- package/apps/desktop/renderer/video-capture.js +154 -0
- package/apps/desktop/renderer/webview-events.js +225 -0
- package/apps/desktop/renderer.js +98 -1604
- package/apps/desktop/resources/component.js +265 -55
- package/apps/desktop/webview-preload.js +19 -1
- package/bin/cli-subcommand.mjs +90 -27
- package/bin/hints.json +9 -4
- package/bin/hj.mjs +61 -2
- package/bin/test-data.mjs +291 -0
- package/bin/tosijs-dev.mjs +95 -20
- package/dist/client.js +5 -1
- package/dist/component.js +265 -55
- package/dist/index.js +444 -76
- package/dist/server.js +444 -76
- package/package.json +2 -1
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -413,18 +413,62 @@ function printBanner(mode, port) {
|
|
|
413
413
|
console.log('')
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
416
|
+
/** Resolve the electron binary path without spawning Electron.
|
|
417
|
+
* IMPORTANT: Never use `npx electron -e ...` or `electron --version` here —
|
|
418
|
+
* those launch a visible Electron window, causing UI flicker on startup. */
|
|
419
|
+
let _cachedElectronBinary = undefined
|
|
420
|
+
function resolveElectronBinary() {
|
|
421
|
+
if (_cachedElectronBinary !== undefined) return _cachedElectronBinary
|
|
422
|
+
|
|
423
|
+
// Helper: given an electron dist dir, return the binary path
|
|
424
|
+
const binaryInDist = (distDir) => {
|
|
425
|
+
const binary = platform() === 'darwin'
|
|
426
|
+
? join(distDir, 'Electron.app/Contents/MacOS/Electron')
|
|
427
|
+
: platform() === 'win32'
|
|
428
|
+
? join(distDir, 'electron.exe')
|
|
429
|
+
: join(distDir, 'electron')
|
|
430
|
+
return existsSync(binary) ? binary : null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 1. Check desktop app's own node_modules
|
|
434
|
+
const desktopDist = join(__dirname, '../apps/desktop/node_modules/electron/dist')
|
|
435
|
+
const desktopBin = binaryInDist(desktopDist)
|
|
436
|
+
if (desktopBin) { _cachedElectronBinary = desktopBin; return desktopBin }
|
|
437
|
+
|
|
438
|
+
// 2. Read the electron npm package's path.txt (no process spawn needed)
|
|
418
439
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
440
|
+
const electronPkgDir = join(__dirname, '../apps/desktop/node_modules/electron')
|
|
441
|
+
const pathTxt = join(electronPkgDir, 'path.txt')
|
|
442
|
+
if (existsSync(pathTxt)) {
|
|
443
|
+
const relPath = readFileSync(pathTxt, 'utf8').trim()
|
|
444
|
+
const absPath = join(electronPkgDir, relPath)
|
|
445
|
+
if (existsSync(absPath)) { _cachedElectronBinary = absPath; return absPath }
|
|
446
|
+
}
|
|
447
|
+
} catch {}
|
|
448
|
+
|
|
449
|
+
// 3. Search npx cache for electron dist directories (no Electron launch)
|
|
450
|
+
if (platform() !== 'win32') {
|
|
451
|
+
try {
|
|
452
|
+
const cacheHits = execSyncImported(
|
|
453
|
+
`find ${homedir()}/.npm/_npx -name "Electron.app" -path "*/electron/dist/*" -type d 2>/dev/null`,
|
|
454
|
+
{ encoding: 'utf-8', timeout: 3000 }
|
|
455
|
+
).trim().split('\n').filter(Boolean)
|
|
456
|
+
for (const hit of cacheHits) {
|
|
457
|
+
// hit is .../electron/dist/Electron.app — we want the dist dir
|
|
458
|
+
const distDir = join(hit, '..')
|
|
459
|
+
const bin = binaryInDist(distDir)
|
|
460
|
+
if (bin) { _cachedElectronBinary = bin; return bin }
|
|
461
|
+
}
|
|
462
|
+
} catch {}
|
|
427
463
|
}
|
|
464
|
+
|
|
465
|
+
_cachedElectronBinary = null
|
|
466
|
+
return null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Check if electron is available (cached by npx or installed) */
|
|
470
|
+
function isElectronAvailable() {
|
|
471
|
+
return resolveElectronBinary() !== null
|
|
428
472
|
}
|
|
429
473
|
|
|
430
474
|
/** Launch the Electron desktop app using npx electron */
|
|
@@ -438,11 +482,17 @@ function launchApp(desktopDir, port) {
|
|
|
438
482
|
|
|
439
483
|
printBanner(ciMode ? 'ci' : 'app', port)
|
|
440
484
|
|
|
441
|
-
//
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
485
|
+
// Resolve electron binary directly to avoid npx cache race conditions (ENOTEMPTY)
|
|
486
|
+
const electronBinary = resolveElectronBinary()
|
|
487
|
+
const child = electronBinary
|
|
488
|
+
? spawn(electronBinary, [desktopDir], {
|
|
489
|
+
env: { ...env, DEV_CHANNEL_PORT: String(port) },
|
|
490
|
+
stdio: 'inherit'
|
|
491
|
+
})
|
|
492
|
+
: spawn('npx', ['--yes', 'electron', desktopDir], {
|
|
493
|
+
env: { ...env, DEV_CHANNEL_PORT: String(port) },
|
|
494
|
+
stdio: 'inherit'
|
|
495
|
+
})
|
|
446
496
|
|
|
447
497
|
child.on('error', (err) => {
|
|
448
498
|
console.error(red('Error:') + ` Failed to launch desktop app: ${err.message}`)
|
|
@@ -700,9 +750,12 @@ async function checkExistingServer(port) {
|
|
|
700
750
|
}
|
|
701
751
|
}
|
|
702
752
|
|
|
703
|
-
/** Kill process on port (cross-platform) */
|
|
753
|
+
/** Kill process on port and any related Electron processes (cross-platform) */
|
|
704
754
|
function killOnPort(port) {
|
|
705
755
|
const myPid = process.pid.toString()
|
|
756
|
+
let killed = false
|
|
757
|
+
|
|
758
|
+
// 1. Kill processes listening on the port (the Haltija server)
|
|
706
759
|
try {
|
|
707
760
|
const output = execSyncImported(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim()
|
|
708
761
|
if (output) {
|
|
@@ -710,12 +763,34 @@ function killOnPort(port) {
|
|
|
710
763
|
for (const pid of pids) {
|
|
711
764
|
try {
|
|
712
765
|
execSyncImported(`kill ${pid} 2>/dev/null`)
|
|
766
|
+
killed = true
|
|
713
767
|
} catch {}
|
|
714
768
|
}
|
|
715
|
-
return pids.length > 0
|
|
716
769
|
}
|
|
717
770
|
} catch {}
|
|
718
|
-
|
|
771
|
+
|
|
772
|
+
// 2. Kill any Electron processes running the desktop app
|
|
773
|
+
// The Electron parent process isn't on the port, so lsof won't find it.
|
|
774
|
+
// Without this, npx's cache directory stays locked causing ENOTEMPTY on restart.
|
|
775
|
+
if (platform() !== 'win32') {
|
|
776
|
+
try {
|
|
777
|
+
const output = execSyncImported(
|
|
778
|
+
`pgrep -f 'Electron.*apps/desktop|electron.*apps/desktop' 2>/dev/null`,
|
|
779
|
+
{ encoding: 'utf-8', timeout: 3000 }
|
|
780
|
+
).trim()
|
|
781
|
+
if (output) {
|
|
782
|
+
const pids = output.split('\n').filter(Boolean).filter(pid => pid !== myPid)
|
|
783
|
+
for (const pid of pids) {
|
|
784
|
+
try {
|
|
785
|
+
execSyncImported(`kill ${pid} 2>/dev/null`)
|
|
786
|
+
killed = true
|
|
787
|
+
} catch {}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} catch {}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return killed
|
|
719
794
|
}
|
|
720
795
|
|
|
721
796
|
// ============================================
|
|
@@ -741,8 +816,8 @@ if (existingServer.running && !forceRestart) {
|
|
|
741
816
|
if (existingServer.running && forceRestart) {
|
|
742
817
|
console.log(yellow('Restarting...') + ' Stopping existing Haltija server.')
|
|
743
818
|
killOnPort(port)
|
|
744
|
-
//
|
|
745
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
819
|
+
// Pause to let port release and Electron processes fully exit
|
|
820
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
746
821
|
}
|
|
747
822
|
|
|
748
823
|
if (headlessMode) {
|
package/dist/client.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
3
7
|
var __export = (target, all) => {
|
|
4
8
|
for (var name in all)
|
|
5
9
|
__defProp(target, name, {
|
|
6
10
|
get: all[name],
|
|
7
11
|
enumerable: true,
|
|
8
12
|
configurable: true,
|
|
9
|
-
set: (
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
10
14
|
});
|
|
11
15
|
};
|
|
12
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
package/dist/component.js
CHANGED
|
@@ -3,27 +3,37 @@
|
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
7
9
|
var __toCommonJS = (from) => {
|
|
8
|
-
var entry = __moduleCache.get(from), desc;
|
|
10
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
9
11
|
if (entry)
|
|
10
12
|
return entry;
|
|
11
13
|
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
-
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (var key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(entry, key))
|
|
17
|
+
__defProp(entry, key, {
|
|
18
|
+
get: __accessProp.bind(from, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
17
22
|
__moduleCache.set(from, entry);
|
|
18
23
|
return entry;
|
|
19
24
|
};
|
|
25
|
+
var __moduleCache;
|
|
26
|
+
var __returnValue = (v) => v;
|
|
27
|
+
function __exportSetter(name, newValue) {
|
|
28
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
29
|
+
}
|
|
20
30
|
var __export = (target, all) => {
|
|
21
31
|
for (var name in all)
|
|
22
32
|
__defProp(target, name, {
|
|
23
33
|
get: all[name],
|
|
24
34
|
enumerable: true,
|
|
25
35
|
configurable: true,
|
|
26
|
-
set: (
|
|
36
|
+
set: __exportSetter.bind(all, name)
|
|
27
37
|
});
|
|
28
38
|
};
|
|
29
39
|
|
|
@@ -36,7 +46,7 @@
|
|
|
36
46
|
});
|
|
37
47
|
|
|
38
48
|
// src/version.ts
|
|
39
|
-
var VERSION = "1.
|
|
49
|
+
var VERSION = "1.2.2";
|
|
40
50
|
|
|
41
51
|
// src/text-selector.ts
|
|
42
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -545,6 +555,40 @@
|
|
|
545
555
|
const candidates = document.querySelectorAll(parsed.baseSelector);
|
|
546
556
|
return Array.from(candidates).filter((el) => elementTextMatches(el, parsed));
|
|
547
557
|
}
|
|
558
|
+
function resolveRefOrSelector(ref, selector) {
|
|
559
|
+
if (ref) {
|
|
560
|
+
const result2 = refRegistry.resolveWithStatus(ref);
|
|
561
|
+
if (result2.element) {
|
|
562
|
+
stats.refsResolved++;
|
|
563
|
+
return { element: result2.element, targetDesc: `@${ref}` };
|
|
564
|
+
}
|
|
565
|
+
stats.refsStale++;
|
|
566
|
+
const errorMsg = result2.status === "never_assigned" ? `Ref @${ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result2.status === "removed_from_dom" ? `Ref @${ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
567
|
+
return { element: null, targetDesc: `@${ref}`, error: errorMsg };
|
|
568
|
+
}
|
|
569
|
+
if (selector) {
|
|
570
|
+
const el = resolveSelector(selector);
|
|
571
|
+
return { element: el, targetDesc: selector };
|
|
572
|
+
}
|
|
573
|
+
return { element: null, targetDesc: "(none)", error: "ref or selector is required" };
|
|
574
|
+
}
|
|
575
|
+
function resolveRefOrSelectorAll(ref, selector) {
|
|
576
|
+
if (ref) {
|
|
577
|
+
const result2 = refRegistry.resolveWithStatus(ref);
|
|
578
|
+
if (result2.element) {
|
|
579
|
+
stats.refsResolved++;
|
|
580
|
+
return { elements: [result2.element], targetDesc: `@${ref}` };
|
|
581
|
+
}
|
|
582
|
+
stats.refsStale++;
|
|
583
|
+
const errorMsg = result2.status === "never_assigned" ? `Ref @${ref} was never assigned (highest ref is @${refRegistry.getStats().highWaterMark})` : result2.status === "removed_from_dom" ? `Ref @${ref} points to an element that was removed from the DOM (try refreshing /tree)` : `Ref @${ref} is stale - element was garbage collected (try refreshing /tree)`;
|
|
584
|
+
return { elements: [], targetDesc: `@${ref}`, error: errorMsg };
|
|
585
|
+
}
|
|
586
|
+
if (selector) {
|
|
587
|
+
const els = resolveSelectorAll(selector);
|
|
588
|
+
return { elements: els, targetDesc: selector };
|
|
589
|
+
}
|
|
590
|
+
return { elements: [], targetDesc: "(none)", error: "ref or selector is required" };
|
|
591
|
+
}
|
|
548
592
|
function extractElement(el) {
|
|
549
593
|
const rect = el.getBoundingClientRect();
|
|
550
594
|
const attrs = {};
|
|
@@ -1650,6 +1694,10 @@
|
|
|
1650
1694
|
recording = null;
|
|
1651
1695
|
testRecording = null;
|
|
1652
1696
|
originalConsole = {};
|
|
1697
|
+
originalDialogs = {};
|
|
1698
|
+
dialogPolicy = { alert: "dismiss", confirm: "accept", prompt: "dismiss", beforeunload: "allow" };
|
|
1699
|
+
dialogHistory = [];
|
|
1700
|
+
_inHaltijaUI = false;
|
|
1653
1701
|
widgetHidden = false;
|
|
1654
1702
|
serverUrl = "wss://localhost:8700/ws/browser";
|
|
1655
1703
|
windowId;
|
|
@@ -1835,12 +1883,14 @@
|
|
|
1835
1883
|
this.style.bottom = `${this.homeBottom}px`;
|
|
1836
1884
|
this.setupKeyboardShortcut();
|
|
1837
1885
|
this.interceptConsole();
|
|
1886
|
+
this.interceptDialogs();
|
|
1838
1887
|
this.connect();
|
|
1839
1888
|
}
|
|
1840
1889
|
disconnectedCallback() {
|
|
1841
1890
|
this.killed = true;
|
|
1842
1891
|
this.disconnect();
|
|
1843
1892
|
this.restoreConsole();
|
|
1893
|
+
this.restoreDialogs();
|
|
1844
1894
|
this.clearEventWatchers();
|
|
1845
1895
|
this.stopMutationWatch();
|
|
1846
1896
|
}
|
|
@@ -3994,6 +4044,16 @@ ${elementSummary}${moreText}`;
|
|
|
3994
4044
|
return `Interact with ${tag}`;
|
|
3995
4045
|
}
|
|
3996
4046
|
addTestAssertion() {
|
|
4047
|
+
if (!this.testRecording)
|
|
4048
|
+
return;
|
|
4049
|
+
this._inHaltijaUI = true;
|
|
4050
|
+
try {
|
|
4051
|
+
this._addTestAssertionImpl();
|
|
4052
|
+
} finally {
|
|
4053
|
+
this._inHaltijaUI = false;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
_addTestAssertionImpl() {
|
|
3997
4057
|
if (!this.testRecording)
|
|
3998
4058
|
return;
|
|
3999
4059
|
const type = prompt(`What to check?
|
|
@@ -4068,6 +4128,14 @@ ${elementSummary}${moreText}`;
|
|
|
4068
4128
|
}
|
|
4069
4129
|
}
|
|
4070
4130
|
saveTest() {
|
|
4131
|
+
this._inHaltijaUI = true;
|
|
4132
|
+
try {
|
|
4133
|
+
this._saveTestImpl();
|
|
4134
|
+
} finally {
|
|
4135
|
+
this._inHaltijaUI = false;
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
_saveTestImpl() {
|
|
4071
4139
|
if (!this.testRecording || this.testRecording.steps.length === 0) {
|
|
4072
4140
|
alert("No steps recorded!");
|
|
4073
4141
|
return;
|
|
@@ -5019,9 +5087,80 @@ ${elementSummary}${moreText}`;
|
|
|
5019
5087
|
case "interaction":
|
|
5020
5088
|
this.handleInteractionMessage(msg2);
|
|
5021
5089
|
break;
|
|
5090
|
+
case "dialog":
|
|
5091
|
+
this.handleDialogMessage(msg2);
|
|
5092
|
+
break;
|
|
5093
|
+
case "video":
|
|
5094
|
+
this.handleVideoMessage(msg2);
|
|
5095
|
+
break;
|
|
5022
5096
|
}
|
|
5023
5097
|
this.render();
|
|
5024
5098
|
}
|
|
5099
|
+
handleDialogMessage(msg2) {
|
|
5100
|
+
const { action: action2, payload: payload2 } = msg2;
|
|
5101
|
+
if (action2 === "configure") {
|
|
5102
|
+
if (payload2.alert !== undefined)
|
|
5103
|
+
this.dialogPolicy.alert = payload2.alert;
|
|
5104
|
+
if (payload2.confirm !== undefined)
|
|
5105
|
+
this.dialogPolicy.confirm = payload2.confirm;
|
|
5106
|
+
if (payload2.prompt !== undefined)
|
|
5107
|
+
this.dialogPolicy.prompt = payload2.prompt;
|
|
5108
|
+
if (payload2.beforeunload !== undefined)
|
|
5109
|
+
this.dialogPolicy.beforeunload = payload2.beforeunload;
|
|
5110
|
+
this.respond(msg2.id, true, { policy: this.dialogPolicy });
|
|
5111
|
+
} else if (action2 === "get-config") {
|
|
5112
|
+
this.respond(msg2.id, true, { policy: this.dialogPolicy });
|
|
5113
|
+
} else if (action2 === "history") {
|
|
5114
|
+
this.respond(msg2.id, true, { history: this.dialogHistory });
|
|
5115
|
+
} else {
|
|
5116
|
+
this.respond(msg2.id, false, undefined, `Unknown dialog action: ${action2}`);
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
handleVideoMessage(msg2) {
|
|
5120
|
+
const { action: action2, payload: payload2 } = msg2;
|
|
5121
|
+
const haltija = window.haltija;
|
|
5122
|
+
if (action2 === "start") {
|
|
5123
|
+
if (!haltija?.startVideoCapture) {
|
|
5124
|
+
this.respond(msg2.id, false, undefined, "Video capture requires the Haltija Desktop app. Run: npx haltija@latest -f");
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
haltija.startVideoCapture({ maxDuration: payload2.maxDuration }).then((result2) => {
|
|
5128
|
+
if (result2.success) {
|
|
5129
|
+
this.respond(msg2.id, true, { recordingId: result2.recordingId });
|
|
5130
|
+
} else {
|
|
5131
|
+
this.respond(msg2.id, false, undefined, result2.error || "Failed to start video");
|
|
5132
|
+
}
|
|
5133
|
+
}).catch((err) => {
|
|
5134
|
+
this.respond(msg2.id, false, undefined, `Failed to start video: ${err.message}`);
|
|
5135
|
+
});
|
|
5136
|
+
} else if (action2 === "stop") {
|
|
5137
|
+
if (!haltija?.stopVideoCapture) {
|
|
5138
|
+
this.respond(msg2.id, false, undefined, "Video capture requires the Haltija Desktop app. Run: npx haltija@latest -f");
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5141
|
+
haltija.stopVideoCapture().then((result2) => {
|
|
5142
|
+
if (result2.success) {
|
|
5143
|
+
this.respond(msg2.id, true, { path: result2.path, duration: result2.duration, size: result2.size, format: result2.format });
|
|
5144
|
+
} else {
|
|
5145
|
+
this.respond(msg2.id, false, undefined, result2.error || "Failed to stop video");
|
|
5146
|
+
}
|
|
5147
|
+
}).catch((err) => {
|
|
5148
|
+
this.respond(msg2.id, false, undefined, `Failed to stop video: ${err.message}`);
|
|
5149
|
+
});
|
|
5150
|
+
} else if (action2 === "status") {
|
|
5151
|
+
if (!haltija?.videoStatus) {
|
|
5152
|
+
this.respond(msg2.id, true, { recording: false });
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
5155
|
+
haltija.videoStatus().then((result2) => {
|
|
5156
|
+
this.respond(msg2.id, true, result2);
|
|
5157
|
+
}).catch(() => {
|
|
5158
|
+
this.respond(msg2.id, true, { recording: false });
|
|
5159
|
+
});
|
|
5160
|
+
} else {
|
|
5161
|
+
this.respond(msg2.id, false, undefined, `Unknown video action: ${action2}`);
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5025
5164
|
handleSemanticMessage(msg2) {
|
|
5026
5165
|
const { action: action2, payload: payload2 } = msg2;
|
|
5027
5166
|
if (action2 === "start" || action2 === "watch") {
|
|
@@ -5168,16 +5307,23 @@ ${elementSummary}${moreText}`;
|
|
|
5168
5307
|
if (action2 === "refresh") {
|
|
5169
5308
|
if (payload2.soft) {
|
|
5170
5309
|
location.reload();
|
|
5310
|
+
this.respond(msg2.id, true);
|
|
5171
5311
|
} else {
|
|
5172
|
-
|
|
5173
|
-
|
|
5312
|
+
const haltija = window.haltija;
|
|
5313
|
+
if (haltija?.hardRefresh) {
|
|
5314
|
+
haltija.hardRefresh().then(() => this.respond(msg2.id, true)).catch(() => {
|
|
5315
|
+
location.reload();
|
|
5316
|
+
this.respond(msg2.id, true);
|
|
5317
|
+
});
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5320
|
+
if (typeof caches !== "undefined") {
|
|
5321
|
+
caches.keys().then((names) => Promise.all(names.map((n) => caches.delete(n)))).then(() => location.reload()).catch(() => location.reload());
|
|
5174
5322
|
} else {
|
|
5175
|
-
|
|
5176
|
-
url.searchParams.set("_haltija_refresh", Date.now().toString());
|
|
5177
|
-
location.href = url.toString();
|
|
5323
|
+
location.reload();
|
|
5178
5324
|
}
|
|
5325
|
+
this.respond(msg2.id, true);
|
|
5179
5326
|
}
|
|
5180
|
-
this.respond(msg2.id, true);
|
|
5181
5327
|
} else if (action2 === "goto") {
|
|
5182
5328
|
const haltija = window.haltija;
|
|
5183
5329
|
if (haltija?.navigate) {
|
|
@@ -5238,20 +5384,32 @@ ${elementSummary}${moreText}`;
|
|
|
5238
5384
|
const req = payload2;
|
|
5239
5385
|
try {
|
|
5240
5386
|
if (req.all) {
|
|
5241
|
-
const elements =
|
|
5387
|
+
const { elements, error } = resolveRefOrSelectorAll(req.ref, req.selector);
|
|
5388
|
+
if (error && elements.length === 0) {
|
|
5389
|
+
this.respond(msg2.id, false, null, error);
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5242
5392
|
this.respond(msg2.id, true, elements.map(extractElement));
|
|
5243
5393
|
} else {
|
|
5244
|
-
const
|
|
5245
|
-
|
|
5394
|
+
const { element, targetDesc, error } = resolveRefOrSelector(req.ref, req.selector);
|
|
5395
|
+
if (error) {
|
|
5396
|
+
this.respond(msg2.id, false, null, error);
|
|
5397
|
+
return;
|
|
5398
|
+
}
|
|
5399
|
+
this.respond(msg2.id, true, element ? extractElement(element) : null);
|
|
5246
5400
|
}
|
|
5247
5401
|
} catch (err) {
|
|
5248
5402
|
this.respond(msg2.id, false, null, err.message);
|
|
5249
5403
|
}
|
|
5250
5404
|
} else if (action2 === "inspect") {
|
|
5251
5405
|
try {
|
|
5252
|
-
const el =
|
|
5406
|
+
const { element: el, targetDesc, error } = resolveRefOrSelector(payload2.ref, payload2.selector);
|
|
5407
|
+
if (error) {
|
|
5408
|
+
this.respond(msg2.id, false, null, error);
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5253
5411
|
if (!el) {
|
|
5254
|
-
this.respond(msg2.id, false, null, `Element not found: ${
|
|
5412
|
+
this.respond(msg2.id, false, null, `Element not found: ${targetDesc}`);
|
|
5255
5413
|
return;
|
|
5256
5414
|
}
|
|
5257
5415
|
const opts = {
|
|
@@ -5264,7 +5422,11 @@ ${elementSummary}${moreText}`;
|
|
|
5264
5422
|
}
|
|
5265
5423
|
} else if (action2 === "inspectAll") {
|
|
5266
5424
|
try {
|
|
5267
|
-
const elements =
|
|
5425
|
+
const { elements, error } = resolveRefOrSelectorAll(payload2.ref, payload2.selector);
|
|
5426
|
+
if (error && elements.length === 0) {
|
|
5427
|
+
this.respond(msg2.id, false, null, error);
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5268
5430
|
const opts = {
|
|
5269
5431
|
fullStyles: payload2.fullStyles,
|
|
5270
5432
|
matchedRules: payload2.matchedRules
|
|
@@ -5276,9 +5438,13 @@ ${elementSummary}${moreText}`;
|
|
|
5276
5438
|
}
|
|
5277
5439
|
} else if (action2 === "highlight") {
|
|
5278
5440
|
try {
|
|
5279
|
-
const el =
|
|
5441
|
+
const { element: el, targetDesc, error } = resolveRefOrSelector(payload2.ref, payload2.selector);
|
|
5442
|
+
if (error) {
|
|
5443
|
+
this.respond(msg2.id, false, null, error);
|
|
5444
|
+
return;
|
|
5445
|
+
}
|
|
5280
5446
|
if (!el) {
|
|
5281
|
-
this.respond(msg2.id, false, null, `Element not found: ${
|
|
5447
|
+
this.respond(msg2.id, false, null, `Element not found: ${targetDesc}`);
|
|
5282
5448
|
return;
|
|
5283
5449
|
}
|
|
5284
5450
|
if (payload2.duration) {
|
|
@@ -5286,7 +5452,7 @@ ${elementSummary}${moreText}`;
|
|
|
5286
5452
|
} else {
|
|
5287
5453
|
showHighlight(el, payload2.label, payload2.color);
|
|
5288
5454
|
}
|
|
5289
|
-
this.respond(msg2.id, true, { highlighted:
|
|
5455
|
+
this.respond(msg2.id, true, { highlighted: targetDesc });
|
|
5290
5456
|
} catch (err) {
|
|
5291
5457
|
this.respond(msg2.id, false, null, err.message);
|
|
5292
5458
|
}
|
|
@@ -5413,10 +5579,21 @@ ${elementSummary}${moreText}`;
|
|
|
5413
5579
|
img.src = dataUrl;
|
|
5414
5580
|
});
|
|
5415
5581
|
};
|
|
5582
|
+
let targetSelector = payload2?.selector;
|
|
5583
|
+
if (payload2?.ref) {
|
|
5584
|
+
const { element, targetDesc, error } = resolveRefOrSelector(payload2.ref, undefined);
|
|
5585
|
+
if (error) {
|
|
5586
|
+
this.respond(msg2.id, false, null, error);
|
|
5587
|
+
return;
|
|
5588
|
+
}
|
|
5589
|
+
if (element) {
|
|
5590
|
+
targetSelector = element.id ? `#${element.id}` : element.getAttribute("data-testid") ? `[data-testid="${element.getAttribute("data-testid")}"]` : undefined;
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5416
5593
|
const haltija = window.haltija;
|
|
5417
5594
|
if (haltija?.capturePage) {
|
|
5418
5595
|
try {
|
|
5419
|
-
const capturePromise =
|
|
5596
|
+
const capturePromise = targetSelector ? haltija.captureElement(targetSelector) : haltija.capturePage();
|
|
5420
5597
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ success: false, error: "Screenshot capture timed out after 10s" }), 1e4));
|
|
5421
5598
|
const result2 = await Promise.race([capturePromise, timeoutPromise]);
|
|
5422
5599
|
if (result2?.success && result2.data) {
|
|
@@ -5439,36 +5616,7 @@ ${elementSummary}${moreText}`;
|
|
|
5439
5616
|
return;
|
|
5440
5617
|
}
|
|
5441
5618
|
}
|
|
5442
|
-
|
|
5443
|
-
if (html2canvas) {
|
|
5444
|
-
const target = payload2?.selector ? resolveSelector(payload2.selector) : document.body;
|
|
5445
|
-
if (!target) {
|
|
5446
|
-
this.respond(msg2.id, false, null, `Element not found: ${payload2?.selector}`);
|
|
5447
|
-
return;
|
|
5448
|
-
}
|
|
5449
|
-
const canvas = await html2canvas(target, {
|
|
5450
|
-
useCORS: true,
|
|
5451
|
-
allowTaint: true,
|
|
5452
|
-
logging: false,
|
|
5453
|
-
scale: payload2?.scale || 1
|
|
5454
|
-
});
|
|
5455
|
-
const dataUrl = canvas.toDataURL(mimeType, quality);
|
|
5456
|
-
this.respond(msg2.id, true, {
|
|
5457
|
-
image: dataUrl,
|
|
5458
|
-
viewport,
|
|
5459
|
-
format,
|
|
5460
|
-
width: canvas.width,
|
|
5461
|
-
height: canvas.height,
|
|
5462
|
-
source: "html2canvas"
|
|
5463
|
-
});
|
|
5464
|
-
} else {
|
|
5465
|
-
this.respond(msg2.id, true, {
|
|
5466
|
-
viewport,
|
|
5467
|
-
image: null,
|
|
5468
|
-
note: "Screenshot capture requires the Haltija Desktop app.",
|
|
5469
|
-
source: "viewport-only"
|
|
5470
|
-
});
|
|
5471
|
-
}
|
|
5619
|
+
this.respond(msg2.id, false, null, "Screenshots require the Haltija Desktop app. Run: npx haltija@latest -f");
|
|
5472
5620
|
} catch (err) {
|
|
5473
5621
|
this.respond(msg2.id, false, null, err.message);
|
|
5474
5622
|
}
|
|
@@ -6936,6 +7084,67 @@ ${elementSummary}${moreText}`;
|
|
|
6936
7084
|
}
|
|
6937
7085
|
this.respond(responseId, true);
|
|
6938
7086
|
}
|
|
7087
|
+
interceptDialogs() {
|
|
7088
|
+
this.originalDialogs.alert = window.alert.bind(window);
|
|
7089
|
+
this.originalDialogs.confirm = window.confirm.bind(window);
|
|
7090
|
+
this.originalDialogs.prompt = window.prompt.bind(window);
|
|
7091
|
+
const self = this;
|
|
7092
|
+
window.alert = function(message) {
|
|
7093
|
+
if (self._inHaltijaUI)
|
|
7094
|
+
return self.originalDialogs.alert(message);
|
|
7095
|
+
const msg2 = String(message ?? "");
|
|
7096
|
+
const entry = { type: "alert", message: msg2, response: undefined, timestamp: Date.now() };
|
|
7097
|
+
self.dialogHistory.push(entry);
|
|
7098
|
+
if (self.dialogHistory.length > 50)
|
|
7099
|
+
self.dialogHistory = self.dialogHistory.slice(-25);
|
|
7100
|
+
self.send("dialog", "opened", { type: "alert", message: msg2, windowId: self.windowId });
|
|
7101
|
+
};
|
|
7102
|
+
window.confirm = function(message) {
|
|
7103
|
+
if (self._inHaltijaUI)
|
|
7104
|
+
return self.originalDialogs.confirm(message);
|
|
7105
|
+
const msg2 = String(message ?? "");
|
|
7106
|
+
const accept = self.dialogPolicy.confirm === "accept";
|
|
7107
|
+
const entry = { type: "confirm", message: msg2, response: accept, timestamp: Date.now() };
|
|
7108
|
+
self.dialogHistory.push(entry);
|
|
7109
|
+
if (self.dialogHistory.length > 50)
|
|
7110
|
+
self.dialogHistory = self.dialogHistory.slice(-25);
|
|
7111
|
+
self.send("dialog", "opened", {
|
|
7112
|
+
type: "confirm",
|
|
7113
|
+
message: msg2,
|
|
7114
|
+
response: accept,
|
|
7115
|
+
windowId: self.windowId
|
|
7116
|
+
});
|
|
7117
|
+
return accept;
|
|
7118
|
+
};
|
|
7119
|
+
window.prompt = function(message, defaultValue) {
|
|
7120
|
+
if (self._inHaltijaUI)
|
|
7121
|
+
return self.originalDialogs.prompt(message, defaultValue);
|
|
7122
|
+
const msg2 = String(message ?? "");
|
|
7123
|
+
const policy = self.dialogPolicy.prompt;
|
|
7124
|
+
const response = policy === "dismiss" ? null : policy.response;
|
|
7125
|
+
const entry = { type: "prompt", message: msg2, defaultValue, response, timestamp: Date.now() };
|
|
7126
|
+
self.dialogHistory.push(entry);
|
|
7127
|
+
if (self.dialogHistory.length > 50)
|
|
7128
|
+
self.dialogHistory = self.dialogHistory.slice(-25);
|
|
7129
|
+
self.send("dialog", "opened", {
|
|
7130
|
+
type: "prompt",
|
|
7131
|
+
message: msg2,
|
|
7132
|
+
defaultValue,
|
|
7133
|
+
response,
|
|
7134
|
+
windowId: self.windowId
|
|
7135
|
+
});
|
|
7136
|
+
return response;
|
|
7137
|
+
};
|
|
7138
|
+
}
|
|
7139
|
+
restoreDialogs() {
|
|
7140
|
+
if (this.originalDialogs.alert)
|
|
7141
|
+
window.alert = this.originalDialogs.alert;
|
|
7142
|
+
if (this.originalDialogs.confirm)
|
|
7143
|
+
window.confirm = this.originalDialogs.confirm;
|
|
7144
|
+
if (this.originalDialogs.prompt)
|
|
7145
|
+
window.prompt = this.originalDialogs.prompt;
|
|
7146
|
+
this.originalDialogs = {};
|
|
7147
|
+
}
|
|
6939
7148
|
interceptConsole() {
|
|
6940
7149
|
const levels = [
|
|
6941
7150
|
"log",
|
|
@@ -6994,6 +7203,7 @@ ${elementSummary}${moreText}`;
|
|
|
6994
7203
|
currentTagName = TAG_NAME;
|
|
6995
7204
|
window.__haltija_resolveSelector = resolveSelector;
|
|
6996
7205
|
window.__haltija_resolveSelectorAll = resolveSelectorAll;
|
|
7206
|
+
window.__haltija_refRegistry = refRegistry;
|
|
6997
7207
|
}
|
|
6998
7208
|
registerDevChannel();
|
|
6999
7209
|
var WIDGET_ID = "haltija-widget";
|