@ia-qa/self-healing 1.6.17 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -1
- package/TUTORIAL.md +31 -4
- package/dist/browser/resolve.d.ts +20 -0
- package/dist/browser/resolve.js +78 -0
- package/dist/browser/resolve.js.map +1 -0
- package/dist/cli/args.js +3 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.js +16 -2
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/diff.d.ts +21 -1
- package/dist/cli/diff.js +143 -25
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/discover.js +9 -0
- package/dist/cli/discover.js.map +1 -1
- package/dist/cli/fix.js +33 -0
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/index.js +50 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/login.d.ts +28 -0
- package/dist/cli/login.js +179 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/map.d.ts +19 -3
- package/dist/cli/map.js +191 -17
- package/dist/cli/map.js.map +1 -1
- package/dist/config.d.ts +68 -0
- package/dist/config.js +94 -1
- package/dist/config.js.map +1 -1
- package/dist/crawl.d.ts +2 -0
- package/dist/crawl.js +13 -4
- package/dist/crawl.js.map +1 -1
- package/dist/fixEngine.d.ts +23 -0
- package/dist/fixEngine.js +27 -0
- package/dist/fixEngine.js.map +1 -1
- package/dist/launcher.d.ts +7 -0
- package/dist/launcher.js +4 -4
- package/dist/launcher.js.map +1 -1
- package/dist/resolution.d.ts +96 -0
- package/dist/resolution.js +197 -0
- package/dist/resolution.js.map +1 -0
- package/dist/ui/page.js +10 -0
- package/dist/ui/page.js.map +1 -1
- package/dist/ui/server.js +66 -0
- package/dist/ui/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/ia-qa-heal/SKILL.md +25 -1
package/README.md
CHANGED
|
@@ -173,7 +173,63 @@ Everything the tool needs lives in **one JSON file**, written for you by `ia-qa-
|
|
|
173
173
|
- **"My diff BLOCKs on links that just… rotate (a news feed, a blog widget)?"** → That is content churn, not UI drift: the feed's links entered the contract as if they were buttons, and the next capture caught the feed mid-rotation. Name what rotates in `"volatile"` (see above) and it never enters a contract again — the diff even prints a hint pointing here when a BLOCK is made of lost external links.
|
|
174
174
|
- **"The same rows come back as `renamed` every run — a cart counter, a call total, a clock."** → Only the *label* is moving, so only the label should stop being compared: put the pattern in `"nameMask"` (`"Cart (*)"`), not in `"volatile"`. `volatile` would delete the element from the contract — counter, button, href and coverage together — so a real selector break on it would go unnoticed forever; a mask keeps all of that gating and exempts the name alone. The diff prints the exact config line to paste when it spots the pattern. Two masks it refuses to apply, and says so: one that would hide a label your tests locate by name (that is a red suite behind a green gate), and one that would leave two elements of the same role wearing one name (a locator coin flip the mask itself created).
|
|
175
175
|
- **"How do I add a page behind a click, not a URL?"** → add `steps` to that page (see `billing` above): a list of `{ "click": { "role": "...", "name": "..." } }`. Named by role + accessible name, exactly like the contract — never a CSS selector.
|
|
176
|
-
- **"My app spans several domains behind a single sign-on (federated SSO) — does `map` cover it?"** → **Not with one `auth` block.** `auth` runs **one** login against **one** domain, before the first page. A federated IdP that re-challenges each project/domain with its own scope (`?projectName=…`, a different sub-domain per portal) is never satisfied by that single login — the second domain bounces back to a login the first one never covered, and the page maps empty (map now warns when an authenticated target returns 0 elements). Two ways through: **one `.ia-qa/` per domain/portal**, each selected with `--config <dir>` (see **Monorepos** below), or **`ia-qa-heal run` with `IAQA_CAPTURE=1`** so your own suite — which already handles the full SSO dance (cookies, redirects, per-project scope) — does the auth and the contract is captured during that real run. For a multi-portal app, capture-during-run is the intended path; `map --auth` is for single-domain apps.
|
|
176
|
+
- **"My app spans several domains behind a single sign-on (federated SSO) — does `map` cover it?"** → **Not with one `auth` block.** `auth` runs **one** login against **one** domain, before the first page. A federated IdP that re-challenges each project/domain with its own scope (`?projectName=…`, a different sub-domain per portal) is never satisfied by that single login — the second domain bounces back to a login the first one never covered, and the page maps empty (map now warns when an authenticated target returns 0 elements). Two ways through: **one `.ia-qa/` per domain/portal**, each selected with `--config <dir>` (see **Monorepos** below), or **`ia-qa-heal run` with `IAQA_CAPTURE=1`** so your own suite — which already handles the full SSO dance (cookies, redirects, per-project scope) — does the auth and the contract is captured during that real run. And since 1.7.0 there is a third, which is the general one: **`ia-qa-heal login`** — you log in once in a visible browser, whatever the app asks, and `map` reuses that session (see below). For a multi-portal app, capture-during-run is the intended path in CI; `map --auth` is for single-domain apps.
|
|
177
|
+
|
|
178
|
+
### Behind any login — `ia-qa-heal login`
|
|
179
|
+
|
|
180
|
+
`auth` fills a form: a username field, a password field, a submit button. That is *one shape of
|
|
181
|
+
login*, and plenty of apps do not have it — federated SSO, MFA, a consent screen, a magic link, a
|
|
182
|
+
device check. No list of selectors gets through any of those, so for those apps "add an `auth`
|
|
183
|
+
block" is not an answer at all.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
ia-qa-heal login # opens a visible browser at your first configured page
|
|
187
|
+
# you log in there, however your app asks
|
|
188
|
+
ia-qa-heal map # reuses that session — no auth block needed
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
It models nothing about your login, which is exactly why it works anywhere: the human does
|
|
192
|
+
whatever the app requires, and the browser session is saved to `.ia-qa/session.json` in
|
|
193
|
+
Playwright's `storageState` shape. `map` and `discover --crawl` pick it up automatically and say
|
|
194
|
+
so; it takes precedence over `auth` when both exist (you ran `login` *because* the form fill
|
|
195
|
+
could not get through — re-running it silently would be the tool arguing with you).
|
|
196
|
+
|
|
197
|
+
- **That file is a secret.** It holds live cookies: whoever has it is logged in as you. It never
|
|
198
|
+
leaves your machine — nothing here uploads it — and `login` writes `.ia-qa/.gitignore` so a
|
|
199
|
+
commit cannot carry it.
|
|
200
|
+
- **It needs a person.** It refuses without a TTY and under CI, and an AI agent driving the CLI
|
|
201
|
+
cannot perform it: the agent hands the step to you by name.
|
|
202
|
+
- **It expires.** When the session dies, `map` hits the login wall again and says to re-run
|
|
203
|
+
`login` — it never silently maps a login form under the name of your dashboard.
|
|
204
|
+
- **It is not a CI mechanism.** In a pipeline, use `auth` (reproducible, secrets from env/SSM) or
|
|
205
|
+
capture-during-run (`IAQA_CAPTURE=1` + `testCommand`, your own suite logs itself in).
|
|
206
|
+
|
|
207
|
+
`--url <path>` opens a page other than the first one in `config.pages`.
|
|
208
|
+
|
|
209
|
+
#### You already have a session — use it
|
|
210
|
+
|
|
211
|
+
An authenticated Playwright suite usually writes a `storageState` in `globalSetup`. Point at
|
|
212
|
+
it and skip logging in a second time:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
ia-qa-heal map --session playwright/.auth/user.json # a flag
|
|
216
|
+
IAQA_SESSION=playwright/.auth/user.json ia-qa-heal map # or an env var
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```jsonc
|
|
220
|
+
// …or in .ia-qa/config.json, so every verb picks it up:
|
|
221
|
+
"session": "playwright/.auth/user.json"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Precedence: `--session` → `IAQA_SESSION` → `config.session` → the file `login` wrote. Relative
|
|
225
|
+
paths resolve from the project root, and `--session` works on `map`, `discover` and `login`
|
|
226
|
+
(there it says *where to write*, so a refresh updates the file you already point at).
|
|
227
|
+
|
|
228
|
+
**A session you named and that cannot be read stops the run** — it never falls through to
|
|
229
|
+
"no session". Silently ignoring it would capture the login page under your pages' names, which
|
|
230
|
+
is the one failure this tool treats as worse than stopping. Same for a file that is not a
|
|
231
|
+
storageState: it says so, instead of failing later inside the browser.
|
|
232
|
+
|
|
177
233
|
|
|
178
234
|
### Monorepos — one config per portal (`--config <dir>`)
|
|
179
235
|
|
|
@@ -337,6 +393,24 @@ capture → ingest → diff → dry-run of the fixes → your confirmation → f
|
|
|
337
393
|
- **After the fix, `run` re-runs your suite once more (without `IAQA_CAPTURE`) to confirm the rewrite is actually green** — a selector fix is only worth anything if the tests pass after it. The exit code follows that re-run: `0` if the suite passes, `1` if it still fails (the rewrite landed but something else is red). `--no-verify` applies the fix and skips the re-run; the re-run is also skipped, with a reason, when nothing was rewritten or there is no `testCommand`.
|
|
338
394
|
- With `--report`, the HTML for a `run` additionally carries the **applied** `old → new` edits (file and line numbers) and the verify verdict — the file-level before/after the `diff` report only promised.
|
|
339
395
|
|
|
396
|
+
### Already ran your suite with `IAQA_CAPTURE=1`? Just `diff`
|
|
397
|
+
|
|
398
|
+
`run` is a convenience, not the only door. If the capture already happened — your suite ran with
|
|
399
|
+
`IAQA_CAPTURE=1`, in CI or by hand — the verdict is one offline command away:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
IAQA_CAPTURE=1 npx playwright test # your suite, as usual
|
|
403
|
+
ia-qa-heal diff # merges .capture/ and compares — no browser, no re-run
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`diff`, `fix` and `baseline` all merge the staging directory (`.ia-qa/mapping/.capture/`) on the
|
|
407
|
+
way in and say so. **Do not run `map` to "finish" a capture**: `map` opens its own browser and
|
|
408
|
+
starts over, without whatever session your suite had.
|
|
409
|
+
|
|
410
|
+
And on a slow suite, `run` runs it **twice** — once to capture, once to verify the fix. `run
|
|
411
|
+
--no-verify` keeps the first only; the exit code then reflects the diff rather than a re-proved
|
|
412
|
+
green suite.
|
|
413
|
+
|
|
340
414
|
### Capture during your test run — `@ia-qa/self-healing/capture`
|
|
341
415
|
|
|
342
416
|
One line at the top of a spec (or wrap your own extended `test` with `withCapture`):
|
|
@@ -402,6 +476,51 @@ Statically scans your tests/POMs → `.ia-qa/usage.json`. Two halves, because su
|
|
|
402
476
|
|
|
403
477
|
Purely static: no execution, no network. A locator built by concatenation or holding `${…}` is invisible here — and invisible to `fix` too, which is why it is skipped rather than guessed at.
|
|
404
478
|
|
|
479
|
+
### Page Objects written in CSS — `_resolved/`
|
|
480
|
+
|
|
481
|
+
Your Page Object says `.btn-primary`. The contract says `#pay-now`. Both name the same
|
|
482
|
+
button, and until they are **bound** every drift row reads `not referenced by your tests` —
|
|
483
|
+
technically true, practically useless, and the reason a CSS-anchored suite used to get
|
|
484
|
+
nothing out of a diff.
|
|
485
|
+
|
|
486
|
+
So `map` binds them: with `usage.json` present (run `ia-qa-heal ingest` first), each
|
|
487
|
+
selector your tests write is evaluated against the page and recorded in
|
|
488
|
+
`.ia-qa/mapping/_resolved/<page>.json`. `baseline` promotes those alongside the contracts,
|
|
489
|
+
and `diff` compares the two moments.
|
|
490
|
+
|
|
491
|
+
What that buys you — an example that no other check in this package can catch:
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
🔧 FIX
|
|
495
|
+
2 ok · 0 renamed · 0 healable · 0 lost ← the contract did not drift at all
|
|
496
|
+
|
|
497
|
+
🔗 1 selector your tests write no longer reaches what it used to:
|
|
498
|
+
🔧 .btn-primary → #pay-now
|
|
499
|
+
the element is still there, only its address moved.
|
|
500
|
+
tests/checkout.spec.js:1, tests/checkout.spec.js:4
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The button never moved; someone renamed a CSS class. The element diff is spotless, the
|
|
504
|
+
suite is broken, and `ia-qa-heal fix` rewrites the literal in your files.
|
|
505
|
+
|
|
506
|
+
Four outcomes, and only one of them is repaired automatically:
|
|
507
|
+
|
|
508
|
+
| | |
|
|
509
|
+
|---|---|
|
|
510
|
+
| the string now reaches **nothing**, and the element is still there | 🔧 rewritten |
|
|
511
|
+
| the string now reaches a **different** element | ⛔ never rewritten — your test still passes and acts on the wrong thing |
|
|
512
|
+
| the string now reaches **several** elements | ⛔ a coin flip, a human decides |
|
|
513
|
+
| the element it named is **gone** | ⛔ nothing to rewrite towards |
|
|
514
|
+
|
|
515
|
+
And the case that stays quiet: a selector matching something the contract does not hold —
|
|
516
|
+
a row, a cell, a container. That is most of a real Page Object, it is proof the selector
|
|
517
|
+
**works**, and calling it "not found" would bury the four rows above under hundreds of
|
|
518
|
+
false ones.
|
|
519
|
+
|
|
520
|
+
No `usage.json` ⇒ no bindings ⇒ nothing changes. Same rule as name drift: **no inventory,
|
|
521
|
+
no escalation.** Today the bindings are captured by `map`; capture-during-run is specified
|
|
522
|
+
in `SPEC-selector-resolution.md` and not built yet.
|
|
523
|
+
|
|
405
524
|
### The rename your suite actually depends on
|
|
406
525
|
|
|
407
526
|
A relabelled element is classified `renamed`: the selector still resolves, the click still lands on the right thing. **For a CSS-anchored suite that is harmless. For a name-anchored one it is fatal** — `getByRole('button', { name: 'Save' })` stops matching the moment the button says "Enregistrer", and a selector-only diff would exit 0 while your suite goes red.
|
package/TUTORIAL.md
CHANGED
|
@@ -248,14 +248,41 @@ It asks a few questions (your app's URL, whether there's a login, which pages to
|
|
|
248
248
|
|
|
249
249
|
- **🔑 Credentials** — you store the *name* of an environment variable, never the password itself. The real value stays in your `.env` / shell / AWS. **Loading it is your job:** the CLI reads `process.env` and never opens a `.env` file, so a key sitting only in `.env` will not be seen — run `map` under `npx dotenv-cli --` (or Node's `--env-file-if-exists`, 20.12+). Only `map` logs in; `diff`, `baseline`, `audit` and `check` read the mapping and need no credentials. If a variable is missing, the tool stops and names the exact variable to set — it never guesses and never writes a secret to disk.
|
|
250
250
|
- **🌐 URLs to visit** — the list of pages to map. A page that lives behind a click instead of its own URL (a tab or modal an app swaps in) is reached by naming the control to click, not by a URL.
|
|
251
|
-
- **🔐 One login** — the wizard sets up a single login sequence. **Want to test as several users (admin, then guest)?** Two options: keep a separate `.ia-qa/` config per role — select each with `--config <dir>`, the same flag a monorepo uses to keep one config per app/portal — **or** skip the login wiring entirely and use `ia-qa-heal run` (Step 6.7) — it captures the map *while your own test suite runs*, so whatever logins your tests already do, for however many users (and across however many domains), are covered automatically.
|
|
251
|
+
- **🔐 One login** — the wizard sets up a single login sequence. **And a form fill only covers a login that *is* a form** — SSO with MFA, a consent screen, a magic link and a device check are out of reach of any list of selectors. For those, `ia-qa-heal login` opens a visible browser, you log in once however the app asks, and the session is reused by `map` (Step 5.2a). Already have a `storageState` from your suite's `globalSetup`? Point at it with `--session <file>`, `IAQA_SESSION`, or `"session"` in the config — no second login. **Want to test as several users (admin, then guest)?** Two options: keep a separate `.ia-qa/` config per role — select each with `--config <dir>`, the same flag a monorepo uses to keep one config per app/portal — **or** skip the login wiring entirely and use `ia-qa-heal run` (Step 6.7) — it captures the map *while your own test suite runs*, so whatever logins your tests already do, for however many users (and across however many domains), are covered automatically.
|
|
252
252
|
|
|
253
253
|
The full annotated `config.json` — every field explained, plus a note for AI agents helping you set it up — is in the package's [README on npm](https://www.npmjs.com/package/@ia-qa/self-healing).
|
|
254
254
|
|
|
255
255
|
> 🔐 **Your password is never written to that file** — only the *name* of the environment variable that holds it.
|
|
256
256
|
|
|
257
|
-
### Step 5.
|
|
257
|
+
### Step 5.2a — Behind a login? `ia-qa-heal login`
|
|
258
|
+
|
|
259
|
+
Most apps worth mapping are behind a login, and only some of those logins are a form. This verb
|
|
260
|
+
models **nothing** about yours, which is why it works anywhere:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npx ia-qa-heal login # a real browser window opens at your first configured page
|
|
264
|
+
# you log in there — SSO, MFA, a consent screen, a magic link, all of it
|
|
265
|
+
npx ia-qa-heal map # reuses that session; no "auth" block needed
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Nothing is typed for you and no credential is read or stored by the tool. What is saved is the
|
|
269
|
+
**browser session** (`.ia-qa/session.json`) — live cookies, so treat it like a password: it stays
|
|
270
|
+
on your machine, nothing uploads it, and `login` writes `.ia-qa/.gitignore` so a commit cannot
|
|
271
|
+
carry it. It expires like any session; when it does, `map` says so and tells you to run this again
|
|
272
|
+
rather than quietly mapping the login page under your dashboard's name.
|
|
258
273
|
|
|
274
|
+
**Already have a session?** An authenticated Playwright suite usually writes a `storageState` in
|
|
275
|
+
`globalSetup` — point at it instead of logging in twice: `ia-qa-heal map --session
|
|
276
|
+
playwright/.auth/user.json` (or `IAQA_SESSION=…`, or `"session"` in `.ia-qa/config.json`).
|
|
277
|
+
|
|
278
|
+
Two things it deliberately will not do: it **refuses without a terminal and under CI** (it waits
|
|
279
|
+
for a person — twice: at the browser, then at the keyboard), and it is **not a CI mechanism**. In a
|
|
280
|
+
pipeline, use `auth` (secrets from env/SSM) or capture-during-run (Step 6.7), where your own suite
|
|
281
|
+
logs itself in. An AI agent driving the CLI cannot perform this step — it will hand it to you.
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
### Step 5.2b — (optional) Find the pages you're *not* testing: `discover`
|
|
259
286
|
You list a handful of pages to map — but how many pages does your app actually have? `discover` answers that: it reads your whole site (the sitemap, or a safe crawl) and shows you **what you're not covering yet**. It **only suggests** — it changes nothing until you add `--apply`.
|
|
260
287
|
|
|
261
288
|
```bash
|
|
@@ -265,7 +292,7 @@ npx ia-qa-heal discover --crawl --apply # happy with the list? add the missing
|
|
|
265
292
|
```
|
|
266
293
|
|
|
267
294
|
- **The sitemap way** (default) is instant and touches nothing — it treats `www.yoursite.com` and `yoursite.com` as the same site, so you don't have to match them exactly. Its limit: a sitemap lists your *public* pages and stops at the login.
|
|
268
|
-
- **The crawl way** (`--crawl`) goes further: it
|
|
295
|
+
- **The crawl way** (`--crawl`) goes further: it authenticates exactly like `map` does — the same `auth` block, or the same saved session (`ia-qa-heal login` / `--session`) — and follows links to find the pages behind the login. It's built to be **safe** — it only ever *reads* pages (never clicks "Delete", "Log out", "Pay"…), and to peek inside a menu or dropdown it just **opens** it and moves on. Nothing is submitted, nothing is changed.
|
|
269
296
|
|
|
270
297
|
You'll see your coverage gap:
|
|
271
298
|
|
|
@@ -455,7 +482,7 @@ Three optional config keys make it better (`ia-qa-heal init` asks for them):
|
|
|
455
482
|
```
|
|
456
483
|
(Other frameworks / non-JS Selenium: write a capture shard directly — the format is documented in the README. The shard, not the import, is the real interface.)
|
|
457
484
|
|
|
458
|
-
(Playwright: your own extended `test`? Wrap it: `withCapture(myTest)`.) All are **inert without `IAQA_CAPTURE=1`** — a normal run pays nothing. **This is also the way through a multi-domain / federated-SSO app**: `map`'s single login can't reach every portal, but your suite already logs into each — capture rides that real run, with no `auth` block and without ever touching your credentials. (Monorepo with one `.ia-qa/` per portal? Set `IAQA_CONFIG_DIR=apps/xsp` so the shards land in the right one.) No `testCommand` → `run` falls back to `map`.
|
|
485
|
+
(Playwright: your own extended `test`? Wrap it: `withCapture(myTest)`.) All are **inert without `IAQA_CAPTURE=1`** — a normal run pays nothing. **This is also the way through a multi-domain / federated-SSO app**: `map`'s single login can't reach every portal, but your suite already logs into each — capture rides that real run, with no `auth` block and without ever touching your credentials. (No suite yet, or you want `map` to work on its own? `ia-qa-heal login` is the other way in: one browser, one human login, one reused session — see Step 5.2a.) (Monorepo with one `.ia-qa/` per portal? Set `IAQA_CONFIG_DIR=apps/xsp` so the shards land in the right one.) No `testCommand` → `run` falls back to `map`.
|
|
459
486
|
|
|
460
487
|
- **`testPaths`** (e.g. `["tests/"]`) — feeds `ia-qa-heal ingest`, which inventories the selector literals your tests/POMs actually use into `.ia-qa/usage.json`. `diff` then tells you *used 3× in 2 test files* vs *not referenced by your tests*, and `fix` finds your files on its own.
|
|
461
488
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a Page Object's selector strings against the live DOM.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the browser (serialized by Playwright into `page.evaluate`), so it stays
|
|
5
|
+
* dependency-free like everything else in `src/browser/`. Unlike its neighbours it is
|
|
6
|
+
* **npm-only**: the web tool imports `match.js`, `contract.js` and `extract.js`, and
|
|
7
|
+
* nothing here is needed there — a page contract has no Page Object.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: `ingest` inventories the literal strings a suite uses (`.btn-primary`),
|
|
10
|
+
* while the contract stores a selector `extract.js` synthesized (`#pay-now`). The two
|
|
11
|
+
* never intersect, so a CSS-written suite gets "0 drifted selectors referenced by your
|
|
12
|
+
* tests" on every run — true, and useless. The missing fact is that both name the same
|
|
13
|
+
* button, and only a browser can establish it.
|
|
14
|
+
*
|
|
15
|
+
* See SPEC-selector-resolution.md. The statuses are load-bearing, `outside` above all:
|
|
16
|
+
* a Page Object is full of selectors for containers, rows and cells, which are real
|
|
17
|
+
* elements the contract deliberately does not hold. Calling those "not found" would print
|
|
18
|
+
* hundreds of false findings on the first run.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveSelectors(args: any): {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveSelectors = resolveSelectors;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a Page Object's selector strings against the live DOM.
|
|
6
|
+
*
|
|
7
|
+
* Runs inside the browser (serialized by Playwright into `page.evaluate`), so it stays
|
|
8
|
+
* dependency-free like everything else in `src/browser/`. Unlike its neighbours it is
|
|
9
|
+
* **npm-only**: the web tool imports `match.js`, `contract.js` and `extract.js`, and
|
|
10
|
+
* nothing here is needed there — a page contract has no Page Object.
|
|
11
|
+
*
|
|
12
|
+
* Why this exists: `ingest` inventories the literal strings a suite uses (`.btn-primary`),
|
|
13
|
+
* while the contract stores a selector `extract.js` synthesized (`#pay-now`). The two
|
|
14
|
+
* never intersect, so a CSS-written suite gets "0 drifted selectors referenced by your
|
|
15
|
+
* tests" on every run — true, and useless. The missing fact is that both name the same
|
|
16
|
+
* button, and only a browser can establish it.
|
|
17
|
+
*
|
|
18
|
+
* See SPEC-selector-resolution.md. The statuses are load-bearing, `outside` above all:
|
|
19
|
+
* a Page Object is full of selectors for containers, rows and cells, which are real
|
|
20
|
+
* elements the contract deliberately does not hold. Calling those "not found" would print
|
|
21
|
+
* hundreds of false findings on the first run.
|
|
22
|
+
*/
|
|
23
|
+
function resolveSelectors(args) {
|
|
24
|
+
// One argument: Playwright serializes this function into the page and calls it with a
|
|
25
|
+
// single serializable value. Two parameters would silently arrive as one.
|
|
26
|
+
const pomSelectors = args.pom;
|
|
27
|
+
const contractSelectors = args.contract;
|
|
28
|
+
// Element → the contract selector that names it. Built once; first writer wins, so two
|
|
29
|
+
// contract entries resolving to the same node cannot make a binding look ambiguous.
|
|
30
|
+
const contracted = new Map();
|
|
31
|
+
for (const cs of contractSelectors) {
|
|
32
|
+
try {
|
|
33
|
+
const el = document.querySelector(cs);
|
|
34
|
+
if (el && !contracted.has(el))
|
|
35
|
+
contracted.set(el, cs);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* a contract selector the browser cannot parse costs its own entry, not the page */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const pom of pomSelectors) {
|
|
43
|
+
let nodes;
|
|
44
|
+
try {
|
|
45
|
+
nodes = Array.prototype.slice.call(document.querySelectorAll(pom));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Not parseable as a selector — usually a template string the static scan caught
|
|
49
|
+
// mid-concatenation, occasionally a real bug in the test.
|
|
50
|
+
out[pom] = { status: 'invalid' };
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (nodes.length === 0) {
|
|
54
|
+
// Says nothing about whether the element exists: this page, this state, this moment.
|
|
55
|
+
out[pom] = { status: 'unresolved' };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const mapped = [];
|
|
59
|
+
for (const n of nodes) {
|
|
60
|
+
const cs = contracted.get(n);
|
|
61
|
+
if (cs && mapped.indexOf(cs) === -1)
|
|
62
|
+
mapped.push(cs);
|
|
63
|
+
}
|
|
64
|
+
if (nodes.length === 1 && mapped.length === 1) {
|
|
65
|
+
out[pom] = { status: 'bound', selector: mapped[0] };
|
|
66
|
+
}
|
|
67
|
+
else if (mapped.length === 0) {
|
|
68
|
+
// It matches the page, just not anything under contract — a row, a cell, a heading.
|
|
69
|
+
// Proof the selector works, and never a finding.
|
|
70
|
+
out[pom] = { status: 'outside' };
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out[pom] = { status: 'ambiguous', count: nodes.length };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/browser/resolve.js"],"names":[],"mappings":";;AAmBA,4CAoDC;AAvED;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,gBAAgB,CAAC,IAAI;IACnC,sFAAsF;IACtF,0EAA0E;IAC1E,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC;IAC9B,MAAM,iBAAiB,GAAG,IAAI,CAAC,QAAQ,CAAC;IACxC,uFAAuF;IACvF,oFAAoF;IACpF,MAAM,UAAU,GAAG,IAAI,GAAG,EAAE,CAAC;IAC7B,KAAK,MAAM,EAAE,IAAI,iBAAiB,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;YACtC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,oFAAoF;QACtF,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,EAAE,CAAC;IACf,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,iFAAiF;YACjF,0DAA0D;YAC1D,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,qFAAqF;YACrF,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;YACpC,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,EAAE,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9C,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,CAAC;aAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,oFAAoF;YACpF,iDAAiD;YACjD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/cli/args.js
CHANGED
|
@@ -16,8 +16,9 @@ exports.rejectUnknownFlags = rejectUnknownFlags;
|
|
|
16
16
|
*/
|
|
17
17
|
exports.KNOWN_FLAGS = {
|
|
18
18
|
init: [],
|
|
19
|
-
discover: ['--sitemap', '--crawl', '--no-reveal', '--strict-host', '--apply', '--max', '--depth'],
|
|
20
|
-
|
|
19
|
+
discover: ['--sitemap', '--crawl', '--no-reveal', '--strict-host', '--apply', '--max', '--depth', '--session'],
|
|
20
|
+
login: ['--url', '--session'],
|
|
21
|
+
map: ['--screenshots', '--session'],
|
|
21
22
|
baseline: [],
|
|
22
23
|
diff: ['--strict', '--json', '--junit', '--dir', '--report', '--open'],
|
|
23
24
|
fix: ['--dry-run', '--locators', '--dir'],
|
package/dist/cli/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AA8BA,gDAeC;AA7CD;;;;;;;;;;;GAWG;AACU,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE,EAAE;IACR,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC;IAC9G,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;IAC7B,GAAG,EAAE,CAAC,eAAe,EAAE,WAAW,CAAC;IACnC,QAAQ,EAAE,EAAE;IACZ,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IACtE,GAAG,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,OAAO,CAAC;IACzC,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC7B,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC;IAC3D,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;IAChD,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACtF,KAAK,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IAC7D,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9B,EAAE,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,CAAC;CACnF,CAAC;AAEF,SAAgB,kBAAkB,CAAC,OAAe,EAAE,IAAc;IAChE,MAAM,KAAK,GAAG,mBAAW,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CACb,eAAe,OAAO,0FAA0F;gBAC9G,4GAA4G;gBAC5G,8FAA8F,CACjG,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,qBAAqB,OAAO,8CAA8C,CAAC,CAAC;IAChH,CAAC;AACH,CAAC"}
|
package/dist/cli/baseline.js
CHANGED
|
@@ -70,6 +70,10 @@ async function runBaseline(args = []) {
|
|
|
70
70
|
}
|
|
71
71
|
const pages = jsonFilesIn(from);
|
|
72
72
|
const layouts = jsonFilesIn(path.join(from, config_1.LAYOUT_DIRNAME));
|
|
73
|
+
// Bindings are promoted with the contracts they describe: a binding verdict is a claim
|
|
74
|
+
// about two moments, exactly like a drift verdict, so the "before" has to be promotable
|
|
75
|
+
// the same way. Absent when no inventory exists — then nothing downstream changes.
|
|
76
|
+
const resolved = jsonFilesIn(path.join(from, config_1.RESOLVED_DIRNAME));
|
|
73
77
|
if (pages.length === 0 && layouts.length === 0) {
|
|
74
78
|
console.error(`❌ ${path.resolve(from)} holds no mapping files — run \`ia-qa-heal map\` first.`);
|
|
75
79
|
process.exitCode = 2;
|
|
@@ -82,6 +86,7 @@ async function runBaseline(args = []) {
|
|
|
82
86
|
// meant to be committed, so deleting is reversible; keeping is the silent lie.
|
|
83
87
|
const staleFiles = jsonFilesIn(to).filter((f) => !pages.includes(f));
|
|
84
88
|
const staleLayouts = jsonFilesIn(path.join(to, config_1.LAYOUT_DIRNAME)).filter((f) => !layouts.includes(f));
|
|
89
|
+
const staleResolved = jsonFilesIn(path.join(to, config_1.RESOLVED_DIRNAME)).filter((f) => !resolved.includes(f));
|
|
85
90
|
fs.mkdirSync(to, { recursive: true });
|
|
86
91
|
for (const file of pages) {
|
|
87
92
|
fs.copyFileSync(path.join(from, file), path.join(to, file));
|
|
@@ -92,19 +97,28 @@ async function runBaseline(args = []) {
|
|
|
92
97
|
fs.copyFileSync(path.join(from, config_1.LAYOUT_DIRNAME, file), path.join(to, config_1.LAYOUT_DIRNAME, file));
|
|
93
98
|
}
|
|
94
99
|
}
|
|
100
|
+
if (resolved.length > 0) {
|
|
101
|
+
fs.mkdirSync(path.join(to, config_1.RESOLVED_DIRNAME), { recursive: true });
|
|
102
|
+
for (const file of resolved) {
|
|
103
|
+
fs.copyFileSync(path.join(from, config_1.RESOLVED_DIRNAME, file), path.join(to, config_1.RESOLVED_DIRNAME, file));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
95
106
|
for (const file of staleFiles)
|
|
96
107
|
fs.unlinkSync(path.join(to, file));
|
|
97
108
|
for (const file of staleLayouts)
|
|
98
109
|
fs.unlinkSync(path.join(to, config_1.LAYOUT_DIRNAME, file));
|
|
99
|
-
const
|
|
110
|
+
for (const file of staleResolved)
|
|
111
|
+
fs.unlinkSync(path.join(to, config_1.RESOLVED_DIRNAME, file));
|
|
112
|
+
const removed = staleFiles.length + staleLayouts.length + staleResolved.length;
|
|
100
113
|
const total = pages.length + layouts.length;
|
|
101
114
|
console.log(`\n📌 Baseline promoted → ${rel(to)}`);
|
|
102
115
|
console.log(` ${pages.length} ${pages.length === 1 ? 'page' : 'pages'}` +
|
|
103
116
|
(layouts.length > 0 ? ` · ${layouts.length} ${layouts.length === 1 ? 'layout' : 'layouts'}` : '') +
|
|
117
|
+
(resolved.length > 0 ? ` · ${resolved.length} with selector bindings` : '') +
|
|
104
118
|
(replaced > 0 ? ` (replaced a baseline of ${replaced} ${replaced === 1 ? 'file' : 'files'})` : ''));
|
|
105
119
|
if (removed > 0) {
|
|
106
120
|
console.log(` 🧹 Removed ${removed} stale ${removed === 1 ? 'file' : 'files'} the new mapping no longer produces: ` +
|
|
107
|
-
[...staleFiles, ...staleLayouts.map((f) => `${config_1.LAYOUT_DIRNAME}/${f}`)].join(', '));
|
|
121
|
+
[...staleFiles, ...staleLayouts.map((f) => `${config_1.LAYOUT_DIRNAME}/${f}`), ...staleResolved.map((f) => `${config_1.RESOLVED_DIRNAME}/${f}`)].join(', '));
|
|
108
122
|
}
|
|
109
123
|
// Detect phantom contracts: mapping files that don't match any configured page.
|
|
110
124
|
// These are left behind when a page is removed from config.json — they get promoted
|
package/dist/cli/baseline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/cli/baseline.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/cli/baseline.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/diff.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MaskOutcome } from '../nameMask';
|
|
2
|
+
import { BindingDrift } from '../resolution';
|
|
2
3
|
import { Usage } from '../ingest';
|
|
3
4
|
import { NameDriftFinding } from '../nameDrift';
|
|
4
5
|
import { FixOutcome, ReportEvidence } from '../htmlReport';
|
|
@@ -91,7 +92,7 @@ export declare function printNameDrift(findings: NameDriftFinding[] | undefined)
|
|
|
91
92
|
* and never in `--json` mode, where it would corrupt the machine-readable payload.
|
|
92
93
|
* Shared with `run`, which ends on the same verdict.
|
|
93
94
|
*/
|
|
94
|
-
export declare function driftSummaryLine(verb: string, verdict: 'PASS' | 'FIX' | 'BLOCK', c: Report['counts'], nameDrift?: NameDriftFinding[]): void;
|
|
95
|
+
export declare function driftSummaryLine(verb: string, verdict: 'PASS' | 'FIX' | 'BLOCK', c: Report['counts'], nameDrift?: NameDriftFinding[], bindingDrift?: BindingDrift[]): void;
|
|
95
96
|
export interface DirDiffCollection {
|
|
96
97
|
reports: Array<{
|
|
97
98
|
name: string;
|
|
@@ -101,6 +102,13 @@ export interface DirDiffCollection {
|
|
|
101
102
|
/** Pairs where both sides came out of the same `map` run — incapable of showing drift. */
|
|
102
103
|
stale: string[];
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* A broken binding whose element survived is repairable — FIX. Everything else is a
|
|
107
|
+
* decision: `rebound` most of all, where the test still passes and now acts on a
|
|
108
|
+
* different element. That is the selector-space twin of the element `rebound`, and the
|
|
109
|
+
* one failure no other check in this package can see.
|
|
110
|
+
*/
|
|
111
|
+
export declare function escalateBindingVerdict(verdict: Report['verdict'], drift: BindingDrift[]): Report['verdict'];
|
|
104
112
|
/**
|
|
105
113
|
* The collection half of a directory diff, shared with `ia-qa-heal run`: walk two
|
|
106
114
|
* directories of mapping files, pair by filename (layouts in `_layouts/` first),
|
|
@@ -130,6 +138,12 @@ export interface DirReport {
|
|
|
130
138
|
nameDrift?: NameDriftFinding[];
|
|
131
139
|
/** `config.nameMask`'s effect across the walk — applied patterns and refusals, unioned. */
|
|
132
140
|
nameMask?: MaskOutcome;
|
|
141
|
+
/**
|
|
142
|
+
* Selector strings from the suite whose binding to a contracted element broke.
|
|
143
|
+
* Present only once `ingest` has inventoried the tests **and** `map` has resolved
|
|
144
|
+
* them — absent means "not measured", never "none". Same rule as `nameDrift`.
|
|
145
|
+
*/
|
|
146
|
+
bindingDrift?: BindingDrift[];
|
|
133
147
|
}
|
|
134
148
|
export declare function aggregateReports(reports: Array<{
|
|
135
149
|
name: string;
|
|
@@ -189,4 +203,10 @@ export interface Report {
|
|
|
189
203
|
* refused with the reason. Absent when no mask is configured.
|
|
190
204
|
*/
|
|
191
205
|
nameMask?: MaskOutcome;
|
|
206
|
+
/**
|
|
207
|
+
* Selector strings from the suite whose binding to a contracted element broke.
|
|
208
|
+
* Present only once `ingest` has inventoried the tests **and** the capture has
|
|
209
|
+
* resolved them — absent means "not measured", never "none". Same rule as `nameDrift`.
|
|
210
|
+
*/
|
|
211
|
+
bindingDrift?: BindingDrift[];
|
|
192
212
|
}
|