@ia-qa/self-healing 1.7.11 → 1.7.13

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.
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.explorePage = explorePage;
4
+ exports.mergeExplored = mergeExplored;
5
+ const aom_1 = require("./aom");
6
+ const openables_1 = require("./browser/openables");
7
+ const match_1 = require("./browser/match");
8
+ const union_1 = require("./browser/union");
9
+ const SETTLE_MS = 250;
10
+ /** Contract identity plus selector: two elements differing only by selector are two rows. */
11
+ function key(e) {
12
+ return `${e.role}|${e.name}|${e.selector}`;
13
+ }
14
+ function fingerprint(elements) {
15
+ return elements.map(key).sort().join('§');
16
+ }
17
+ /**
18
+ * Walk one page's reachable states, breadth-first.
19
+ *
20
+ * Determinism matters more here than anywhere else in this package: a contract whose contents
21
+ * depend on the order a walk happened to take is not comparable to the next one, and the whole
22
+ * point is comparing it. `findOpenables` returns candidates in DOM order, the queue is FIFO,
23
+ * and the union is order-stable — so two runs over an unchanged app produce the same file.
24
+ */
25
+ async function explorePage(page, opts) {
26
+ const depth = opts.depth ?? 1;
27
+ const budget = opts.budget ?? 150;
28
+ let blockedMutations = 0;
29
+ // The guard is installed on the page, not the context: the context is shared by every page
30
+ // in a `map` run, and a route left behind would silently apply to captures that never asked
31
+ // for exploration.
32
+ await page.route('**/*', (route) => {
33
+ if (route.request().method() === 'GET')
34
+ return route.continue();
35
+ blockedMutations++;
36
+ return route.abort();
37
+ });
38
+ let clicks = 0;
39
+ let reloads = 0;
40
+ let exhausted = false;
41
+ const revealed = [];
42
+ try {
43
+ const base = await (0, aom_1.extractInteractiveElements)(page);
44
+ const baseFp = fingerprint(base);
45
+ const known = new Set(base.map(key));
46
+ const seenStates = new Set([baseFp]);
47
+ // A path is a list of candidate indices, replayed from the loaded state. Its `trail` is
48
+ // the same hops named — carried here rather than reconstructed, because only the state
49
+ // that explored a hop knows what that hop was called.
50
+ const queue = [{ path: [], trail: [] }];
51
+ /** Re-reach a state from scratch. Only called when Escape failed to restore. */
52
+ const replay = async (path) => {
53
+ reloads++;
54
+ await opts.restore();
55
+ for (const idx of path) {
56
+ const list = (await page.evaluate(openables_1.findOpenables));
57
+ if (idx >= list.length)
58
+ return false;
59
+ try {
60
+ await page.locator(list[idx].handle).first().click({ timeout: 2000 });
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ await page.waitForTimeout(SETTLE_MS);
66
+ }
67
+ return true;
68
+ };
69
+ while (queue.length > 0 && clicks < budget) {
70
+ const { path, trail } = queue.shift();
71
+ if (path.length >= depth)
72
+ continue;
73
+ if (path.length > 0 && !(await replay(path)))
74
+ continue;
75
+ // Snapshot the state we are exploring *from*, so a click that changes nothing can be
76
+ // detected without paying for a reload.
77
+ let stateFp = fingerprint(await (0, aom_1.extractInteractiveElements)(page));
78
+ const candidates = (await page.evaluate(openables_1.findOpenables));
79
+ for (let i = 0; i < candidates.length; i++) {
80
+ // Checked here rather than in the loop head so that running out mid-state is
81
+ // recorded: those remaining candidates are unexplored surface, and the outer
82
+ // queue does not know about them.
83
+ if (clicks >= budget) {
84
+ exhausted = true;
85
+ break;
86
+ }
87
+ // Re-stamp before every click. The stamps are DOM attributes, and a framework that
88
+ // re-renders on interaction throws them away — so after any cheap restore (Escape, a
89
+ // re-click) the handles from the start of this state point at nothing, every click
90
+ // fails, and the failures are indistinguishable from "this control does nothing". That
91
+ // cost 54 of 138 revealed elements the first time it was measured, silently. One
92
+ // `evaluate` per click is nothing next to the reload it prevents.
93
+ const live = (await page.evaluate(openables_1.findOpenables));
94
+ // Find the control we meant to click *by name*, not by position.
95
+ //
96
+ // This is what makes the walk affordable. Indices only mean something in the state
97
+ // that issued them, so an index-keyed walk must reload after every click that moves
98
+ // anything — and a reload is ~2 s, which was 3.8 of the 5.5 minutes a single page
99
+ // cost. But most controls survive their own state change: click one tab and all the
100
+ // tabs are still there, open one accordion and its siblings have not moved. Looking
101
+ // the control up by name lets the walk carry on in the shifted state and pay nothing.
102
+ //
103
+ // It is not a loosening: we click the control we intended, identified by the same
104
+ // accessible name the contract identifies elements by. The index was only ever a way
105
+ // to say "that one".
106
+ let target = live[i]?.name === candidates[i].name ? live[i] : undefined;
107
+ if (!target)
108
+ target = live.find((o) => o.name === candidates[i].name);
109
+ if (!target) {
110
+ // Genuinely not on screen any more — this is where a reload is the honest answer.
111
+ if (!(await replay(path)))
112
+ break;
113
+ const re = (await page.evaluate(openables_1.findOpenables));
114
+ target = re.find((o) => o.name === candidates[i].name);
115
+ if (!target)
116
+ continue;
117
+ }
118
+ const urlBefore = page.url();
119
+ clicks++;
120
+ try {
121
+ await page.locator(target.handle).first().click({ timeout: 2000 });
122
+ }
123
+ catch {
124
+ // Covered, detached or moved between the stamp and the click. Nothing changed.
125
+ continue;
126
+ }
127
+ // A click that navigated was not a disclosure: that page has a contract of its own,
128
+ // and folding its elements into this one would name them after the wrong page.
129
+ if (page.url() !== urlBefore) {
130
+ if (!(await replay(path)))
131
+ break;
132
+ const re = (await page.evaluate(openables_1.findOpenables));
133
+ if (re.length !== candidates.length)
134
+ break;
135
+ continue;
136
+ }
137
+ await page.waitForTimeout(SETTLE_MS);
138
+ const after = await (0, aom_1.extractInteractiveElements)(page);
139
+ const afterFp = fingerprint(after);
140
+ // Nothing moved. The state is provably unchanged, so the next candidate can be
141
+ // clicked without restoring anything — this is what makes the walk affordable.
142
+ if (afterFp === stateFp)
143
+ continue;
144
+ const fresh = after.filter((e) => !known.has(key(e)));
145
+ if (fresh.length > 0 && !seenStates.has(afterFp)) {
146
+ seenStates.add(afterFp);
147
+ const here = [...trail, candidates[i].name];
148
+ for (const e of fresh) {
149
+ known.add(key(e));
150
+ revealed.push({ ...e, via: here });
151
+ }
152
+ if (path.length + 1 < depth)
153
+ queue.push({ path: [...path, i], trail: here });
154
+ }
155
+ // The state changed, so it has to be restored before the next candidate. Two cheap
156
+ // attempts before the expensive one, because the reload is what makes a deep walk cost
157
+ // hours: Escape closes most dialogs and popovers, and clicking the same control again
158
+ // closes most of the rest — a disclosure toggle is, by definition, a toggle. Each
159
+ // attempt is *verified* against the fingerprint rather than assumed, so a control that
160
+ // does not toggle cleanly falls through to the reload instead of quietly leaving the
161
+ // walk in a state it thinks it is not in.
162
+ let back = false;
163
+ for (const attempt of ['escape', 'reclick']) {
164
+ if (attempt === 'escape') {
165
+ await page.keyboard.press('Escape').catch(() => undefined);
166
+ }
167
+ else {
168
+ await page.locator(target.handle).first().click({ timeout: 1000 }).catch(() => undefined);
169
+ }
170
+ await page.waitForTimeout(80);
171
+ if (fingerprint(await (0, aom_1.extractInteractiveElements)(page)) === stateFp) {
172
+ back = true;
173
+ break;
174
+ }
175
+ }
176
+ if (!back) {
177
+ // Neither cheap restore worked — a tab that stays switched, a wizard that advanced.
178
+ // The old code reloaded here, and that single line was most of the runtime.
179
+ //
180
+ // It is no longer needed: the next candidate is found by *name*, so it does not
181
+ // matter which state we are standing in, only that the control is reachable. So we
182
+ // adopt the state instead of paying to leave it. The walk becomes a walk rather
183
+ // than a star, which is the honest description — and the cost is that a `via` trail
184
+ // names the last hop rather than the full route from load. `via` is documented as
185
+ // never part of identity precisely because it is an aid to a human, not a verdict.
186
+ //
187
+ // A reload is still spent where it is the only answer: a click that navigated, or a
188
+ // control that has genuinely left the screen.
189
+ stateFp = fingerprint(await (0, aom_1.extractInteractiveElements)(page));
190
+ }
191
+ }
192
+ }
193
+ // Truncation is "the budget stopped us", not "we spent the budget": a walk that used its
194
+ // last click on the last candidate of the last state explored everything there was. Both
195
+ // ways of running out count — mid-state (candidates left on the bench) and mid-queue
196
+ // (whole states never reached).
197
+ const truncated = exhausted || (clicks >= budget && queue.length > 0);
198
+ return { revealed, states: seenStates.size - 1, clicks, blockedMutations, reloads, truncated };
199
+ }
200
+ finally {
201
+ await page.unroute('**/*').catch(() => undefined);
202
+ }
203
+ }
204
+ function mergeExplored(base, revealed) {
205
+ const inBase = new Set(base.map((e) => `${e.role}|${(0, match_1.normalize)(e.name)}`));
206
+ const selectors = new Set(base.map((e) => e.selector));
207
+ const fresh = [];
208
+ const taken = new Set();
209
+ let collided = 0;
210
+ for (const e of revealed) {
211
+ const id = `${e.role}|${(0, match_1.normalize)(e.name)}`;
212
+ if (inBase.has(id) || taken.has(id))
213
+ continue;
214
+ if (selectors.has(e.selector)) {
215
+ collided++;
216
+ continue;
217
+ }
218
+ taken.add(id);
219
+ selectors.add(e.selector);
220
+ fresh.push(e);
221
+ }
222
+ return { elements: (0, union_1.unionElements)(base, fresh), collided };
223
+ }
224
+ //# sourceMappingURL=explore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explore.js","sourceRoot":"","sources":["../src/explore.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ia-qa/self-healing",
3
- "version": "1.7.11",
3
+ "version": "1.7.13",
4
4
  "description": "Your Playwright, Cypress or Selenium tests break when a selector moves — this finds the element again and rewrites the test. Deterministic: no LLM decides whether your build passes. Runs entirely on your machine, with a local MCP server for agents.",
5
5
  "keywords": [
6
6
  "self-healing",
@@ -54,6 +54,7 @@ exists only in a state nothing captured (behind a tab, a modal, a mode toggle).
54
54
  | "their suite takes 20 min and `run` runs it twice" | `run --no-verify` — the second run is the post-fix verification, and it is optional. Say what they lose: the exit code then reflects the diff, not a re-proved green suite |
55
55
  | "here is my failing test / red build — why?" | `explain --junit <file>` (or `--message "<error text>"`, or pipe the log). Start here when the user hands you a failure rather than a question: it names the locator, judges it against the two captures, and a **PASS rules drift out** so you stop hunting selectors. It re-runs nothing and edits nothing |
56
56
  | "day one, no baseline — is my suite still valid?" | `audit` |
57
+ | "the contract misses everything behind my menus / tabs / modals" | `map --deep`. A plain `map` records one state of a page — what is visible when it loads. `--deep` also opens menus, tabs, dialogs and accordions and puts what they reveal in the same contract, each element carrying the click path (`via`) that reaches it. Every non-GET request is blocked while it walks, so a click cannot mutate anything server-side. **A deep baseline may only be diffed against a deep capture** (§4.13) |
57
58
  | "is the app I mapped even sound?" | `check` (dead links · unnamed elements · name collisions · orphan pages) |
58
59
  | "which pages am I *not* covering?" | `discover` (`--sitemap` / `--crawl`; suggests only). Behind a login `--sitemap` sees nothing — `--crawl` is the one that works, and it reuses the session |
59
60
  | "give me the app's structure" | `graph --format mermaid\|svg\|json\|markdown` |
@@ -183,6 +184,30 @@ clicking the wrong thing), or a name-drift finding that is not attributable.
183
184
  - *"…both captures ran under the same browser state"*: the app itself no longer renders them.
184
185
  Real — and still one finding, not N.
185
186
 
187
+ 17. **Never diff a `--deep` baseline against a shallow capture** (nor the reverse). A deep contract
188
+ holds the elements behind menus, tabs and dialogs; a shallow one cannot, so all of them read as
189
+ `lost`. This is not a handful of noisy rows — on a real page most of the surface can sit behind
190
+ interaction, so the mismatch manufactures the entire BLOCK. `diff` prints the warning *before*
191
+ the verdict; take it, and re-capture both sides the same way. Depth is a property of a
192
+ **baseline**, not a flag to toggle per run.
193
+
194
+ 18. **Never read a partly-explored deep contract as full coverage.** When the click budget runs
195
+ out, `map` says `N pages only PARTLY explored` and the summary counts them. Those contracts
196
+ describe *part* of the reachable surface, so drift in the rest is ungated — the same rule as
197
+ `run`'s live coverage and `map`'s "N of M pages": a surface left unmeasured is never a pass.
198
+ The remedy is `--deep-budget <n>` or a narrower `--depth 1`, never ignoring the line.
199
+
200
+ 19. **Never present `--deep` as free.** It clicks its way through the page's states, so it costs
201
+ ~1 min per page where a plain `map` costs seconds. Recommend it for building or refreshing a
202
+ **baseline**, not for a per-commit gate, and say so when you propose it.
203
+
204
+ 20. **Never raise `--depth` to "get better coverage".** The default is 1 because it *finishes*.
205
+ Depth 2 was measured at +30% elements for 3× the time and **never completed at any budget** —
206
+ 80, 150 and 250 clicks all returned the same elements, so the extra clicks re-walk states that
207
+ hold nothing new. If a user asks for deeper, tell them what it costs and that the contract
208
+ will read `PARTLY explored` permanently. A partial contract that reports green is the failure
209
+ this package exists to prevent.
210
+
186
211
  The durable fix is upstream, in `config.json`, and it is not a rewrite:
187
212
 
188
213
  ```json