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.
@@ -413,18 +413,62 @@ function printBanner(mode, port) {
413
413
  console.log('')
414
414
  }
415
415
 
416
- /** Check if electron is available (cached by npx or installed) */
417
- function isElectronAvailable() {
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
- // Check if electron is in npx cache or node_modules
420
- execSyncImported('npx --no electron --version', {
421
- stdio: 'pipe',
422
- timeout: 5000
423
- })
424
- return true
425
- } catch {
426
- return false
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
- // Use npx --yes to auto-accept installation if needed (no interactive prompt)
442
- const child = spawn('npx', ['--yes', 'electron', desktopDir], {
443
- env: { ...env, DEV_CHANNEL_PORT: String(port) },
444
- stdio: 'inherit'
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
- return false
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
- // Brief pause to let port release
745
- await new Promise(resolve => setTimeout(resolve, 200))
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: (newValue) => all[name] = () => newValue
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
- var __moduleCache = /* @__PURE__ */ new WeakMap;
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
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
14
- get: () => from[key],
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
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: (newValue) => all[name] = () => newValue
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.1.21";
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
- if (location.protocol === "blob:" || location.protocol === "data:") {
5173
- location.reload();
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
- const url = new URL(location.href);
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 = resolveSelectorAll(req.selector);
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 el = resolveSelector(req.selector);
5245
- this.respond(msg2.id, true, el ? extractElement(el) : null);
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 = resolveSelector(payload2.selector);
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: ${payload2.selector}`);
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 = resolveSelectorAll(payload2.selector);
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 = resolveSelector(payload2.selector);
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: ${payload2.selector}`);
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: payload2.selector });
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 = payload2?.selector ? haltija.captureElement(payload2.selector) : haltija.capturePage();
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
- const html2canvas = window.html2canvas;
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";