@ia-qa/self-healing 1.19.1 → 1.23.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 +55 -0
- package/TUTORIAL.md +22 -0
- package/dist/aom.d.ts +20 -0
- package/dist/aom.js +25 -0
- package/dist/aom.js.map +1 -1
- package/dist/browser/resolve.js +4 -1
- package/dist/browser/resolve.js.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/diff.d.ts +62 -0
- package/dist/cli/diff.js +104 -4
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/explain.js +57 -12
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/fix.js +2 -1
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/map.d.ts +2 -0
- package/dist/cli/map.js +13 -0
- package/dist/cli/map.js.map +1 -1
- package/dist/cli-ai/index.js +2 -1
- package/dist/cli-ai/index.js.map +1 -1
- package/dist/explain.d.ts +78 -3
- package/dist/explain.js +87 -9
- package/dist/explain.js.map +1 -1
- package/dist/htmlReport.js +39 -0
- package/dist/htmlReport.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +30 -0
- package/dist/ingest.js +46 -1
- package/dist/ingest.js.map +1 -1
- package/dist/junit.js +41 -0
- package/dist/junit.js.map +1 -1
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/redirectFix.d.ts +60 -0
- package/dist/redirectFix.js +73 -0
- package/dist/redirectFix.js.map +1 -0
- package/dist/resolution.d.ts +41 -4
- package/dist/resolution.js +66 -5
- package/dist/resolution.js.map +1 -1
- package/dist/routeDrift.d.ts +105 -0
- package/dist/routeDrift.js +96 -0
- package/dist/routeDrift.js.map +1 -0
- package/dist/ui/server.js +5 -6
- package/dist/ui/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/ia-qa-heal/SKILL.md +22 -0
package/README.md
CHANGED
|
@@ -422,8 +422,63 @@ rewrites the literal in your files. Four outcomes, one repaired automatically:
|
|
|
422
422
|
| reaches **nothing**, and its element is still there | 🔧 rewritten |
|
|
423
423
|
| reaches a **different** element | ⛔ never rewritten — your test passes and acts on the wrong thing |
|
|
424
424
|
| reaches **several** elements | ⛔ a coin flip |
|
|
425
|
+
| reached **several** and now reaches **none** | ⛔ the candidates are named, none is chosen for you |
|
|
425
426
|
| its element is **gone** | ⛔ nothing to rewrite towards |
|
|
426
427
|
|
|
428
|
+
That fourth row is the one a route migration produces — every `a[href="/index.php/cart"]`
|
|
429
|
+
becoming `a[href="/cart"]` in one commit. The string used to match two links and now matches
|
|
430
|
+
neither, so your suite is red; what it could have meant is still on the page and is listed with
|
|
431
|
+
its new address. No rewrite is offered, and that is not timidity: the binding was already
|
|
432
|
+
ambiguous, so which of the two your test meant was never established. Picking one would be a
|
|
433
|
+
coin flip wearing a fix's clothes.
|
|
434
|
+
|
|
435
|
+
### A link that leads somewhere else
|
|
436
|
+
|
|
437
|
+
`href` is a **destination**, not an address. Because `stableSelector` prefers `a[href="…"]`,
|
|
438
|
+
a link whose route moved shows up as a moved selector — reported, until now, on the same line
|
|
439
|
+
as a button that slid one slot down the DOM:
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
🧭 2 links now lead somewhere else (the element is the same — its destination is not):
|
|
443
|
+
• Write a contract in the browser → [home]
|
|
444
|
+
/devtools/eval-contract-runner → /qatools/eval-contract
|
|
445
|
+
• ? [mcp-server]
|
|
446
|
+
/devtools/eval-contract-runner → /qatools/eval-contract
|
|
447
|
+
Not failing the run — no test navigates to these. Check the ones you reach by URL.
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
It **does not fail your run on its own**: an app is allowed to reorganise its URLs, and a gate
|
|
451
|
+
that goes red on that is a gate someone switches off. It fails only when your suite navigates
|
|
452
|
+
to the route that moved — `ingest` now inventories `page.goto`, `cy.visit`, `driver.get` and
|
|
453
|
+
`browser.url`, so that case arrives with its `file:line`. No `ingest`, no escalation.
|
|
454
|
+
|
|
455
|
+
Nothing here is ever rewritten. A `page.goto` is not a locator, and the new route may be the
|
|
456
|
+
same page renamed or a different page entirely — you can tell in a second from two URLs, and
|
|
457
|
+
the tool cannot tell at all.
|
|
458
|
+
|
|
459
|
+
Measured across two real captures of a 142-page app: **2 findings over 4 563 named links**, and
|
|
460
|
+
zero on a second corpus. Details, including the false positives the first measurement produced,
|
|
461
|
+
are in `SPEC-route-drift.md`.
|
|
462
|
+
|
|
463
|
+
### When a page you declared has moved
|
|
464
|
+
|
|
465
|
+
`map` already notices that the browser landed somewhere other than the URL in `config.json`.
|
|
466
|
+
It now offers the correction, and `--fix-redirects` writes it:
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
↪️ 1 declared page is no longer at the URL config.json declares:
|
|
470
|
+
articles: /articles → /resources
|
|
471
|
+
Run `ia-qa-heal map --fix-redirects` to write these, or edit config.json yourself.
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Left uncorrected, the contract for `/resources` is written under the name `articles`, which is
|
|
475
|
+
the same wrong-identity problem two apps sharing a `/login` produce.
|
|
476
|
+
|
|
477
|
+
**It refuses when several declared pages land on the same place.** That is not a migration —
|
|
478
|
+
it is an expired session, a redirect to sign-in, or an outage, and applying it would rewrite
|
|
479
|
+
your whole page list to point at the login screen. The group is named and skipped, a genuine
|
|
480
|
+
single move in the same run still applies, and the message sends you to `session --check`.
|
|
481
|
+
|
|
427
482
|
**Your app may already carry the answer.** The contract holds interactive elements only, so a
|
|
428
483
|
selector on a status div, a cell or an alert is not in it. Usually that stays quiet — such an
|
|
429
484
|
element has no identity independent of what drifts, so nothing could be repaired anyway. But
|
package/TUTORIAL.md
CHANGED
|
@@ -558,6 +558,28 @@ the `file:line` the error message never carried.
|
|
|
558
558
|
> **cannot tell which locator the failure is about**, that's not a pass — usually the failing
|
|
559
559
|
> file was never `ingest`ed, or the locator is built at runtime from a variable.
|
|
560
560
|
|
|
561
|
+
> 🧭 **A locator can be fine and the page still wrong.** If a link's *destination* moved —
|
|
562
|
+
> `/devtools/eval-contract-runner` → `/qatools/eval-contract` — the element is untouched, so
|
|
563
|
+
> every locator check passes. `explain` used to answer PASS there, which is true and sends you
|
|
564
|
+
> to look at timing and test data instead of at the route. It now intersects the failure text
|
|
565
|
+
> with the URLs your suite navigates to and answers **BLOCK**, naming both routes and the line
|
|
566
|
+
> that goes to the old one. `diff` reports the same thing under *"N links now lead somewhere else"*, with
|
|
567
|
+
> the page each is on. It does not fail your run unless your suite navigates to the old route,
|
|
568
|
+
> which it knows because `ingest` inventories `page.goto`, `cy.visit`, `driver.get` and
|
|
569
|
+
> `browser.url`. Nothing there is ever rewritten: only you can say whether the new route is the
|
|
570
|
+
> same page renamed or a different page. And when it is a page *you declared* that moved,
|
|
571
|
+
> `ia-qa-heal map --fix-redirects` updates `config.json` — refusing if several pages landed in
|
|
572
|
+
> the same place, because that is an expired session, not a migration.
|
|
573
|
+
|
|
574
|
+
> 🐛 **Fixed in 1.20.0, and worth knowing if you are on an older version.** `explain` used to
|
|
575
|
+
> answer *"this string reached nothing at either moment — it is not drift"* about locators that
|
|
576
|
+
> had reached something at baseline. It read only the current capture and phrased the verdict as
|
|
577
|
+
> if it had read both. That is the worst place in this tool for a false negative: "not drift" is
|
|
578
|
+
> the sentence that sends you away from the selectors and off to look at timing and test data.
|
|
579
|
+
> It now reads `baseline/_resolved/` too, and a locator that resolved then and resolves no longer
|
|
580
|
+
> comes back as drift, with `never-bound` kept for what it always meant — probed at both moments,
|
|
581
|
+
> reached nothing at either.
|
|
582
|
+
|
|
561
583
|
Read-only: it opens no browser, re-runs nothing, and edits nothing. Repairs still go through
|
|
562
584
|
`fix`, below.
|
|
563
585
|
|
package/dist/aom.d.ts
CHANGED
|
@@ -132,5 +132,25 @@ export declare function depthMismatchMessage(m: {
|
|
|
132
132
|
current: number;
|
|
133
133
|
}): string;
|
|
134
134
|
export declare function extractInteractiveElements(page: Page): Promise<MappedElement[]>;
|
|
135
|
+
/**
|
|
136
|
+
* The `.json` files in a contract directory that are **contracts**.
|
|
137
|
+
*
|
|
138
|
+
* A leading underscore is how this codebase spells "not a page" — `_exercised.json`,
|
|
139
|
+
* `_resolved/`, `_layouts/`. The rule was written down as *"every consumer that enumerates
|
|
140
|
+
* contracts already filters it out"*, and it was true of three of them and false of two.
|
|
141
|
+
*
|
|
142
|
+
* What that cost, reproduced end to end: `run` writes `mapping/_exercised.json`, `baseline`
|
|
143
|
+
* promotes it (this directory is copied wholesale, which is right — the record belongs to the
|
|
144
|
+
* moment), and then **`diff --dir` and `fix --dir` both read it as a page named `_exercised`**,
|
|
145
|
+
* fail on `has no "elements" array`, put it in `unreadable` and refuse the entire comparison
|
|
146
|
+
* with exit 2. The remedy they printed — *restore the file from git, or re-capture it* — could
|
|
147
|
+
* not work: re-capturing writes the same file again. A tool breaking its own documented loop
|
|
148
|
+
* with a file it wrote itself, and telling the user to fix it by hand.
|
|
149
|
+
*
|
|
150
|
+
* So the rule lives here now, in the module that knows what a contract is, and the five call
|
|
151
|
+
* sites share it. Three of them already agreed; the point is that agreeing by coincidence is
|
|
152
|
+
* what produced this.
|
|
153
|
+
*/
|
|
154
|
+
export declare function contractFilesIn(dir: string): string[];
|
|
135
155
|
export declare function saveMapping(mapping: PageMapping, dir?: string): string;
|
|
136
156
|
export declare function loadMapping(pageName: string, dir?: string): PageMapping;
|
package/dist/aom.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.isSameCapture = isSameCapture;
|
|
|
37
37
|
exports.depthMismatch = depthMismatch;
|
|
38
38
|
exports.depthMismatchMessage = depthMismatchMessage;
|
|
39
39
|
exports.extractInteractiveElements = extractInteractiveElements;
|
|
40
|
+
exports.contractFilesIn = contractFilesIn;
|
|
40
41
|
exports.saveMapping = saveMapping;
|
|
41
42
|
exports.loadMapping = loadMapping;
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
@@ -112,6 +113,30 @@ function depthMismatchMessage(m) {
|
|
|
112
113
|
async function extractInteractiveElements(page) {
|
|
113
114
|
return page.evaluate(extract_1.extractInPage);
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* The `.json` files in a contract directory that are **contracts**.
|
|
118
|
+
*
|
|
119
|
+
* A leading underscore is how this codebase spells "not a page" — `_exercised.json`,
|
|
120
|
+
* `_resolved/`, `_layouts/`. The rule was written down as *"every consumer that enumerates
|
|
121
|
+
* contracts already filters it out"*, and it was true of three of them and false of two.
|
|
122
|
+
*
|
|
123
|
+
* What that cost, reproduced end to end: `run` writes `mapping/_exercised.json`, `baseline`
|
|
124
|
+
* promotes it (this directory is copied wholesale, which is right — the record belongs to the
|
|
125
|
+
* moment), and then **`diff --dir` and `fix --dir` both read it as a page named `_exercised`**,
|
|
126
|
+
* fail on `has no "elements" array`, put it in `unreadable` and refuse the entire comparison
|
|
127
|
+
* with exit 2. The remedy they printed — *restore the file from git, or re-capture it* — could
|
|
128
|
+
* not work: re-capturing writes the same file again. A tool breaking its own documented loop
|
|
129
|
+
* with a file it wrote itself, and telling the user to fix it by hand.
|
|
130
|
+
*
|
|
131
|
+
* So the rule lives here now, in the module that knows what a contract is, and the five call
|
|
132
|
+
* sites share it. Three of them already agreed; the point is that agreeing by coincidence is
|
|
133
|
+
* what produced this.
|
|
134
|
+
*/
|
|
135
|
+
function contractFilesIn(dir) {
|
|
136
|
+
if (!fs.existsSync(dir))
|
|
137
|
+
return [];
|
|
138
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('.') && !f.startsWith('_'));
|
|
139
|
+
}
|
|
115
140
|
function saveMapping(mapping, dir = (0, config_1.mappingDir)()) {
|
|
116
141
|
fs.mkdirSync(dir, { recursive: true });
|
|
117
142
|
const file = (0, config_1.mappingPath)(mapping.page, dir);
|
package/dist/aom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GA,sCAKC;AAkBD,sCAOC;AAGD,oDAsBC;AAED,gEAEC;AAED,kCAwBC;AAED,kCAQC;
|
|
1
|
+
{"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GA,sCAKC;AAkBD,sCAOC;AAGD,oDAsBC;AAED,gEAEC;AAqBD,0CAGC;AAED,kCAwBC;AAED,kCAQC;AAlOD,uCAAyB;AACzB,2CAA6B;AAE7B,qCAAmD;AACnD,+CAAkD;AAoFlD;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,aAAa,CAC3B,QAAoD,EACpD,OAAmD;IAEnD,OAAO,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,QAAS,CAAC,UAAU,KAAK,OAAO,EAAE,UAAU,CAAC;AACvF,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,aAAa,CAC3B,QAAuD,EACvD,OAAsD;IAEtD,MAAM,CAAC,GAAG,QAAQ,EAAE,aAAa,IAAI,CAAC,CAAC;IACvC,MAAM,CAAC,GAAG,OAAO,EAAE,aAAa,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AACtD,CAAC;AAED,wFAAwF;AACxF,SAAgB,oBAAoB,CAAC,CAAwC;IAC3E,MAAM,cAAc,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC;IAC9C,MAAM,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,iBAAiB,CAAC;IAC/D,MAAM,SAAS,GAAG,cAAc,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC;IAClE,MAAM,IAAI,GACR,oCAAoC,MAAM,0CAA0C,SAAS,GAAG;QAChG,kBAAkB,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,OAAO,+CAA+C;QAC3F,iDAAiD,CAAC;IACpD,qFAAqF;IACrF,yFAAyF;IACzF,wFAAwF;IACxF,wFAAwF;IACxF,uFAAuF;IACvF,8DAA8D;IAC9D,OAAO,cAAc;QACnB,CAAC,CAAC,IAAI;YACF,wFAAwF;YACxF,uFAAuF;YACvF,mBAAmB;QACvB,CAAC,CAAC,IAAI;YACF,2FAA2F;YAC3F,8EAA8E,CAAC;AACvF,CAAC;AAEM,KAAK,UAAU,0BAA0B,CAAC,IAAU;IACzD,OAAO,IAAI,CAAC,QAAQ,CAAC,uBAAa,CAA6B,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,eAAe,CAAC,GAAW;IACzC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AAC5G,CAAC;AAED,SAAgB,WAAW,CAAC,OAAoB,EAAE,MAAc,IAAA,mBAAU,GAAE;IAC1E,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAA,oBAAW,EAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC5C,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgB,CAAC;QACtE,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC;YAC9D,OAAO,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,8BAA8B,IAAI,CAAC,UAAU,iBAAiB,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACxD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAExC,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACxC,IAAI,KAAK,KAAK,GAAG,OAAO,CAAC,IAAI,OAAO;YAAE,SAAS;QAC/C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAChE,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,OAAO,EAAE,CAAC;gBAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBAC3C,OAAO,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,wBAAwB,KAAK,oCAAoC,CAAC,CAAC;YACpG,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,WAAW,CAAC,QAAgB,EAAE,MAAc,IAAA,mBAAU,GAAE;IACtE,MAAM,IAAI,GAAG,IAAA,oBAAW,EAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,8BAA8B,QAAQ,MAAM,IAAI,2BAA2B,QAAQ,WAAW,CAC/F,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgB,CAAC;AAClE,CAAC"}
|
package/dist/browser/resolve.js
CHANGED
|
@@ -157,7 +157,10 @@ function resolveSelectors(args) {
|
|
|
157
157
|
out[pom] = anchor ? { status: 'anchored', selector: anchor } : { status: 'outside' };
|
|
158
158
|
}
|
|
159
159
|
else {
|
|
160
|
-
|
|
160
|
+
// `mapped` is every contracted element this string could have meant. Recording it is
|
|
161
|
+
// what lets the next capture ask whether they survived: without it an ambiguous
|
|
162
|
+
// binding is a dead end, and the drift it hides is invisible to every other check.
|
|
163
|
+
out[pom] = { status: 'ambiguous', count: nodes.length, selectors: mapped };
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
return out;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/browser/resolve.js"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;;AAEH,
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/browser/resolve.js"],"names":[],"mappings":"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"}
|
package/dist/cli/args.js
CHANGED
|
@@ -20,7 +20,7 @@ exports.KNOWN_FLAGS = {
|
|
|
20
20
|
login: ['--url', '--session'],
|
|
21
21
|
secret: ['--json'],
|
|
22
22
|
session: ['--check', '--offline', '--url', '--session', '--json'],
|
|
23
|
-
map: ['--screenshots', '--session', '--deep', '--depth', '--deep-budget'],
|
|
23
|
+
map: ['--screenshots', '--session', '--deep', '--depth', '--deep-budget', '--fix-redirects'],
|
|
24
24
|
baseline: ['--dry-run'],
|
|
25
25
|
diff: ['--strict', '--json', '--junit', '--dir', '--report', '--open'],
|
|
26
26
|
fix: ['--dry-run', '--locators', '--dir', '--json'],
|
package/dist/cli/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AAkCA,gDAeC;AAjDD;;;;;;;;;;;GAWG;AACU,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC;IAClG,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,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC;IACjE,GAAG,EAAE,CAAC,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;AAkCA,gDAeC;AAjDD;;;;;;;;;;;GAWG;AACU,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC;IAClG,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,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC;IACjE,GAAG,EAAE,CAAC,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,CAAC;IAC5F,QAAQ,EAAE,CAAC,WAAW,CAAC;IACvB,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IACtE,GAAG,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC;IACnD,MAAM,EAAE,CAAC,QAAQ,CAAC;IAClB,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC7B,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC;IACvD,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,YAAY,EAAE,CAAC,QAAQ,CAAC;IACxB,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/diff.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { PageMapping } from '../aom';
|
|
|
2
2
|
import { MaskOutcome } from '../nameMask';
|
|
3
3
|
import { BindingDrift } from '../resolution';
|
|
4
4
|
import { Usage } from '../ingest';
|
|
5
|
+
import { RouteChange } from '../routeDrift';
|
|
5
6
|
import { NameDriftFinding } from '../nameDrift';
|
|
6
7
|
import { FixOutcome, ReportEvidence } from '../htmlReport';
|
|
7
8
|
import { SystemicOutcome } from '../systemicDelta';
|
|
@@ -284,6 +285,12 @@ export interface DirReport {
|
|
|
284
285
|
nameDrift?: NameDriftFinding[];
|
|
285
286
|
/** `config.nameMask`'s effect across the walk — applied patterns and refusals, unioned. */
|
|
286
287
|
nameMask?: MaskOutcome;
|
|
288
|
+
/**
|
|
289
|
+
* Links whose **destination** changed while their identity held. Reported beside the
|
|
290
|
+
* `healable` row, never instead of it: the row's promise holds at the element level and
|
|
291
|
+
* breaks at the navigation level. Absent when nothing moved. See SPEC-route-drift.md.
|
|
292
|
+
*/
|
|
293
|
+
routeDrift?: RouteChange[];
|
|
287
294
|
/**
|
|
288
295
|
* What to run next, or `null` when the next step is not a command this caller should
|
|
289
296
|
* run. See `nextStep.ts` — BLOCK is deliberately null.
|
|
@@ -387,6 +394,12 @@ export interface Report {
|
|
|
387
394
|
* refused with the reason. Absent when no mask is configured.
|
|
388
395
|
*/
|
|
389
396
|
nameMask?: MaskOutcome;
|
|
397
|
+
/**
|
|
398
|
+
* Links whose **destination** changed while their identity held. Reported beside the
|
|
399
|
+
* `healable` row, never instead of it: the row's promise holds at the element level and
|
|
400
|
+
* breaks at the navigation level. Absent when nothing moved. See SPEC-route-drift.md.
|
|
401
|
+
*/
|
|
402
|
+
routeDrift?: RouteChange[];
|
|
390
403
|
/**
|
|
391
404
|
* Selector strings from the suite whose binding to a contracted element broke.
|
|
392
405
|
* Present only once `ingest` has inventoried the tests **and** the capture has
|
|
@@ -420,3 +433,52 @@ export interface BindingCoverage {
|
|
|
420
433
|
inventoried: number;
|
|
421
434
|
side: 'baseline' | 'current' | 'both';
|
|
422
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Re-attach link destinations from the baseline elements onto the diff rows.
|
|
438
|
+
* Done here rather than in `match.js`: href is not element identity (it never
|
|
439
|
+
* feeds matching), and `browser/` ships twice — once on npm, once served by the
|
|
440
|
+
* web tool — so it stays dependency-free and untouched.
|
|
441
|
+
*/
|
|
442
|
+
/**
|
|
443
|
+
* A link that now goes somewhere else — computed once, applied at every door.
|
|
444
|
+
*
|
|
445
|
+
* Sits beside `attachBaselineHrefs` because it is the same fact about the same field, and
|
|
446
|
+
* outside `match.js` for the same stated reason: href is not identity, and that file ships
|
|
447
|
+
* three times. See `SPEC-route-drift.md`.
|
|
448
|
+
*
|
|
449
|
+
* It raises the verdict **only** when the suite navigates to the old route. An app is allowed
|
|
450
|
+
* to reorganise its URLs; a gate that goes red on that is a gate someone switches off. With
|
|
451
|
+
* `usage.routes` absent — an old inventory, or a project that never ran `ingest` — nothing
|
|
452
|
+
* escalates and the report is byte-identical to before.
|
|
453
|
+
*/
|
|
454
|
+
export declare function attachRouteDrift(report: Report, baseline: PageMapping, current: PageMapping, usage: Usage | null): Report;
|
|
455
|
+
/**
|
|
456
|
+
* What happened to the selector strings the suite actually writes.
|
|
457
|
+
*
|
|
458
|
+
* This is the half a CSS-anchored Page Object suite could never see: the contract stores
|
|
459
|
+
* a synthesized selector, the suite writes its own, and until the two are bound every
|
|
460
|
+
* drift row reads `not referenced by your tests`. Printed with the test sites so the
|
|
461
|
+
* reader goes straight to the line, exactly like the name-drift findings.
|
|
462
|
+
*
|
|
463
|
+
* Never prints what did not break. A Page Object is mostly selectors for rows, cells and
|
|
464
|
+
* containers — real elements the contract does not hold — and listing those as findings
|
|
465
|
+
* is the wall of false positives this whole feature is designed around.
|
|
466
|
+
*/
|
|
467
|
+
/**
|
|
468
|
+
* Exported for its test only. Two rows here carry `status: 'ambiguous'` and describe
|
|
469
|
+
* opposite facts, and the sentence that tells them apart is derived rather than stored —
|
|
470
|
+
* the kind of thing that can be wrong for a release without anyone noticing, because both
|
|
471
|
+
* outputs look plausible.
|
|
472
|
+
*/
|
|
473
|
+
export declare function bindingDriftNote(drift: BindingDrift[] | undefined, usage: Usage | null): void;
|
|
474
|
+
/**
|
|
475
|
+
* Links that now lead somewhere else.
|
|
476
|
+
*
|
|
477
|
+
* Its own section, and that is the whole feature: these lines were already in the report,
|
|
478
|
+
* rendered as `🔧 drifted` beside a button that slid down the DOM. Same icon, same list, and
|
|
479
|
+
* one of them means the user ends up on a different page.
|
|
480
|
+
*
|
|
481
|
+
* Exported for its test: the sentence is derived, and a wrong one here is well-formed prose
|
|
482
|
+
* describing a fact that did not happen.
|
|
483
|
+
*/
|
|
484
|
+
export declare function routeDriftNote(changes: RouteChange[] | undefined): void;
|
package/dist/cli/diff.js
CHANGED
|
@@ -57,6 +57,9 @@ exports.dirDiffAggregate = dirDiffAggregate;
|
|
|
57
57
|
exports.aggregateReports = aggregateReports;
|
|
58
58
|
exports.printAggregate = printAggregate;
|
|
59
59
|
exports.readMapping = readMapping;
|
|
60
|
+
exports.attachRouteDrift = attachRouteDrift;
|
|
61
|
+
exports.bindingDriftNote = bindingDriftNote;
|
|
62
|
+
exports.routeDriftNote = routeDriftNote;
|
|
60
63
|
const fs = __importStar(require("fs"));
|
|
61
64
|
const path = __importStar(require("path"));
|
|
62
65
|
const aom_1 = require("../aom");
|
|
@@ -67,6 +70,7 @@ const config_2 = require("../config");
|
|
|
67
70
|
const ingest_1 = require("../ingest");
|
|
68
71
|
const captureMerge_1 = require("../captureMerge");
|
|
69
72
|
const nameHint_1 = require("../nameHint");
|
|
73
|
+
const routeDrift_1 = require("../routeDrift");
|
|
70
74
|
const nameDrift_1 = require("../nameDrift");
|
|
71
75
|
const htmlReport_1 = require("../htmlReport");
|
|
72
76
|
const evidence_1 = require("../evidence");
|
|
@@ -285,7 +289,7 @@ function isDirectory(p) {
|
|
|
285
289
|
}
|
|
286
290
|
function capturedAtOf(dir) {
|
|
287
291
|
try {
|
|
288
|
-
const file =
|
|
292
|
+
const file = (0, aom_1.contractFilesIn)(dir)[0];
|
|
289
293
|
if (!file)
|
|
290
294
|
return undefined;
|
|
291
295
|
const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
@@ -402,6 +406,7 @@ async function runDiff(args) {
|
|
|
402
406
|
// raised here rather than inside the shared matcher.
|
|
403
407
|
report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
|
|
404
408
|
report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
|
|
409
|
+
attachRouteDrift(report, baseline, current, usage);
|
|
405
410
|
if (json) {
|
|
406
411
|
console.log(JSON.stringify(report, null, 2));
|
|
407
412
|
}
|
|
@@ -921,6 +926,7 @@ opts = {}) {
|
|
|
921
926
|
const report = attachBaselineHrefs((0, nameMask_1.diffWithMasks)(baseline, current, masks, usage), baseline);
|
|
922
927
|
report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
|
|
923
928
|
report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
|
|
929
|
+
attachRouteDrift(report, baseline, current, usage);
|
|
924
930
|
return attachBindingDrift(report, baselineFile, currentFile, page, baseline, current);
|
|
925
931
|
};
|
|
926
932
|
const layoutsDir = path.join(baselineDir, '_layouts');
|
|
@@ -950,8 +956,7 @@ opts = {}) {
|
|
|
950
956
|
}
|
|
951
957
|
}
|
|
952
958
|
}
|
|
953
|
-
const baselineFiles =
|
|
954
|
-
.filter((f) => f.endsWith('.json') && !f.startsWith('.'));
|
|
959
|
+
const baselineFiles = (0, aom_1.contractFilesIn)(baselineDir);
|
|
955
960
|
const scope = opts.pages ? new Set(opts.pages) : null;
|
|
956
961
|
const outOfScope = [];
|
|
957
962
|
for (const file of baselineFiles) {
|
|
@@ -1060,6 +1065,9 @@ function aggregateReports(reports) {
|
|
|
1060
1065
|
// inherits the raise for free — this only collects the findings for reporting.
|
|
1061
1066
|
const nameDrift = (0, nameDrift_1.mergeFindings)(reports.map((r) => r.report.nameDrift ?? []));
|
|
1062
1067
|
const bindingDrift = reports.flatMap((r) => r.report.bindingDrift ?? []);
|
|
1068
|
+
// Collected like the rest: a finding that exists per page and vanishes from the total is
|
|
1069
|
+
// a finding nobody sees, since --dir is how a whole app is judged.
|
|
1070
|
+
const routeChanges = reports.flatMap((r) => r.report.routeDrift ?? []);
|
|
1063
1071
|
const nameMask = (0, nameMask_1.mergeMaskOutcomes)(reports.map((r) => r.report.nameMask));
|
|
1064
1072
|
const depthUnmeasured = (0, depthDelta_1.mergeDepthUnmeasured)(reports.map((r) => r.report.depthUnmeasured));
|
|
1065
1073
|
// One fact about the run, not one per page: every page shares the same _resolved/ dir.
|
|
@@ -1068,6 +1076,7 @@ function aggregateReports(reports) {
|
|
|
1068
1076
|
verdict,
|
|
1069
1077
|
...(nameDrift.length > 0 ? { nameDrift } : {}),
|
|
1070
1078
|
...(bindingDrift.length > 0 ? { bindingDrift } : {}),
|
|
1079
|
+
...(routeChanges.length > 0 ? { routeDrift: routeChanges } : {}),
|
|
1071
1080
|
...(nameMask ? { nameMask } : {}),
|
|
1072
1081
|
...(depthUnmeasured ? { depthUnmeasured } : {}),
|
|
1073
1082
|
...(bindingCoverage ? { bindingCoverage } : {}),
|
|
@@ -1180,6 +1189,7 @@ function printAggregate(agg, reports, baselineDir, currentDir, usage) {
|
|
|
1180
1189
|
nameDriftNote(reports.flatMap((r) => r.report.rows));
|
|
1181
1190
|
}
|
|
1182
1191
|
bindingDriftNote(reports.flatMap((r) => r.report.bindingDrift ?? []), (0, ingest_1.loadUsage)());
|
|
1192
|
+
routeDriftNote(reports.flatMap((r) => r.report.routeDrift ?? []));
|
|
1183
1193
|
console.log('');
|
|
1184
1194
|
}
|
|
1185
1195
|
function statusIcon(status) {
|
|
@@ -1239,6 +1249,33 @@ function readMapping(file) {
|
|
|
1239
1249
|
* feeds matching), and `browser/` ships twice — once on npm, once served by the
|
|
1240
1250
|
* web tool — so it stays dependency-free and untouched.
|
|
1241
1251
|
*/
|
|
1252
|
+
/**
|
|
1253
|
+
* A link that now goes somewhere else — computed once, applied at every door.
|
|
1254
|
+
*
|
|
1255
|
+
* Sits beside `attachBaselineHrefs` because it is the same fact about the same field, and
|
|
1256
|
+
* outside `match.js` for the same stated reason: href is not identity, and that file ships
|
|
1257
|
+
* three times. See `SPEC-route-drift.md`.
|
|
1258
|
+
*
|
|
1259
|
+
* It raises the verdict **only** when the suite navigates to the old route. An app is allowed
|
|
1260
|
+
* to reorganise its URLs; a gate that goes red on that is a gate someone switches off. With
|
|
1261
|
+
* `usage.routes` absent — an old inventory, or a project that never ran `ingest` — nothing
|
|
1262
|
+
* escalates and the report is byte-identical to before.
|
|
1263
|
+
*/
|
|
1264
|
+
function attachRouteDrift(report, baseline, current, usage) {
|
|
1265
|
+
const changes = (0, routeDrift_1.routeDrift)({
|
|
1266
|
+
rows: report.rows,
|
|
1267
|
+
before: baseline.elements,
|
|
1268
|
+
after: current.elements,
|
|
1269
|
+
baseUrl: (0, config_2.configuredBaseUrl)() ?? undefined,
|
|
1270
|
+
usage,
|
|
1271
|
+
page: current.page || baseline.page,
|
|
1272
|
+
});
|
|
1273
|
+
if (changes.length > 0)
|
|
1274
|
+
report.routeDrift = changes;
|
|
1275
|
+
if ((0, routeDrift_1.routeDriftBlocks)(changes).length > 0)
|
|
1276
|
+
report.verdict = 'BLOCK';
|
|
1277
|
+
return report;
|
|
1278
|
+
}
|
|
1242
1279
|
function attachBaselineHrefs(report, baseline) {
|
|
1243
1280
|
const hrefBySelector = new Map(baseline.elements.filter((e) => e.href).map((e) => [e.selector, e.href]));
|
|
1244
1281
|
for (const row of report.rows) {
|
|
@@ -1291,6 +1328,12 @@ const MAX_HINT_GROUPS = 6;
|
|
|
1291
1328
|
* containers — real elements the contract does not hold — and listing those as findings
|
|
1292
1329
|
* is the wall of false positives this whole feature is designed around.
|
|
1293
1330
|
*/
|
|
1331
|
+
/**
|
|
1332
|
+
* Exported for its test only. Two rows here carry `status: 'ambiguous'` and describe
|
|
1333
|
+
* opposite facts, and the sentence that tells them apart is derived rather than stored —
|
|
1334
|
+
* the kind of thing that can be wrong for a release without anyone noticing, because both
|
|
1335
|
+
* outputs look plausible.
|
|
1336
|
+
*/
|
|
1294
1337
|
function bindingDriftNote(drift, usage) {
|
|
1295
1338
|
if (!drift || drift.length === 0)
|
|
1296
1339
|
return;
|
|
@@ -1313,17 +1356,73 @@ function bindingDriftNote(drift, usage) {
|
|
|
1313
1356
|
site(d, usage);
|
|
1314
1357
|
}
|
|
1315
1358
|
for (const d of rest) {
|
|
1359
|
+
// Two opposite facts share the word `ambiguous`, and the discriminator is structural:
|
|
1360
|
+
// a baseline that BOUND one element carries `was`, so the ambiguity is new (it now
|
|
1361
|
+
// matches several). Without `was` the ambiguity is the *baseline's* — it matched
|
|
1362
|
+
// several and now matches none. Printing the first sentence over the second case
|
|
1363
|
+
// would describe the exact opposite of what happened.
|
|
1364
|
+
const lostFromAmbiguous = d.status === 'ambiguous' && !d.was;
|
|
1316
1365
|
const why = d.status === 'gone'
|
|
1317
1366
|
? 'the element it named is gone — nothing to rewrite towards'
|
|
1318
1367
|
: d.status === 'dangling'
|
|
1319
1368
|
? 'it matched an element the contract does not hold, and now matches nothing —\n' +
|
|
1320
1369
|
' the test is red, and there is no recorded identity to rewrite towards'
|
|
1321
|
-
:
|
|
1370
|
+
: lostFromAmbiguous
|
|
1371
|
+
? 'it used to match several elements and now matches none — the test is red'
|
|
1372
|
+
: 'it now matches several elements — a coin flip, not a repair';
|
|
1322
1373
|
console.log(` ⛔ ${d.selector}${d.was ? ` (was ${d.was})` : ''}`);
|
|
1323
1374
|
console.log(` ${why}.`);
|
|
1375
|
+
if (lostFromAmbiguous) {
|
|
1376
|
+
// Named, never offered as a rewrite: the baseline never established which of them
|
|
1377
|
+
// the test meant, so choosing is the reader's job and the tool says so out loud.
|
|
1378
|
+
if (d.candidates && d.candidates.length > 0) {
|
|
1379
|
+
console.log(` what it could have meant is still on the page, now at:`);
|
|
1380
|
+
for (const c of d.candidates)
|
|
1381
|
+
console.log(` ${c}`);
|
|
1382
|
+
console.log(` pick the one your test meant — no rewrite is offered for a coin flip.`);
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1385
|
+
console.log(` none of what it could have meant is still on the page.`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1324
1388
|
site(d, usage);
|
|
1325
1389
|
}
|
|
1326
1390
|
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Links that now lead somewhere else.
|
|
1393
|
+
*
|
|
1394
|
+
* Its own section, and that is the whole feature: these lines were already in the report,
|
|
1395
|
+
* rendered as `🔧 drifted` beside a button that slid down the DOM. Same icon, same list, and
|
|
1396
|
+
* one of them means the user ends up on a different page.
|
|
1397
|
+
*
|
|
1398
|
+
* Exported for its test: the sentence is derived, and a wrong one here is well-formed prose
|
|
1399
|
+
* describing a fact that did not happen.
|
|
1400
|
+
*/
|
|
1401
|
+
function routeDriftNote(changes) {
|
|
1402
|
+
if (!changes || changes.length === 0)
|
|
1403
|
+
return;
|
|
1404
|
+
const n = changes.length;
|
|
1405
|
+
console.log(`\n 🧭 ${n} link${n === 1 ? '' : 's'} now lead${n === 1 ? 's' : ''} somewhere else ` +
|
|
1406
|
+
`(the element is the same — its destination is not):`);
|
|
1407
|
+
for (const c of changes) {
|
|
1408
|
+
const gating = (c.usedAt?.length ?? 0) > 0;
|
|
1409
|
+
console.log(` ${gating ? '⛔' : '•'} ${c.name || '(no name)'}${c.page ? ` [${c.page}]` : ''}`);
|
|
1410
|
+
console.log(` ${c.from} → ${c.to}`);
|
|
1411
|
+
if (gating) {
|
|
1412
|
+
// The only thing that makes this more than a remark: the suite goes to the old route,
|
|
1413
|
+
// so it is red now and the tool can prove it.
|
|
1414
|
+
const shown = c.usedAt.slice(0, 3).map((s) => `${s.file}:${s.line}`).join(', ');
|
|
1415
|
+
const more = c.usedAt.length > 3 ? ` (+${c.usedAt.length - 3} more)` : '';
|
|
1416
|
+
console.log(` your suite navigates to the old one: ${shown}${more}`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (!changes.some((c) => (c.usedAt?.length ?? 0) > 0)) {
|
|
1420
|
+
// Said out loud rather than implied by the absence of a line: an app is allowed to
|
|
1421
|
+
// reorganise its URLs, and a reader who cannot tell "not gating" from "not checked"
|
|
1422
|
+
// will treat the next one the same way.
|
|
1423
|
+
console.log(` Not failing the run — no test navigates to these. Check the ones you reach by URL.`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1327
1426
|
/** Where that string is written, so the reader opens the file instead of grepping. */
|
|
1328
1427
|
function site(d, usage) {
|
|
1329
1428
|
const sites = usage?.selectors?.[d.selector];
|
|
@@ -1427,6 +1526,7 @@ function printHuman(report, files, usage) {
|
|
|
1427
1526
|
nameDriftNote(rows);
|
|
1428
1527
|
}
|
|
1429
1528
|
bindingDriftNote(report.bindingDrift, usage ?? null);
|
|
1529
|
+
routeDriftNote(report.routeDrift);
|
|
1430
1530
|
if (verdict === 'PASS' && counts.renamed === 0) {
|
|
1431
1531
|
console.log(' ✅ No drift — your suite is safe to run.');
|
|
1432
1532
|
}
|