@ia-qa/self-healing 1.5.7 → 1.6.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/README.md +80 -6
- package/dist/checks.d.ts +152 -0
- package/dist/checks.js +0 -0
- package/dist/checks.js.map +1 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/audit.d.ts +63 -0
- package/dist/cli/audit.js +328 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/check.d.ts +1 -0
- package/dist/cli/check.js +329 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/diff.d.ts +20 -1
- package/dist/cli/diff.js +106 -9
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/fix.js +53 -7
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/index.js +30 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ingest.js +26 -9
- package/dist/cli/ingest.js.map +1 -1
- package/dist/cli/run.js +11 -3
- package/dist/cli/run.js.map +1 -1
- package/dist/fixEngine.d.ts +41 -2
- package/dist/fixEngine.js +65 -5
- package/dist/fixEngine.js.map +1 -1
- package/dist/htmlReport.js +55 -6
- package/dist/htmlReport.js.map +1 -1
- package/dist/ingest.d.ts +15 -1
- package/dist/ingest.js +0 -0
- package/dist/ingest.js.map +1 -1
- package/dist/mcp/server.js +22 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/nameDrift.d.ts +85 -0
- package/dist/nameDrift.js +177 -0
- package/dist/nameDrift.js.map +1 -0
- package/dist/nameHint.d.ts +51 -0
- package/dist/nameHint.js +0 -0
- package/dist/nameHint.js.map +1 -0
- package/dist/nameLocators.d.ts +103 -0
- package/dist/nameLocators.js +235 -0
- package/dist/nameLocators.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -279,13 +279,84 @@ The shard format **is** the interface; the Playwright/Cypress/Selenium helpers a
|
|
|
279
279
|
|
|
280
280
|
Any `*.json` in that directory is merged into contracts by the next `ia-qa-heal` verb — named to match `config.pages` so it pairs with your baseline. That is the whole contract: no import, no Node required.
|
|
281
281
|
|
|
282
|
-
###
|
|
282
|
+
### Locator inventory — `ia-qa-heal ingest`
|
|
283
283
|
|
|
284
284
|
```bash
|
|
285
285
|
npx ia-qa-heal ingest tests/ # or no args: reads config.testPaths
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
-
Statically scans your tests/POMs
|
|
288
|
+
Statically scans your tests/POMs → `.ia-qa/usage.json`. Two halves, because suites are written two ways:
|
|
289
|
+
|
|
290
|
+
- **Selectors** — Playwright, Cypress, Selenium call shapes + `…Selector = '…'` properties. `diff` annotates every drifted selector with *used N× in M test files*, and `fix` no longer needs the test paths on the command line.
|
|
291
|
+
- **Names** — the `get|find|queryBy…` family (Playwright, Testing Library, `@testing-library/cypress`): `ByRole`, `ByLabel(Text)`, `ByPlaceholder(Text)`, `ByText`, `ByTitle`, `ByAltText` — plus `cy.contains(…)` (both forms) and Selenium's `By.linkText` / `By.LINK_TEXT` / `By.PARTIAL_LINK_TEXT`. These are what makes the rename gate below possible.
|
|
292
|
+
|
|
293
|
+
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.
|
|
294
|
+
|
|
295
|
+
### The rename your suite actually depends on
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
Once `ingest` has run, `diff` cross-references every rename against what your tests reference:
|
|
300
|
+
|
|
301
|
+
| Situation | Verdict | What `fix` does |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| No test names that label | unchanged (`PASS`) | nothing — adopting this adds no noise |
|
|
304
|
+
| One element carries the new label | **FIX** | rewrites the call: `getByRole('button', { name: 'Enregistrer' })` |
|
|
305
|
+
| The **old** label now sits on another element | **BLOCK** | nothing — your locator would silently target the wrong element (the name-space twin of `rebound`) |
|
|
306
|
+
| Several elements carry the new label | **BLOCK** | nothing — a human picks |
|
|
307
|
+
| The reference is text-anchored (see below) | **BLOCK** | nothing — reported with its `file:line` |
|
|
308
|
+
|
|
309
|
+
**Only calls that state a role are ever rewritten.** That is the line between the two halves of the inventory, and it is not arbitrary:
|
|
310
|
+
|
|
311
|
+
| Rewritten | Reported, never rewritten |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `getByRole` / `findByRole` / `queryByRole` (Playwright, Testing Library, `@testing-library/cypress`) | `getByText` / `findByText` |
|
|
314
|
+
| `getByLabel` · `getByLabelText` · `getByPlaceholder(Text)` — the role is implied | `cy.contains(…)`, both forms |
|
|
315
|
+
| `By.linkText` / `By.LINK_TEXT` / `By.PARTIAL_LINK_TEXT` (Selenium: role = link) | `getByTitle` · `getByAltText` |
|
|
316
|
+
|
|
317
|
+
`getByText('Archive')` names a *string*, not an element, and the contract maps interactive elements only — so it cannot prove the test meant the renamed button rather than a heading that never moved. Rewriting it would break a test that was passing, which is worse than any drift this tool detects. Those references are printed with their `file:line` and hold the verdict at BLOCK for a human instead.
|
|
318
|
+
|
|
319
|
+
Without `usage.json` nothing is escalated at all, so an un-ingested project behaves exactly as before.
|
|
320
|
+
|
|
321
|
+
### Is the app itself sound? — `ia-qa-heal check`
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npx ia-qa-heal check # dead links + static checks
|
|
325
|
+
npx ia-qa-heal check --offline # skip the network half, fully local
|
|
326
|
+
npx ia-qa-heal check --strict # fail the pipeline on warnings too
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`diff` answers *what moved since the baseline?*. `check` answers what a diff structurally cannot: **is the current contract healthy at all?** A green diff over a page whose every link 404s is still a green diff — verified on a sample app where `diff` exits 0 and `check` exits 1 at the same instant.
|
|
330
|
+
|
|
331
|
+
Every check is admitted under one rule: **it must be able to fail for a reason a contract diff would not already catch.** "The button still exists" is rejected by that rule — `diff` proves it better and without a browser. What survives:
|
|
332
|
+
|
|
333
|
+
| Check | Fails when | Verdict |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| **dead links** | the app answered 4xx/5xx (401/403 excluded — that is a working link behind auth) | **FAIL** |
|
|
336
|
+
| **unnamed** | an interactive element has no accessible name: no screen-reader announcement, no `getByRole`, no healing | WARN |
|
|
337
|
+
| **ambiguous** | two elements share role + name — every name-anchored locator for them is a coin flip, and a strict-mode violation waiting | WARN |
|
|
338
|
+
| **orphans** | a mapped page nothing links to | WARN |
|
|
339
|
+
|
|
340
|
+
FAIL is reserved for a fact the app itself stated. The rest are real but arguable — an unnamed icon button, a deep-link-only page — so they warn and exit 0 unless you ask for `--strict`. A gate nobody can leave on is a gate nobody turns on.
|
|
341
|
+
|
|
342
|
+
**Findings are de-duplicated across pages.** A navbar repeated on 142 pages is one thing to fix, not 142 findings — same reasoning that puts shared layouts in `_layouts/` for the diff.
|
|
343
|
+
|
|
344
|
+
**Network.** The link check requests URLs on your own `baseUrl` and nothing else: same site only, GET only, and never a URL that *acts* — `/logout`, `/orders/1/delete` and friends are skipped using the same denylist as `discover --crawl`, because a GET on those is not a check, it is a logout or a deletion. The run announces the host and the count **before** the first request. `--offline` drops that half entirely.
|
|
345
|
+
|
|
346
|
+
### Day one, no baseline — `ia-qa-heal audit`
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npx ia-qa-heal audit tests/ # advisory, exit 0
|
|
350
|
+
npx ia-qa-heal audit --strict # exit 1 on a finding
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Every other verb compares two moments, so its value arrives one drift *after* you install it. Nobody adopts a healing tool before their tests break. `audit` answers the question you have on the day you arrive — *does my suite still name things that exist?* — from a **single capture**: it cross-references the inventory against `mapping/` and flags locators that match no element, with the closest live label as a suggestion.
|
|
354
|
+
|
|
355
|
+
Deliberately conservative, and it says so out loud:
|
|
356
|
+
|
|
357
|
+
- Only **role-anchored** locators carry the verdict, and only for roles a contract can hold. `getByText('Terms of service')` may point at a paragraph `map` never captured, and `getByRole('heading', …)` at a heading it never captures — their absence proves nothing, so they are counted and not judged.
|
|
358
|
+
- **Advisory by default** (exit 0). A "missing" locator may live on a page not in `config.pages`, or in a page *state* `map` never saw (a label behind a tab or a mode toggle). Both caveats are printed next to the findings. Add `--strict` once your coverage is real.
|
|
359
|
+
- Read-only. `audit` never edits and never applies a suggestion.
|
|
289
360
|
|
|
290
361
|
### Elements with no accessible name
|
|
291
362
|
|
|
@@ -296,7 +367,7 @@ An icon button or image link with no `aria-label`/text has **no role + name iden
|
|
|
296
367
|
| | For | Entry point |
|
|
297
368
|
|---|---|---|
|
|
298
369
|
| **MCP server** (stdio) | AI agents — "map my app at \<url\>" | `ia-qa-heal-mcp` → tools `map_app`, `diff_mappings`, `fix_tests`, `suggest_heal` (optional AI) |
|
|
299
|
-
| **CLI** | humans & CI | `ia-qa-heal` → `init`, `discover`, `map`, `baseline`, `diff`, `fix`, `ingest`, `run` |
|
|
370
|
+
| **CLI** | humans & CI | `ia-qa-heal` → `init`, `discover`, `map`, `baseline`, `diff`, `fix`, `ingest`, `audit`, `run` |
|
|
300
371
|
| **AI add-on** (optional, BYOK) | semantic renames the deterministic engine gives up on | `ia-qa-heal-ai` → `suggest` — see [Optional AI add-on](#optional-ai-add-on--semantic-suggestions-ia-qa-heal-ai-byok) |
|
|
301
372
|
|
|
302
373
|
### Locale
|
|
@@ -513,11 +584,14 @@ jobs:
|
|
|
513
584
|
with: { node-version: '20' }
|
|
514
585
|
- run: npm ci
|
|
515
586
|
- run: npx playwright install --with-deps chromium
|
|
516
|
-
- run: npm start &
|
|
517
|
-
- run: npx ia-qa-heal map
|
|
518
|
-
- run: npx ia-qa-heal
|
|
587
|
+
- run: npm start & # your app on its usual port
|
|
588
|
+
- run: npx ia-qa-heal map # re-capture into .ia-qa/mapping/
|
|
589
|
+
- run: npx ia-qa-heal ingest # inventory selectors AND names your tests use
|
|
590
|
+
- run: npx ia-qa-heal diff # 0 = PASS/FIX · 1 = BLOCK · 2 = bad input
|
|
519
591
|
```
|
|
520
592
|
|
|
593
|
+
`ingest` is what makes the gate see a rename your tests locate by name. Skip it and `diff` judges selectors only — which is what it did before 1.6.0, and what it still does for any project without a `usage.json`.
|
|
594
|
+
|
|
521
595
|
`diff` with no arguments compares the committed `.ia-qa/baseline/` against what `map` just re-captured — which is why the baseline has to be in git. Add `--report drift-report.html` to attach a branded HTML report to the build, and `--strict` to fail on FIX too. The full workflow (report upload, artifact, `--strict`) is in **[TUTORIAL.md → Putting it in CI](TUTORIAL.md#8-putting-it-in-ci)**.
|
|
522
596
|
|
|
523
597
|
## Map artifacts
|
package/dist/checks.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract checks — the assertions a mapped app answers on its own.
|
|
3
|
+
*
|
|
4
|
+
* `diff` compares two moments and says what moved. These say something a diff
|
|
5
|
+
* never can: whether the *current* contract is sound at all. They exist under one
|
|
6
|
+
* rule, and it is the rule that keeps this from becoming a generator of tests
|
|
7
|
+
* that pass forever —
|
|
8
|
+
*
|
|
9
|
+
* **a check earns its place only if it can fail for a reason the contract diff
|
|
10
|
+
* would not already catch.**
|
|
11
|
+
*
|
|
12
|
+
* "This button still exists" fails that test: `diff` proves it better, faster,
|
|
13
|
+
* and without a browser. "This link 404s", "two buttons share a name", "no page
|
|
14
|
+
* links here" all pass it — a diff is green on every one of them.
|
|
15
|
+
*
|
|
16
|
+
* Pure functions over contracts: no I/O, no network, no config. The CLI does the
|
|
17
|
+
* reading and the fetching; this file is what the tests can hold still.
|
|
18
|
+
*/
|
|
19
|
+
export interface CheckElement {
|
|
20
|
+
role: string;
|
|
21
|
+
name: string;
|
|
22
|
+
selector: string;
|
|
23
|
+
context?: string;
|
|
24
|
+
href?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface CheckPage {
|
|
27
|
+
/** Mapping file basename — `_layouts/foo` for a shared layout. */
|
|
28
|
+
page: string;
|
|
29
|
+
/** The page's own URL as captured, absolute or path. */
|
|
30
|
+
url?: string;
|
|
31
|
+
elements: CheckElement[];
|
|
32
|
+
isLayout?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface UnnamedFinding {
|
|
35
|
+
role: string;
|
|
36
|
+
selector: string;
|
|
37
|
+
context?: string;
|
|
38
|
+
/** Every page carrying it — one entry means one place to fix. */
|
|
39
|
+
pages: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* An interactive element with no accessible name.
|
|
43
|
+
*
|
|
44
|
+
* Three separate failures in one: a screen reader announces nothing, a
|
|
45
|
+
* `getByRole` locator cannot target it, and self-healing cannot recover it after
|
|
46
|
+
* a move — identity here *is* role + name. This is the precondition of
|
|
47
|
+
* everything else this package does, which is why it is checked rather than
|
|
48
|
+
* merely counted.
|
|
49
|
+
*/
|
|
50
|
+
export declare function findUnnamed(pages: CheckPage[]): UnnamedFinding[];
|
|
51
|
+
export interface AmbiguousFinding {
|
|
52
|
+
role: string;
|
|
53
|
+
name: string;
|
|
54
|
+
count: number;
|
|
55
|
+
selectors: string[];
|
|
56
|
+
/** Every page where this exact collision occurs. */
|
|
57
|
+
pages: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Two elements on one page sharing a role AND an accessible name.
|
|
61
|
+
*
|
|
62
|
+
* Every name-anchored locator for them is a coin flip — `getByRole('button', {
|
|
63
|
+
* name: 'Edit' })` raises a Playwright strict-mode violation, and the healer
|
|
64
|
+
* reports `ambiguous` and refuses. This is drift that has not happened yet: the
|
|
65
|
+
* check names it while it is still cheap to fix with a `data-testid`.
|
|
66
|
+
*
|
|
67
|
+
* Layout elements are excluded from a page's count by the caller — a navbar
|
|
68
|
+
* repeated on 40 pages is one element, not 40 collisions.
|
|
69
|
+
*/
|
|
70
|
+
export declare function findAmbiguousNames(pages: CheckPage[]): AmbiguousFinding[];
|
|
71
|
+
export interface OrphanFinding {
|
|
72
|
+
page: string;
|
|
73
|
+
url: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A configured page that no *other* mapped page links to.
|
|
77
|
+
*
|
|
78
|
+
* Either the navigation lost a link (a real regression a selector diff shows as
|
|
79
|
+
* a perfectly healthy `lost` on some other page, or not at all), or the page is
|
|
80
|
+
* deep-link-only and the finding is noise the user dismisses once. Reported, not
|
|
81
|
+
* failed, for exactly that reason.
|
|
82
|
+
*
|
|
83
|
+
* Links from layouts count: a navbar entry is how most pages are reached.
|
|
84
|
+
*/
|
|
85
|
+
export declare function findOrphanPages(pages: CheckPage[], pathOf: (href: string) => string | null): OrphanFinding[];
|
|
86
|
+
export interface LinkTarget {
|
|
87
|
+
/** Absolute URL to request. */
|
|
88
|
+
url: string;
|
|
89
|
+
/** Where it was found — every page carrying this href. */
|
|
90
|
+
from: Array<{
|
|
91
|
+
page: string;
|
|
92
|
+
name: string;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Collect the distinct internal destinations the app links to.
|
|
97
|
+
*
|
|
98
|
+
* De-duplicated by absolute URL so a footer link on 40 pages is one request, and
|
|
99
|
+
* filtered by the caller's safety predicate — the same one `discover --crawl`
|
|
100
|
+
* uses. A GET is not harmless on a URL that *acts*: requesting `/logout` ends the
|
|
101
|
+
* session the rest of the run depends on, and `/orders/12/delete` is not a link
|
|
102
|
+
* check, it is data loss. Those are skipped and said out loud.
|
|
103
|
+
*/
|
|
104
|
+
export declare function collectLinkTargets(pages: CheckPage[], resolve: (href: string) => string | null): {
|
|
105
|
+
targets: LinkTarget[];
|
|
106
|
+
skipped: number;
|
|
107
|
+
};
|
|
108
|
+
export interface DeadLink extends LinkTarget {
|
|
109
|
+
status: number | null;
|
|
110
|
+
error?: string;
|
|
111
|
+
}
|
|
112
|
+
/** A response code that means the link is broken, not merely guarded. */
|
|
113
|
+
export declare function isDeadStatus(status: number): boolean;
|
|
114
|
+
export interface CheckReport {
|
|
115
|
+
pagesChecked: number;
|
|
116
|
+
elements: number;
|
|
117
|
+
unnamed: UnnamedFinding[];
|
|
118
|
+
ambiguous: AmbiguousFinding[];
|
|
119
|
+
orphans: OrphanFinding[];
|
|
120
|
+
deadLinks: DeadLink[];
|
|
121
|
+
linksChecked: number;
|
|
122
|
+
linksSkipped: number;
|
|
123
|
+
/** True when the network half was skipped — the verdict then covers less. */
|
|
124
|
+
offline: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Checks that did not run, and why.
|
|
127
|
+
*
|
|
128
|
+
* A verdict that omits its own gaps is the false green this project keeps
|
|
129
|
+
* relearning: "0 locators checked · all resolve", "N ok that were never
|
|
130
|
+
* compared". PASS here means "the checks that ran found nothing", and the only
|
|
131
|
+
* way that is not a lie is to say which ones ran.
|
|
132
|
+
*/
|
|
133
|
+
skipped: Array<{
|
|
134
|
+
check: string;
|
|
135
|
+
why: string;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
export type CheckVerdict = 'PASS' | 'WARN' | 'FAIL';
|
|
139
|
+
/**
|
|
140
|
+
* FAIL is reserved for a dead link: a request the app itself answered with an
|
|
141
|
+
* error, which is a fact, not a judgement. Everything else is real but arguable —
|
|
142
|
+
* an unnamed icon button, a deep-link-only page — so it warns and exits 0 unless
|
|
143
|
+
* the user asks for `--strict`. A gate nobody can leave on is a gate nobody turns
|
|
144
|
+
* on.
|
|
145
|
+
*/
|
|
146
|
+
export declare function checkVerdict(r: CheckReport): CheckVerdict;
|
|
147
|
+
/** How much of the check ran: e.g. `{ ran: 2, total: 4 }`. */
|
|
148
|
+
export declare const CHECK_NAMES: readonly ["dead links", "unnamed", "ambiguous", "orphans"];
|
|
149
|
+
export declare function checkCoverage(r: CheckReport): {
|
|
150
|
+
ran: number;
|
|
151
|
+
total: number;
|
|
152
|
+
};
|
package/dist/checks.js
ADDED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/args.js
CHANGED
|
@@ -22,6 +22,8 @@ exports.KNOWN_FLAGS = {
|
|
|
22
22
|
diff: ['--strict', '--json', '--dir', '--report', '--open'],
|
|
23
23
|
fix: ['--dry-run', '--locators', '--dir'],
|
|
24
24
|
ingest: [],
|
|
25
|
+
audit: ['--strict', '--json'],
|
|
26
|
+
check: ['--strict', '--json', '--offline'],
|
|
25
27
|
graph: ['--format', '--kind', '--out', '--open'],
|
|
26
28
|
run: ['--yes', '--no-verify', '--locators', '--report', '--open'],
|
|
27
29
|
};
|
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":";;;AA0BA,gDAeC;AAzCD;;;;;;;;;;;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,CAAC;IACjG,GAAG,EAAE,EAAE;IACP,QAAQ,EAAE,EAAE;IACZ,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IAC3D,GAAG,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,OAAO,CAAC;IACzC,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC7B,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,CAAC;IAC1C,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;IAChD,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,CAAC;CAClE,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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Usage } from '../ingest';
|
|
2
|
+
import { NameSite } from '../nameLocators';
|
|
3
|
+
/**
|
|
4
|
+
* `ia-qa-heal audit [--strict] [--json]` — a verdict on day one, with no baseline.
|
|
5
|
+
*
|
|
6
|
+
* Every other verb in this tool compares two moments, which means its value
|
|
7
|
+
* arrives one drift *after* you install it: map, wait for the app to change, then
|
|
8
|
+
* find out. Nobody adopts a healing tool before their tests break, so that
|
|
9
|
+
* ordering is the adoption problem, not the technology.
|
|
10
|
+
*
|
|
11
|
+
* `audit` answers the question you have the day you arrive — *does my suite still
|
|
12
|
+
* name things that exist?* — from a single capture. It cross-references the
|
|
13
|
+
* locator inventory (`ingest`) against the current contract (`map`): a
|
|
14
|
+
* `getByRole('button', { name: 'Save' })` that matches no button on any mapped
|
|
15
|
+
* page cannot pass, and saying so needs no history at all.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately conservative, and deliberately advisory by default:
|
|
18
|
+
*
|
|
19
|
+
* - Only **role-anchored** locators carry the verdict. `extractInPage` maps
|
|
20
|
+
* interactive elements, so `getByText('Terms of service')` may well point at a
|
|
21
|
+
* paragraph the contract never saw — its absence is not evidence, and
|
|
22
|
+
* reporting it would be a false positive. Those are counted, never judged.
|
|
23
|
+
* - Exit 0 unless `--strict`. The mapping covers the pages in `config.pages`, not
|
|
24
|
+
* necessarily the pages the suite visits; a locator that is "missing" may just
|
|
25
|
+
* live on an unmapped page. Gate on this once coverage is real — the header
|
|
26
|
+
* says how many pages that is.
|
|
27
|
+
*
|
|
28
|
+
* Local end to end: reads `.ia-qa/`, reads your test files, calls nothing.
|
|
29
|
+
*/
|
|
30
|
+
interface DeadLocator {
|
|
31
|
+
/** As written in the test file, not as normalized for lookup. */
|
|
32
|
+
name: string;
|
|
33
|
+
roles: string[] | null;
|
|
34
|
+
sites: NameSite[];
|
|
35
|
+
suggestion?: {
|
|
36
|
+
name: string;
|
|
37
|
+
role: string;
|
|
38
|
+
score: number;
|
|
39
|
+
pages: string[];
|
|
40
|
+
kind: 'fuzzy' | 'extended';
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
interface MappedElement {
|
|
44
|
+
role: string;
|
|
45
|
+
name: string;
|
|
46
|
+
selector: string;
|
|
47
|
+
page: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Does `haystack` contain `needle` on word boundaries? "save" is inside "save
|
|
51
|
+
* changes" but not inside "saved searches" — substring alone would claim both.
|
|
52
|
+
*/
|
|
53
|
+
export declare function containsAsWords(haystack: string, needle: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Which inventoried names resolve to nothing the contract holds?
|
|
56
|
+
*
|
|
57
|
+
* A name is alive as soon as ONE mapped page answers it: the audit has no way of
|
|
58
|
+
* knowing which page a given test visits, so "found somewhere" is the only
|
|
59
|
+
* reading that cannot manufacture a false positive.
|
|
60
|
+
*/
|
|
61
|
+
export declare function findDeadLocators(usage: Usage, elements: MappedElement[]): DeadLocator[];
|
|
62
|
+
export declare function runAudit(args: string[]): Promise<void>;
|
|
63
|
+
export {};
|