haltija 1.12.0 → 1.12.1
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/CHANGELOG.md +91 -0
- package/apps/desktop/main.js +38 -19
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +26 -2
- package/bin/hj.mjs +52 -0
- package/bin/version.mjs +1 -1
- package/dist/component.d.ts +9 -1
- package/dist/component.esm.js +26 -2
- package/dist/component.js +26 -2
- package/dist/hj.js +25 -2
- package/dist/index.js +54 -8
- package/dist/server.js +54 -8
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.1
|
|
4
|
+
|
|
5
|
+
A patch of fixes reported by an agent driving a real React + web-components admin app against
|
|
6
|
+
1.12.0 — the kind of surface no fixture reproduces. Four of the five below are cases where haltija
|
|
7
|
+
answered confidently and wrongly, which is the same thread 1.12.0 was pulling on.
|
|
8
|
+
|
|
9
|
+
**Still open: [#26](https://github.com/tonioloewald/haltija/issues/26)** — tabs reportedly
|
|
10
|
+
disconnect permanently on webpack-dev-server (CRA) origins in 1.12.0, and rc.5 is unaffected. It is
|
|
11
|
+
**not fixed here**, because I could not reproduce it: a real webpack-dev-server (v5 client, `hot` +
|
|
12
|
+
`liveReload`) survived on both 1.12.0 and the rc.5 widget, the opposite of the reporter's A/B. The
|
|
13
|
+
re-injection fix below may cover it and may not. If you drive a CRA dev server, test before you rely
|
|
14
|
+
on this release, and please add to that issue.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Text selectors pick the element you could actually click — [#27](https://github.com/tonioloewald/haltija/issues/27)
|
|
18
|
+
|
|
19
|
+
Two ways the same selector chose the wrong element, both found driving a real admin app:
|
|
20
|
+
|
|
21
|
+
- **A hidden duplicate that came first won.** `click` took the first match in DOM order and left the
|
|
22
|
+
visibility gate to complain afterwards, so a `display:none` copy made `hj click ':text(Save
|
|
23
|
+
Changes)'` fail with "zero-size bounding rect" while the visible copy sat right there — and
|
|
24
|
+
`hj find`, which filters before choosing, returned the right one. One selector, two answers.
|
|
25
|
+
- **An off-canvas element was clicked silently.** The `position:absolute; left:-9999px` skip-link
|
|
26
|
+
idiom has a perfectly normal box (measured: 99x35 at x=-9999), so every size and style check
|
|
27
|
+
passed it. `click` actuated an element no human can see and reported **success** — a script then
|
|
28
|
+
asserts against a state it never produced. This is the worse of the two: it fails confidently.
|
|
29
|
+
|
|
30
|
+
Resolution now filters *before* choosing, and `find` and `click` share one predicate so they cannot
|
|
31
|
+
disagree. An off-canvas element that is the ONLY match fails loudly (`positioned off-canvas`) rather
|
|
32
|
+
than being clicked invisibly.
|
|
33
|
+
|
|
34
|
+
Off-canvas is measured in **page** coordinates, so content merely **below the fold** is unaffected —
|
|
35
|
+
visible still means *rendered*, not *on screen*. That is also why `elementFromPoint` isn't used
|
|
36
|
+
here despite catching this case: it rejects everything scrolled out of view, which would fail
|
|
37
|
+
legitimate content in a small headless viewport.
|
|
38
|
+
|
|
39
|
+
### `hj doctor` probes requestAnimationFrame — [#28](https://github.com/tonioloewald/haltija/issues/28)
|
|
40
|
+
|
|
41
|
+
**A tab can report `visibilityState: "visible"` and still not be compositing.** Occluded windows,
|
|
42
|
+
offscreen windows and a sleeping display all do it. Nothing rAF-driven then renders — React's
|
|
43
|
+
scheduler, tosijs `queueRender`, animations, virtual scrollers — while geometry probes keep
|
|
44
|
+
returning real numbers, so the absence of an element stops being evidence of anything.
|
|
45
|
+
|
|
46
|
+
That doesn't merely hide information, it manufactures a plausible wrong answer. The reporter found
|
|
47
|
+
four routes "not mounting" on hard navigation, had a coherent mechanism (the router gates its first
|
|
48
|
+
mount on rAF), reproduced it four times, and nearly filed it as an application bug. Opening a second
|
|
49
|
+
tab fixed all four.
|
|
50
|
+
|
|
51
|
+
`hj doctor` now measures it directly and fails with `requestAnimationFrame DID NOT FIRE within 2s`.
|
|
52
|
+
If the probe itself can't run, that is reported as **unchecked** — never as a pass — and the probe
|
|
53
|
+
is bounded at 3s so a pre-flight can't hang on a socket that never answers.
|
|
54
|
+
|
|
55
|
+
### `hj tabs` — the array is `windows`, and a `tabs` alias now exists ([#29](https://github.com/tonioloewald/haltija/issues/29))
|
|
56
|
+
|
|
57
|
+
`d['tabs']` KeyError'd because the payload key is `windows` — accurate, since the list holds popups
|
|
58
|
+
and iframes too, but the command is `hj tabs`, so the obvious guess failed and it looked like the
|
|
59
|
+
caller's bug. `tabs` is now sent as an alias of the same array. The hint no longer mixes the two
|
|
60
|
+
vocabularies in one sentence ("Multiple **tabs** connected. Use `?window=<id>`"), which is where the
|
|
61
|
+
wrong idea came from.
|
|
62
|
+
|
|
63
|
+
Popups being indistinguishable from user-opened tabs, the other half of that report, is fixed by the
|
|
64
|
+
popup work below: they carry `windowType: "popup"` and never take focus.
|
|
65
|
+
|
|
66
|
+
### Popups are popups again (desktop app) — [#25](https://github.com/tonioloewald/haltija/issues/25)
|
|
67
|
+
|
|
68
|
+
A page calling `window.open(url, name, 'width=...')` now gets a genuine popup: `window.open()`
|
|
69
|
+
returns a real `WindowProxy`, the child has `window.opener`, and `opener.postMessage(...)` reaches
|
|
70
|
+
the parent.
|
|
71
|
+
|
|
72
|
+
Previously the desktop app decided by **guessing from the URL** — allow if it contained `oauth`,
|
|
73
|
+
`signin`, `login`, `accounts.google.com` or `/__/auth/`; deny everything else and re-open it as a
|
|
74
|
+
tab, which severs the opener in both directions. `window.open()` returned `null` and the child had
|
|
75
|
+
no `opener`. That is the shape of every OAuth popup flow: the SDK keeps the returned window to poll
|
|
76
|
+
`.closed` and to `.close()`, and the callback page delivers its credential via `opener.postMessage`.
|
|
77
|
+
With neither, a user can complete a sign-in in a window that cannot report back and the app just
|
|
78
|
+
waits — arguably worse than a clean block.
|
|
79
|
+
|
|
80
|
+
The heuristic failed both ways: an innocent `/login-help` page became a popup, while the common SDK
|
|
81
|
+
pattern of opening `about:blank` and *then* navigating matched nothing and was denied — the very
|
|
82
|
+
case the list existed to catch. The decision now keys on Electron's `disposition`, which is what the
|
|
83
|
+
page actually asked for, so there is nothing to guess.
|
|
84
|
+
|
|
85
|
+
Two things come free, because the window model already handled popups correctly: the popup registers
|
|
86
|
+
as `windowType: "popup"` (tellable from a user-opened tab) and it **does not steal focus**, so
|
|
87
|
+
untargeted commands keep going to the tab you were driving.
|
|
88
|
+
|
|
89
|
+
Ordinary `<a target="_blank">` links still open as tabs in the app's tab strip — that behaviour is
|
|
90
|
+
deliberate and unchanged. **Known residual:** a featureless `window.open(url, '_blank')` is
|
|
91
|
+
indistinguishable from a `target="_blank"` link at this layer (both report `foreground-tab`), so it
|
|
92
|
+
still becomes a tab and still returns `null`.
|
|
93
|
+
|
|
3
94
|
## 1.12.0 — trustworthy by default
|
|
4
95
|
|
|
5
96
|
**Trustworthy by default.** A minor, gated on the nine-lens pre-release review: every finding it
|
package/apps/desktop/main.js
CHANGED
|
@@ -690,28 +690,47 @@ function setupWebContentsInjection(wc) {
|
|
|
690
690
|
|
|
691
691
|
console.log('[Haltija Desktop] Monitoring webContents:', wc.id, wc.getType())
|
|
692
692
|
|
|
693
|
-
// Intercept window.open()
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
693
|
+
// Intercept window.open(): a genuine POPUP stays a popup; an ordinary new-window link becomes a
|
|
694
|
+
// tab in our own tab strip.
|
|
695
|
+
//
|
|
696
|
+
// This used to decide by GUESSING FROM THE URL — allow if it contained `oauth`, `signin`,
|
|
697
|
+
// `login`, `accounts.google.com`, `/__/auth/`. Everything else was denied and re-opened as a tab,
|
|
698
|
+
// which severs the opener relationship in both directions: `window.open()` returns **null** and
|
|
699
|
+
// the child has no `window.opener`. That is the shape of every OAuth popup flow (issue #25):
|
|
700
|
+
// an SDK keeps the returned WindowProxy to poll `.closed` and to `.close()`, and the callback
|
|
701
|
+
// page delivers its credential via `opener.postMessage(...)`. With neither, the user can complete
|
|
702
|
+
// a sign-in in a window that cannot report back — worse than a clean block, because the app just
|
|
703
|
+
// waits.
|
|
704
|
+
//
|
|
705
|
+
// The heuristic failed in both directions: an innocent `/login-help` page became a popup, while
|
|
706
|
+
// the common SDK pattern of opening `about:blank` and *then* navigating matched nothing and was
|
|
707
|
+
// denied — the case the list was written to catch.
|
|
708
|
+
//
|
|
709
|
+
// `disposition` is what the page actually asked for, so there is nothing to guess (verified
|
|
710
|
+
// against Electron):
|
|
711
|
+
// window.open(url, name, 'width=420,height=320') -> 'new-window' features: "width=..."
|
|
712
|
+
// window.open(url, '_blank') -> 'foreground-tab' features: ""
|
|
713
|
+
// <a target="_blank"> -> 'foreground-tab' features: ""
|
|
714
|
+
//
|
|
715
|
+
// Allowing a real popup also fixes two things for free, because the window model already handles
|
|
716
|
+
// popups correctly: it registers as `windowType: "popup"` (so scripts can tell it from a
|
|
717
|
+
// user-opened tab) and it does NOT steal focus, since only real tabs become the untargeted
|
|
718
|
+
// command target.
|
|
719
|
+
//
|
|
720
|
+
// KNOWN RESIDUAL: a featureless `window.open(url, '_blank')` is indistinguishable from a
|
|
721
|
+
// `target="_blank"` link at this layer — both report 'foreground-tab' — so it still becomes a tab
|
|
722
|
+
// and still returns null. Preserving the tab UX for ordinary links is worth that; if an SDK turns
|
|
723
|
+
// up that opens featureless popups and needs the opener, this is the line to revisit.
|
|
724
|
+
wc.setWindowOpenHandler((details) => {
|
|
725
|
+
const { url, disposition } = details
|
|
726
|
+
console.log('[Haltija Desktop] Intercepted window.open:', url, `(disposition: ${disposition})`)
|
|
727
|
+
|
|
728
|
+
if (disposition === 'new-window') {
|
|
729
|
+
console.log('[Haltija Desktop] Genuine popup — preserving the opener relationship:', url)
|
|
711
730
|
return { action: 'allow' }
|
|
712
731
|
}
|
|
713
732
|
|
|
714
|
-
//
|
|
733
|
+
// Ordinary new-window link: open as a tab instead.
|
|
715
734
|
if (mainWindow && mainWindow.webContents) {
|
|
716
735
|
mainWindow.webContents.send('open-url-in-tab', url)
|
|
717
736
|
}
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.12.
|
|
49
|
+
var VERSION = "1.12.1";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -624,6 +624,19 @@
|
|
|
624
624
|
const candidates = queryAllDeep(parsed.baseSelector).filter((el) => !NON_RENDERED_TEXT.has(el.tagName) && elementTextMatches(el, parsed));
|
|
625
625
|
return candidates.filter((el) => !candidates.some((other) => other !== el && containsDeep(el, other)));
|
|
626
626
|
}
|
|
627
|
+
function isOffCanvas(el) {
|
|
628
|
+
const r = el.getBoundingClientRect();
|
|
629
|
+
return r.right + window.scrollX <= 0 || r.bottom + window.scrollY <= 0;
|
|
630
|
+
}
|
|
631
|
+
function isActionable(el) {
|
|
632
|
+
const cs = getComputedStyle(el);
|
|
633
|
+
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0")
|
|
634
|
+
return false;
|
|
635
|
+
const r = el.getBoundingClientRect();
|
|
636
|
+
if (r.width <= 0 || r.height <= 0)
|
|
637
|
+
return false;
|
|
638
|
+
return !isOffCanvas(el);
|
|
639
|
+
}
|
|
627
640
|
function resolveSelector(selector) {
|
|
628
641
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
629
642
|
return document.querySelector(selector) || queryAllDeep(selector)[0] || null;
|
|
@@ -631,7 +644,8 @@
|
|
|
631
644
|
const parsed = parseTextSelector(selector);
|
|
632
645
|
if (!parsed)
|
|
633
646
|
return document.querySelector(selector);
|
|
634
|
-
|
|
647
|
+
const matches = textMatchesInDocument(parsed);
|
|
648
|
+
return matches.find(isActionable) || matches[0] || null;
|
|
635
649
|
}
|
|
636
650
|
function resolveSelectorAll(selector) {
|
|
637
651
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
@@ -5027,6 +5041,9 @@ ${elementSummary}${moreText}`;
|
|
|
5027
5041
|
}
|
|
5028
5042
|
this.render();
|
|
5029
5043
|
}
|
|
5044
|
+
get isDefunct() {
|
|
5045
|
+
return this.killed;
|
|
5046
|
+
}
|
|
5030
5047
|
kill() {
|
|
5031
5048
|
this.killed = true;
|
|
5032
5049
|
hideHighlight();
|
|
@@ -7555,6 +7572,9 @@ ${elementSummary}${moreText}`;
|
|
|
7555
7572
|
};
|
|
7556
7573
|
}
|
|
7557
7574
|
getHiddenReason(el) {
|
|
7575
|
+
if (isOffCanvas(el)) {
|
|
7576
|
+
return "positioned off-canvas (e.g. left:-9999px) — rendered but not reachable by a user";
|
|
7577
|
+
}
|
|
7558
7578
|
const rect = el.getBoundingClientRect();
|
|
7559
7579
|
if (rect.width === 0 && rect.height === 0) {
|
|
7560
7580
|
return "zero-size bounding rect (element not rendered or in hidden container)";
|
|
@@ -8711,6 +8731,7 @@ ${elementSummary}${moreText}`;
|
|
|
8711
8731
|
currentTagName = TAG_NAME;
|
|
8712
8732
|
window.__haltija_resolveSelector = resolveSelector;
|
|
8713
8733
|
window.__haltija_resolveSelectorAll = resolveSelectorAll;
|
|
8734
|
+
window.__haltija_isActionable = isActionable;
|
|
8714
8735
|
window.__haltija_refRegistry = refRegistry;
|
|
8715
8736
|
}
|
|
8716
8737
|
registerDevChannel();
|
|
@@ -8723,6 +8744,9 @@ ${elementSummary}${moreText}`;
|
|
|
8723
8744
|
if (existingVersion !== VERSION2) {
|
|
8724
8745
|
console.log(`${LOG_PREFIX} Version mismatch (${existingVersion} -> ${VERSION2}), replacing`);
|
|
8725
8746
|
existing.remove();
|
|
8747
|
+
} else if (existing.isDefunct) {
|
|
8748
|
+
console.log(`${LOG_PREFIX} Existing widget is defunct (killed), replacing`);
|
|
8749
|
+
existing.remove();
|
|
8726
8750
|
} else {
|
|
8727
8751
|
return existing;
|
|
8728
8752
|
}
|
package/bin/hj.mjs
CHANGED
|
@@ -357,6 +357,58 @@ async function runDoctor(port, portSource, portSourceKind, jsonOutput) {
|
|
|
357
357
|
if (status.serverVersion && differsBeyondPatch(HJ_VERSION, status.serverVersion)) {
|
|
358
358
|
notes.push(`hj ${HJ_VERSION} is driving server ${status.serverVersion} (version skew)`)
|
|
359
359
|
}
|
|
360
|
+
|
|
361
|
+
// Does this tab actually PAINT? `visibilityState` answers "is this tab selected", not "is this
|
|
362
|
+
// tab being composited", and the two diverge for occluded windows, offscreen windows and a
|
|
363
|
+
// sleeping display. A starved tab renders nothing while reporting `visible`, geometry probes
|
|
364
|
+
// still return real numbers, and the absence of an element stops being evidence of anything.
|
|
365
|
+
//
|
|
366
|
+
// That is worse than a missing feature: it invites a plausible code-level explanation. An agent
|
|
367
|
+
// driving a React app found four routes "not mounting" on hard navigation, had a coherent
|
|
368
|
+
// mechanism (the router gates its first mount on rAF), reproduced it four times, and nearly
|
|
369
|
+
// filed it as an application bug. Opening a second tab fixed all four (#28). This check is here
|
|
370
|
+
// to convert that silent, confidently-wrong outcome into a visible one.
|
|
371
|
+
if (ready && tabs.length) {
|
|
372
|
+
const probe =
|
|
373
|
+
`new Promise(r => { const s = Date.now();` +
|
|
374
|
+
` const t = setTimeout(() => r({ fired: false, ms: Date.now() - s }), 2000);` +
|
|
375
|
+
` requestAnimationFrame(() => { clearTimeout(t); r({ fired: true, ms: Date.now() - s }) }) })`
|
|
376
|
+
let raf = null
|
|
377
|
+
try {
|
|
378
|
+
// BOUND IT. The probe resolves in ~2s at the latest when a browser is there to answer, so a
|
|
379
|
+
// longer wait means nothing is coming — and doctor is a pre-flight: a lane runs it to find
|
|
380
|
+
// out quickly, not to sit through another component's timeout. Without this, a connected
|
|
381
|
+
// socket that never answers `/eval` (a widget mid-teardown, or a test harness holding an
|
|
382
|
+
// open WebSocket) made `hj doctor` block for the server's full browser timeout.
|
|
383
|
+
const cancel = AbortSignal.timeout(3000)
|
|
384
|
+
const r = await fetch(`http://localhost:${port}/eval`, {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { 'X-Haltija-Token': token } : {}) },
|
|
387
|
+
body: JSON.stringify({ code: probe }),
|
|
388
|
+
signal: cancel,
|
|
389
|
+
})
|
|
390
|
+
if (r.ok) {
|
|
391
|
+
const j = await r.json()
|
|
392
|
+
if (j && j.success && j.data && typeof j.data.fired === 'boolean') raf = j.data
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Fall through to the unchecked branch — never a pass.
|
|
396
|
+
}
|
|
397
|
+
if (raf === null) {
|
|
398
|
+
unchecked.push(
|
|
399
|
+
`could not run the requestAnimationFrame probe — whether this tab actually paints is ` +
|
|
400
|
+
`UNKNOWN. If elements seem missing, suspect a non-compositing tab before the page.`,
|
|
401
|
+
)
|
|
402
|
+
} else if (!raf.fired) {
|
|
403
|
+
problems.push(
|
|
404
|
+
`requestAnimationFrame DID NOT FIRE within 2s — this tab is not compositing, even though ` +
|
|
405
|
+
`it reports visibilityState "visible". Anything rAF-driven (React's scheduler, tosijs ` +
|
|
406
|
+
`queueRender, animations, virtual scrollers) will never render, so a missing element ` +
|
|
407
|
+
`is NOT evidence of an application bug. Bring a window to the front, or wake the ` +
|
|
408
|
+
`display, and re-run.`,
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
360
412
|
// Declared origins decide WHICH tab answers. A broken declaration silently disables the
|
|
361
413
|
// routing it configures, and doctor is where a CI lane finds out.
|
|
362
414
|
const origins = describeOrigins(tabs)
|
package/bin/version.mjs
CHANGED
package/dist/component.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - Option+Tab toggles visibility (but active state always shows briefly)
|
|
21
21
|
* - Localhost only by default
|
|
22
22
|
*/
|
|
23
|
-
export declare const VERSION = "1.12.
|
|
23
|
+
export declare const VERSION = "1.12.1";
|
|
24
24
|
export declare class DevChannel extends HTMLElement {
|
|
25
25
|
static get tagName(): string;
|
|
26
26
|
static elementCreator(): () => DevChannel;
|
|
@@ -195,6 +195,14 @@ export declare class DevChannel extends HTMLElement {
|
|
|
195
195
|
private stopScreenCapture;
|
|
196
196
|
private toggleMinimize;
|
|
197
197
|
private togglePause;
|
|
198
|
+
/**
|
|
199
|
+
* Can this widget never talk to the server again?
|
|
200
|
+
*
|
|
201
|
+
* `killed` is set when the element is removed from the DOM or `kill()` runs, and it permanently
|
|
202
|
+
* stops the 3-second reconnect loop. An ordinary disconnected socket is NOT defunct — that retries
|
|
203
|
+
* forever and heals itself, so callers must not treat it as dead.
|
|
204
|
+
*/
|
|
205
|
+
get isDefunct(): boolean;
|
|
198
206
|
private kill;
|
|
199
207
|
private testRecordingHandler;
|
|
200
208
|
private toggleTestRecording;
|
package/dist/component.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var VERSION = "1.12.
|
|
2
|
+
var VERSION = "1.12.1";
|
|
3
3
|
|
|
4
4
|
// src/text-selector.ts
|
|
5
5
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -577,6 +577,19 @@ function textMatchesInDocument(parsed) {
|
|
|
577
577
|
const candidates = queryAllDeep(parsed.baseSelector).filter((el) => !NON_RENDERED_TEXT.has(el.tagName) && elementTextMatches(el, parsed));
|
|
578
578
|
return candidates.filter((el) => !candidates.some((other) => other !== el && containsDeep(el, other)));
|
|
579
579
|
}
|
|
580
|
+
function isOffCanvas(el) {
|
|
581
|
+
const r = el.getBoundingClientRect();
|
|
582
|
+
return r.right + window.scrollX <= 0 || r.bottom + window.scrollY <= 0;
|
|
583
|
+
}
|
|
584
|
+
function isActionable(el) {
|
|
585
|
+
const cs = getComputedStyle(el);
|
|
586
|
+
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0")
|
|
587
|
+
return false;
|
|
588
|
+
const r = el.getBoundingClientRect();
|
|
589
|
+
if (r.width <= 0 || r.height <= 0)
|
|
590
|
+
return false;
|
|
591
|
+
return !isOffCanvas(el);
|
|
592
|
+
}
|
|
580
593
|
function resolveSelector(selector) {
|
|
581
594
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
582
595
|
return document.querySelector(selector) || queryAllDeep(selector)[0] || null;
|
|
@@ -584,7 +597,8 @@ function resolveSelector(selector) {
|
|
|
584
597
|
const parsed = parseTextSelector(selector);
|
|
585
598
|
if (!parsed)
|
|
586
599
|
return document.querySelector(selector);
|
|
587
|
-
|
|
600
|
+
const matches = textMatchesInDocument(parsed);
|
|
601
|
+
return matches.find(isActionable) || matches[0] || null;
|
|
588
602
|
}
|
|
589
603
|
function resolveSelectorAll(selector) {
|
|
590
604
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
@@ -4980,6 +4994,9 @@ ${elementSummary}${moreText}`;
|
|
|
4980
4994
|
}
|
|
4981
4995
|
this.render();
|
|
4982
4996
|
}
|
|
4997
|
+
get isDefunct() {
|
|
4998
|
+
return this.killed;
|
|
4999
|
+
}
|
|
4983
5000
|
kill() {
|
|
4984
5001
|
this.killed = true;
|
|
4985
5002
|
hideHighlight();
|
|
@@ -7508,6 +7525,9 @@ ${elementSummary}${moreText}`;
|
|
|
7508
7525
|
};
|
|
7509
7526
|
}
|
|
7510
7527
|
getHiddenReason(el) {
|
|
7528
|
+
if (isOffCanvas(el)) {
|
|
7529
|
+
return "positioned off-canvas (e.g. left:-9999px) — rendered but not reachable by a user";
|
|
7530
|
+
}
|
|
7511
7531
|
const rect = el.getBoundingClientRect();
|
|
7512
7532
|
if (rect.width === 0 && rect.height === 0) {
|
|
7513
7533
|
return "zero-size bounding rect (element not rendered or in hidden container)";
|
|
@@ -8664,6 +8684,7 @@ function registerDevChannel() {
|
|
|
8664
8684
|
currentTagName = TAG_NAME;
|
|
8665
8685
|
window.__haltija_resolveSelector = resolveSelector;
|
|
8666
8686
|
window.__haltija_resolveSelectorAll = resolveSelectorAll;
|
|
8687
|
+
window.__haltija_isActionable = isActionable;
|
|
8667
8688
|
window.__haltija_refRegistry = refRegistry;
|
|
8668
8689
|
}
|
|
8669
8690
|
registerDevChannel();
|
|
@@ -8676,6 +8697,9 @@ function inject(serverUrl2 = "wss://localhost:8700/ws/browser", options) {
|
|
|
8676
8697
|
if (existingVersion !== VERSION2) {
|
|
8677
8698
|
console.log(`${LOG_PREFIX} Version mismatch (${existingVersion} -> ${VERSION2}), replacing`);
|
|
8678
8699
|
existing.remove();
|
|
8700
|
+
} else if (existing.isDefunct) {
|
|
8701
|
+
console.log(`${LOG_PREFIX} Existing widget is defunct (killed), replacing`);
|
|
8702
|
+
existing.remove();
|
|
8679
8703
|
} else {
|
|
8680
8704
|
return existing;
|
|
8681
8705
|
}
|
package/dist/component.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.12.
|
|
49
|
+
var VERSION = "1.12.1";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -624,6 +624,19 @@
|
|
|
624
624
|
const candidates = queryAllDeep(parsed.baseSelector).filter((el) => !NON_RENDERED_TEXT.has(el.tagName) && elementTextMatches(el, parsed));
|
|
625
625
|
return candidates.filter((el) => !candidates.some((other) => other !== el && containsDeep(el, other)));
|
|
626
626
|
}
|
|
627
|
+
function isOffCanvas(el) {
|
|
628
|
+
const r = el.getBoundingClientRect();
|
|
629
|
+
return r.right + window.scrollX <= 0 || r.bottom + window.scrollY <= 0;
|
|
630
|
+
}
|
|
631
|
+
function isActionable(el) {
|
|
632
|
+
const cs = getComputedStyle(el);
|
|
633
|
+
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0")
|
|
634
|
+
return false;
|
|
635
|
+
const r = el.getBoundingClientRect();
|
|
636
|
+
if (r.width <= 0 || r.height <= 0)
|
|
637
|
+
return false;
|
|
638
|
+
return !isOffCanvas(el);
|
|
639
|
+
}
|
|
627
640
|
function resolveSelector(selector) {
|
|
628
641
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
629
642
|
return document.querySelector(selector) || queryAllDeep(selector)[0] || null;
|
|
@@ -631,7 +644,8 @@
|
|
|
631
644
|
const parsed = parseTextSelector(selector);
|
|
632
645
|
if (!parsed)
|
|
633
646
|
return document.querySelector(selector);
|
|
634
|
-
|
|
647
|
+
const matches = textMatchesInDocument(parsed);
|
|
648
|
+
return matches.find(isActionable) || matches[0] || null;
|
|
635
649
|
}
|
|
636
650
|
function resolveSelectorAll(selector) {
|
|
637
651
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
@@ -5027,6 +5041,9 @@ ${elementSummary}${moreText}`;
|
|
|
5027
5041
|
}
|
|
5028
5042
|
this.render();
|
|
5029
5043
|
}
|
|
5044
|
+
get isDefunct() {
|
|
5045
|
+
return this.killed;
|
|
5046
|
+
}
|
|
5030
5047
|
kill() {
|
|
5031
5048
|
this.killed = true;
|
|
5032
5049
|
hideHighlight();
|
|
@@ -7555,6 +7572,9 @@ ${elementSummary}${moreText}`;
|
|
|
7555
7572
|
};
|
|
7556
7573
|
}
|
|
7557
7574
|
getHiddenReason(el) {
|
|
7575
|
+
if (isOffCanvas(el)) {
|
|
7576
|
+
return "positioned off-canvas (e.g. left:-9999px) — rendered but not reachable by a user";
|
|
7577
|
+
}
|
|
7558
7578
|
const rect = el.getBoundingClientRect();
|
|
7559
7579
|
if (rect.width === 0 && rect.height === 0) {
|
|
7560
7580
|
return "zero-size bounding rect (element not rendered or in hidden container)";
|
|
@@ -8711,6 +8731,7 @@ ${elementSummary}${moreText}`;
|
|
|
8711
8731
|
currentTagName = TAG_NAME;
|
|
8712
8732
|
window.__haltija_resolveSelector = resolveSelector;
|
|
8713
8733
|
window.__haltija_resolveSelectorAll = resolveSelectorAll;
|
|
8734
|
+
window.__haltija_isActionable = isActionable;
|
|
8714
8735
|
window.__haltija_refRegistry = refRegistry;
|
|
8715
8736
|
}
|
|
8716
8737
|
registerDevChannel();
|
|
@@ -8723,6 +8744,9 @@ ${elementSummary}${moreText}`;
|
|
|
8723
8744
|
if (existingVersion !== VERSION2) {
|
|
8724
8745
|
console.log(`${LOG_PREFIX} Version mismatch (${existingVersion} -> ${VERSION2}), replacing`);
|
|
8725
8746
|
existing.remove();
|
|
8747
|
+
} else if (existing.isDefunct) {
|
|
8748
|
+
console.log(`${LOG_PREFIX} Existing widget is defunct (killed), replacing`);
|
|
8749
|
+
existing.remove();
|
|
8726
8750
|
} else {
|
|
8727
8751
|
return existing;
|
|
8728
8752
|
}
|
package/dist/hj.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// haltija-cli:do-not-edit v1.12.
|
|
2
|
+
// haltija-cli:do-not-edit v1.12.1
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
5
|
|
|
@@ -756,7 +756,7 @@ function substituteGeneratedVars(text, seed) {
|
|
|
756
756
|
}
|
|
757
757
|
|
|
758
758
|
// bin/version.mjs
|
|
759
|
-
var HJ_VERSION = "1.12.
|
|
759
|
+
var HJ_VERSION = "1.12.1";
|
|
760
760
|
|
|
761
761
|
// bin/semver.mjs
|
|
762
762
|
function parseVersion(v) {
|
|
@@ -2676,6 +2676,29 @@ async function runDoctor(port, portSource, portSourceKind, jsonOutput) {
|
|
|
2676
2676
|
if (status.serverVersion && differsBeyondPatch(HJ_VERSION, status.serverVersion)) {
|
|
2677
2677
|
notes.push(`hj ${HJ_VERSION} is driving server ${status.serverVersion} (version skew)`);
|
|
2678
2678
|
}
|
|
2679
|
+
if (ready && tabs.length) {
|
|
2680
|
+
const probe = `new Promise(r => { const s = Date.now();` + ` const t = setTimeout(() => r({ fired: false, ms: Date.now() - s }), 2000);` + ` requestAnimationFrame(() => { clearTimeout(t); r({ fired: true, ms: Date.now() - s }) }) })`;
|
|
2681
|
+
let raf = null;
|
|
2682
|
+
try {
|
|
2683
|
+
const cancel = AbortSignal.timeout(3000);
|
|
2684
|
+
const r = await fetch(`http://localhost:${port}/eval`, {
|
|
2685
|
+
method: "POST",
|
|
2686
|
+
headers: { "Content-Type": "application/json", ...token ? { "X-Haltija-Token": token } : {} },
|
|
2687
|
+
body: JSON.stringify({ code: probe }),
|
|
2688
|
+
signal: cancel
|
|
2689
|
+
});
|
|
2690
|
+
if (r.ok) {
|
|
2691
|
+
const j = await r.json();
|
|
2692
|
+
if (j && j.success && j.data && typeof j.data.fired === "boolean")
|
|
2693
|
+
raf = j.data;
|
|
2694
|
+
}
|
|
2695
|
+
} catch {}
|
|
2696
|
+
if (raf === null) {
|
|
2697
|
+
unchecked.push(`could not run the requestAnimationFrame probe — whether this tab actually paints is ` + `UNKNOWN. If elements seem missing, suspect a non-compositing tab before the page.`);
|
|
2698
|
+
} else if (!raf.fired) {
|
|
2699
|
+
problems.push(`requestAnimationFrame DID NOT FIRE within 2s — this tab is not compositing, even though ` + `it reports visibilityState "visible". Anything rAF-driven (React's scheduler, tosijs ` + `queueRender, animations, virtual scrollers) will never render, so a missing element ` + `is NOT evidence of an application bug. Bring a window to the front, or wake the ` + `display, and re-run.`);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2679
2702
|
const origins = describeOrigins(tabs);
|
|
2680
2703
|
if (origins.problem)
|
|
2681
2704
|
problems.push(origins.problem);
|
package/dist/index.js
CHANGED
|
@@ -674,7 +674,7 @@ var injectorCode = `
|
|
|
674
674
|
`;
|
|
675
675
|
|
|
676
676
|
// src/version.ts
|
|
677
|
-
var VERSION = "1.12.
|
|
677
|
+
var VERSION = "1.12.1";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -2251,7 +2251,15 @@ Deprecated: Use POST /select {"action":"clear"} instead.
|
|
|
2251
2251
|
|
|
2252
2252
|
Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
2253
2253
|
|
|
2254
|
-
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], count, ready, hint }
|
|
2254
|
+
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], tabs, count, ready, hint }
|
|
2255
|
+
|
|
2256
|
+
The array is **\`windows\`**, not \`tabs\` \u2014 it includes popups (\`windowType: "popup"\`) and
|
|
2257
|
+
iframes (\`"iframe"\`), not only tabs. Because the CLI command is \`hj tabs\`, \`d['tabs']\` is the
|
|
2258
|
+
natural first guess and used to KeyError, so **\`tabs\` is also sent as an alias** of the same
|
|
2259
|
+
array. Prefer \`windows\`; \`tabs\` exists so the obvious guess works.
|
|
2260
|
+
|
|
2261
|
+
Only real tabs can be the target of an UNTARGETED command \u2014 a popup or iframe never becomes
|
|
2262
|
+
\`focused\`, so a page opening a popup cannot silently re-route your commands.
|
|
2255
2263
|
|
|
2256
2264
|
\`active\` and \`hidden\` are exact inverses, and BOTH are sent on purpose: /status historically
|
|
2257
2265
|
emitted only \`hidden\` and /windows only \`active\`, so code moving between the two silently
|
|
@@ -3441,7 +3449,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3441
3449
|
});
|
|
3442
3450
|
|
|
3443
3451
|
// src/version.ts
|
|
3444
|
-
var VERSION = "1.12.
|
|
3452
|
+
var VERSION = "1.12.1";
|
|
3445
3453
|
|
|
3446
3454
|
// src/text-selector.ts
|
|
3447
3455
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -4019,6 +4027,19 @@ var COMPONENT_JS = `(() => {
|
|
|
4019
4027
|
const candidates = queryAllDeep(parsed.baseSelector).filter((el) => !NON_RENDERED_TEXT.has(el.tagName) && elementTextMatches(el, parsed));
|
|
4020
4028
|
return candidates.filter((el) => !candidates.some((other) => other !== el && containsDeep(el, other)));
|
|
4021
4029
|
}
|
|
4030
|
+
function isOffCanvas(el) {
|
|
4031
|
+
const r = el.getBoundingClientRect();
|
|
4032
|
+
return r.right + window.scrollX <= 0 || r.bottom + window.scrollY <= 0;
|
|
4033
|
+
}
|
|
4034
|
+
function isActionable(el) {
|
|
4035
|
+
const cs = getComputedStyle(el);
|
|
4036
|
+
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0")
|
|
4037
|
+
return false;
|
|
4038
|
+
const r = el.getBoundingClientRect();
|
|
4039
|
+
if (r.width <= 0 || r.height <= 0)
|
|
4040
|
+
return false;
|
|
4041
|
+
return !isOffCanvas(el);
|
|
4042
|
+
}
|
|
4022
4043
|
function resolveSelector(selector) {
|
|
4023
4044
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
4024
4045
|
return document.querySelector(selector) || queryAllDeep(selector)[0] || null;
|
|
@@ -4026,7 +4047,8 @@ var COMPONENT_JS = `(() => {
|
|
|
4026
4047
|
const parsed = parseTextSelector(selector);
|
|
4027
4048
|
if (!parsed)
|
|
4028
4049
|
return document.querySelector(selector);
|
|
4029
|
-
|
|
4050
|
+
const matches = textMatchesInDocument(parsed);
|
|
4051
|
+
return matches.find(isActionable) || matches[0] || null;
|
|
4030
4052
|
}
|
|
4031
4053
|
function resolveSelectorAll(selector) {
|
|
4032
4054
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
@@ -8422,6 +8444,9 @@ var COMPONENT_JS = `(() => {
|
|
|
8422
8444
|
}
|
|
8423
8445
|
this.render();
|
|
8424
8446
|
}
|
|
8447
|
+
get isDefunct() {
|
|
8448
|
+
return this.killed;
|
|
8449
|
+
}
|
|
8425
8450
|
kill() {
|
|
8426
8451
|
this.killed = true;
|
|
8427
8452
|
hideHighlight();
|
|
@@ -10950,6 +10975,9 @@ var COMPONENT_JS = `(() => {
|
|
|
10950
10975
|
};
|
|
10951
10976
|
}
|
|
10952
10977
|
getHiddenReason(el) {
|
|
10978
|
+
if (isOffCanvas(el)) {
|
|
10979
|
+
return "positioned off-canvas (e.g. left:-9999px) \u2014 rendered but not reachable by a user";
|
|
10980
|
+
}
|
|
10953
10981
|
const rect = el.getBoundingClientRect();
|
|
10954
10982
|
if (rect.width === 0 && rect.height === 0) {
|
|
10955
10983
|
return "zero-size bounding rect (element not rendered or in hidden container)";
|
|
@@ -12106,6 +12134,7 @@ var COMPONENT_JS = `(() => {
|
|
|
12106
12134
|
currentTagName = TAG_NAME;
|
|
12107
12135
|
window.__haltija_resolveSelector = resolveSelector;
|
|
12108
12136
|
window.__haltija_resolveSelectorAll = resolveSelectorAll;
|
|
12137
|
+
window.__haltija_isActionable = isActionable;
|
|
12109
12138
|
window.__haltija_refRegistry = refRegistry;
|
|
12110
12139
|
}
|
|
12111
12140
|
registerDevChannel();
|
|
@@ -12118,6 +12147,9 @@ var COMPONENT_JS = `(() => {
|
|
|
12118
12147
|
if (existingVersion !== VERSION2) {
|
|
12119
12148
|
console.log(\`\${LOG_PREFIX} Version mismatch (\${existingVersion} -> \${VERSION2}), replacing\`);
|
|
12120
12149
|
existing.remove();
|
|
12150
|
+
} else if (existing.isDefunct) {
|
|
12151
|
+
console.log(\`\${LOG_PREFIX} Existing widget is defunct (killed), replacing\`);
|
|
12152
|
+
existing.remove();
|
|
12121
12153
|
} else {
|
|
12122
12154
|
return existing;
|
|
12123
12155
|
}
|
|
@@ -14547,7 +14579,15 @@ var windows = endpoint({
|
|
|
14547
14579
|
summary: "List connected windows",
|
|
14548
14580
|
description: `Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
14549
14581
|
|
|
14550
|
-
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], count, ready, hint }
|
|
14582
|
+
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], tabs, count, ready, hint }
|
|
14583
|
+
|
|
14584
|
+
The array is **\`windows\`**, not \`tabs\` \u2014 it includes popups (\`windowType: "popup"\`) and
|
|
14585
|
+
iframes (\`"iframe"\`), not only tabs. Because the CLI command is \`hj tabs\`, \`d['tabs']\` is the
|
|
14586
|
+
natural first guess and used to KeyError, so **\`tabs\` is also sent as an alias** of the same
|
|
14587
|
+
array. Prefer \`windows\`; \`tabs\` exists so the obvious guess works.
|
|
14588
|
+
|
|
14589
|
+
Only real tabs can be the target of an UNTARGETED command \u2014 a popup or iframe never becomes
|
|
14590
|
+
\`focused\`, so a page opening a popup cannot silently re-route your commands.
|
|
14551
14591
|
|
|
14552
14592
|
\`active\` and \`hidden\` are exact inverses, and BOTH are sent on purpose: /status historically
|
|
14553
14593
|
emitted only \`hidden\` and /windows only \`active\`, so code moving between the two silently
|
|
@@ -16578,12 +16618,17 @@ registerHandler(find, async (body, ctx) => {
|
|
|
16578
16618
|
// "Rendered", matching assert visible/hidden \u2014 NOT \`offsetParent === null\`, which is null for
|
|
16579
16619
|
// \`html\`, \`body\` and EVERY \`position: fixed\` element. Fixed shells, modals and sticky chrome
|
|
16580
16620
|
// are perfectly visible and were all being skipped.
|
|
16581
|
-
|
|
16621
|
+
//
|
|
16622
|
+
// Prefers the WIDGET'S OWN predicate so \`find\` and \`click\` cannot disagree about which element
|
|
16623
|
+
// a text selector means \u2014 they did, and it also let \`find\` return an element parked off-canvas
|
|
16624
|
+
// at \`left:-9999px\` (#27). The inline copy below is only a fallback for a widget too old to
|
|
16625
|
+
// export it; keeping two rules in permanent use is what caused #24 in the first place.
|
|
16626
|
+
const isVisible = window.__haltija_isActionable || ((el) => {
|
|
16582
16627
|
const cs = getComputedStyle(el);
|
|
16583
16628
|
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
|
16584
16629
|
const r = el.getBoundingClientRect();
|
|
16585
16630
|
return r.width > 0 && r.height > 0;
|
|
16586
|
-
};
|
|
16631
|
+
});
|
|
16587
16632
|
|
|
16588
16633
|
const textOf = (el) => (el.textContent || '').trim();
|
|
16589
16634
|
const hits = elements.filter((el) => {
|
|
@@ -21014,9 +21059,10 @@ ${messageText}`;
|
|
|
21014
21059
|
label: w.label
|
|
21015
21060
|
}));
|
|
21016
21061
|
const ready = isDrivable(windowList);
|
|
21017
|
-
const hint = windowList.length > 1 ? "Multiple
|
|
21062
|
+
const hint = windowList.length > 1 ? "Multiple windows connected. Use ?window=<id> to target a specific one (e.g., /tree?window=abc123). The array is `windows` \u2014 it includes popups and iframes, not only tabs." : windowList.length === 1 ? "One window connected. Commands automatically target it." : "No windows connected. Inject the widget into a browser tab.";
|
|
21018
21063
|
return Response.json({
|
|
21019
21064
|
windows: windowList,
|
|
21065
|
+
tabs: windowList,
|
|
21020
21066
|
focused: focusedWindowId,
|
|
21021
21067
|
count: windowList.length,
|
|
21022
21068
|
ready,
|
package/dist/server.js
CHANGED
|
@@ -674,7 +674,7 @@ var injectorCode = `
|
|
|
674
674
|
`;
|
|
675
675
|
|
|
676
676
|
// src/version.ts
|
|
677
|
-
var VERSION = "1.12.
|
|
677
|
+
var VERSION = "1.12.1";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -2251,7 +2251,15 @@ Deprecated: Use POST /select {"action":"clear"} instead.
|
|
|
2251
2251
|
|
|
2252
2252
|
Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
2253
2253
|
|
|
2254
|
-
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], count, ready, hint }
|
|
2254
|
+
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], tabs, count, ready, hint }
|
|
2255
|
+
|
|
2256
|
+
The array is **\`windows\`**, not \`tabs\` \u2014 it includes popups (\`windowType: "popup"\`) and
|
|
2257
|
+
iframes (\`"iframe"\`), not only tabs. Because the CLI command is \`hj tabs\`, \`d['tabs']\` is the
|
|
2258
|
+
natural first guess and used to KeyError, so **\`tabs\` is also sent as an alias** of the same
|
|
2259
|
+
array. Prefer \`windows\`; \`tabs\` exists so the obvious guess works.
|
|
2260
|
+
|
|
2261
|
+
Only real tabs can be the target of an UNTARGETED command \u2014 a popup or iframe never becomes
|
|
2262
|
+
\`focused\`, so a page opening a popup cannot silently re-route your commands.
|
|
2255
2263
|
|
|
2256
2264
|
\`active\` and \`hidden\` are exact inverses, and BOTH are sent on purpose: /status historically
|
|
2257
2265
|
emitted only \`hidden\` and /windows only \`active\`, so code moving between the two silently
|
|
@@ -3441,7 +3449,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3441
3449
|
});
|
|
3442
3450
|
|
|
3443
3451
|
// src/version.ts
|
|
3444
|
-
var VERSION = "1.12.
|
|
3452
|
+
var VERSION = "1.12.1";
|
|
3445
3453
|
|
|
3446
3454
|
// src/text-selector.ts
|
|
3447
3455
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -4019,6 +4027,19 @@ var COMPONENT_JS = `(() => {
|
|
|
4019
4027
|
const candidates = queryAllDeep(parsed.baseSelector).filter((el) => !NON_RENDERED_TEXT.has(el.tagName) && elementTextMatches(el, parsed));
|
|
4020
4028
|
return candidates.filter((el) => !candidates.some((other) => other !== el && containsDeep(el, other)));
|
|
4021
4029
|
}
|
|
4030
|
+
function isOffCanvas(el) {
|
|
4031
|
+
const r = el.getBoundingClientRect();
|
|
4032
|
+
return r.right + window.scrollX <= 0 || r.bottom + window.scrollY <= 0;
|
|
4033
|
+
}
|
|
4034
|
+
function isActionable(el) {
|
|
4035
|
+
const cs = getComputedStyle(el);
|
|
4036
|
+
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0")
|
|
4037
|
+
return false;
|
|
4038
|
+
const r = el.getBoundingClientRect();
|
|
4039
|
+
if (r.width <= 0 || r.height <= 0)
|
|
4040
|
+
return false;
|
|
4041
|
+
return !isOffCanvas(el);
|
|
4042
|
+
}
|
|
4022
4043
|
function resolveSelector(selector) {
|
|
4023
4044
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
4024
4045
|
return document.querySelector(selector) || queryAllDeep(selector)[0] || null;
|
|
@@ -4026,7 +4047,8 @@ var COMPONENT_JS = `(() => {
|
|
|
4026
4047
|
const parsed = parseTextSelector(selector);
|
|
4027
4048
|
if (!parsed)
|
|
4028
4049
|
return document.querySelector(selector);
|
|
4029
|
-
|
|
4050
|
+
const matches = textMatchesInDocument(parsed);
|
|
4051
|
+
return matches.find(isActionable) || matches[0] || null;
|
|
4030
4052
|
}
|
|
4031
4053
|
function resolveSelectorAll(selector) {
|
|
4032
4054
|
if (!TEXT_PSEUDO_RE.test(selector)) {
|
|
@@ -8422,6 +8444,9 @@ var COMPONENT_JS = `(() => {
|
|
|
8422
8444
|
}
|
|
8423
8445
|
this.render();
|
|
8424
8446
|
}
|
|
8447
|
+
get isDefunct() {
|
|
8448
|
+
return this.killed;
|
|
8449
|
+
}
|
|
8425
8450
|
kill() {
|
|
8426
8451
|
this.killed = true;
|
|
8427
8452
|
hideHighlight();
|
|
@@ -10950,6 +10975,9 @@ var COMPONENT_JS = `(() => {
|
|
|
10950
10975
|
};
|
|
10951
10976
|
}
|
|
10952
10977
|
getHiddenReason(el) {
|
|
10978
|
+
if (isOffCanvas(el)) {
|
|
10979
|
+
return "positioned off-canvas (e.g. left:-9999px) \u2014 rendered but not reachable by a user";
|
|
10980
|
+
}
|
|
10953
10981
|
const rect = el.getBoundingClientRect();
|
|
10954
10982
|
if (rect.width === 0 && rect.height === 0) {
|
|
10955
10983
|
return "zero-size bounding rect (element not rendered or in hidden container)";
|
|
@@ -12106,6 +12134,7 @@ var COMPONENT_JS = `(() => {
|
|
|
12106
12134
|
currentTagName = TAG_NAME;
|
|
12107
12135
|
window.__haltija_resolveSelector = resolveSelector;
|
|
12108
12136
|
window.__haltija_resolveSelectorAll = resolveSelectorAll;
|
|
12137
|
+
window.__haltija_isActionable = isActionable;
|
|
12109
12138
|
window.__haltija_refRegistry = refRegistry;
|
|
12110
12139
|
}
|
|
12111
12140
|
registerDevChannel();
|
|
@@ -12118,6 +12147,9 @@ var COMPONENT_JS = `(() => {
|
|
|
12118
12147
|
if (existingVersion !== VERSION2) {
|
|
12119
12148
|
console.log(\`\${LOG_PREFIX} Version mismatch (\${existingVersion} -> \${VERSION2}), replacing\`);
|
|
12120
12149
|
existing.remove();
|
|
12150
|
+
} else if (existing.isDefunct) {
|
|
12151
|
+
console.log(\`\${LOG_PREFIX} Existing widget is defunct (killed), replacing\`);
|
|
12152
|
+
existing.remove();
|
|
12121
12153
|
} else {
|
|
12122
12154
|
return existing;
|
|
12123
12155
|
}
|
|
@@ -14547,7 +14579,15 @@ var windows = endpoint({
|
|
|
14547
14579
|
summary: "List connected windows",
|
|
14548
14580
|
description: `Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
14549
14581
|
|
|
14550
|
-
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], count, ready, hint }
|
|
14582
|
+
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], tabs, count, ready, hint }
|
|
14583
|
+
|
|
14584
|
+
The array is **\`windows\`**, not \`tabs\` \u2014 it includes popups (\`windowType: "popup"\`) and
|
|
14585
|
+
iframes (\`"iframe"\`), not only tabs. Because the CLI command is \`hj tabs\`, \`d['tabs']\` is the
|
|
14586
|
+
natural first guess and used to KeyError, so **\`tabs\` is also sent as an alias** of the same
|
|
14587
|
+
array. Prefer \`windows\`; \`tabs\` exists so the obvious guess works.
|
|
14588
|
+
|
|
14589
|
+
Only real tabs can be the target of an UNTARGETED command \u2014 a popup or iframe never becomes
|
|
14590
|
+
\`focused\`, so a page opening a popup cannot silently re-route your commands.
|
|
14551
14591
|
|
|
14552
14592
|
\`active\` and \`hidden\` are exact inverses, and BOTH are sent on purpose: /status historically
|
|
14553
14593
|
emitted only \`hidden\` and /windows only \`active\`, so code moving between the two silently
|
|
@@ -16578,12 +16618,17 @@ registerHandler(find, async (body, ctx) => {
|
|
|
16578
16618
|
// "Rendered", matching assert visible/hidden \u2014 NOT \`offsetParent === null\`, which is null for
|
|
16579
16619
|
// \`html\`, \`body\` and EVERY \`position: fixed\` element. Fixed shells, modals and sticky chrome
|
|
16580
16620
|
// are perfectly visible and were all being skipped.
|
|
16581
|
-
|
|
16621
|
+
//
|
|
16622
|
+
// Prefers the WIDGET'S OWN predicate so \`find\` and \`click\` cannot disagree about which element
|
|
16623
|
+
// a text selector means \u2014 they did, and it also let \`find\` return an element parked off-canvas
|
|
16624
|
+
// at \`left:-9999px\` (#27). The inline copy below is only a fallback for a widget too old to
|
|
16625
|
+
// export it; keeping two rules in permanent use is what caused #24 in the first place.
|
|
16626
|
+
const isVisible = window.__haltija_isActionable || ((el) => {
|
|
16582
16627
|
const cs = getComputedStyle(el);
|
|
16583
16628
|
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
|
16584
16629
|
const r = el.getBoundingClientRect();
|
|
16585
16630
|
return r.width > 0 && r.height > 0;
|
|
16586
|
-
};
|
|
16631
|
+
});
|
|
16587
16632
|
|
|
16588
16633
|
const textOf = (el) => (el.textContent || '').trim();
|
|
16589
16634
|
const hits = elements.filter((el) => {
|
|
@@ -21014,9 +21059,10 @@ ${messageText}`;
|
|
|
21014
21059
|
label: w.label
|
|
21015
21060
|
}));
|
|
21016
21061
|
const ready = isDrivable(windowList);
|
|
21017
|
-
const hint = windowList.length > 1 ? "Multiple
|
|
21062
|
+
const hint = windowList.length > 1 ? "Multiple windows connected. Use ?window=<id> to target a specific one (e.g., /tree?window=abc123). The array is `windows` \u2014 it includes popups and iframes, not only tabs." : windowList.length === 1 ? "One window connected. Commands automatically target it." : "No windows connected. Inject the widget into a browser tab.";
|
|
21018
21063
|
return Response.json({
|
|
21019
21064
|
windows: windowList,
|
|
21065
|
+
tabs: windowList,
|
|
21020
21066
|
focused: focusedWindowId,
|
|
21021
21067
|
count: windowList.length,
|
|
21022
21068
|
ready,
|
package/dist/version.d.ts
CHANGED