@ia-qa/qa-discovery 0.3.0 → 0.4.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 +27 -1
- package/ROADMAP.md +11 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/generate.d.ts +56 -0
- package/dist/cli/generate.js +293 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +19 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/coverage.d.ts +52 -2
- package/dist/coverage.js +67 -7
- package/dist/coverage.js.map +1 -1
- package/dist/coverageView.js +17 -1
- package/dist/coverageView.js.map +1 -1
- package/dist/generate.d.ts +62 -0
- package/dist/generate.js +163 -0
- package/dist/generate.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -243,7 +243,11 @@ The file it produces holds live cookies — whoever has it is logged in as you.
|
|
|
243
243
|
npx ia-qa-discover coverage
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
Reads local files only — no browser, no network, no argument. It compares the pages this scan found against the pages your suite was **observed** visiting, and ranks the gap by what each page takes as input: credentials, form submissions, required fields
|
|
246
|
+
Reads local files only — no browser, no network, no argument. It compares the pages this scan found against the pages your suite was **observed** visiting, and ranks the gap by what each page takes as input: credentials, form submissions, required fields. The factors *are* the ranking — there is no score, because a single number invites tuning and hides that a page ranks high for a reason you may consider irrelevant.
|
|
247
|
+
|
|
248
|
+
One factor comes from the other package: **how often a page has actually changed**, read from healing's run history. A page that changes every week and that no test visits is where bugs are born; a page untouched for six months does not need a new test — and no scan of an app can see the difference. Runs where more than half the mapped pages drifted at once are held out and counted: that describes a stale baseline or a changed capture, not volatile pages, and including them made every page carry the factor, which ranks nothing. Below three comparable runs the factor is simply absent, and the report says why.
|
|
249
|
+
|
|
250
|
+
How much a page *holds* — its field and call counts — is shown beside the factors and never counted with them. Describing a page and ranking it are two jobs: merged, `1 input · 1 API call` out-ranked a page that had actually changed three times.
|
|
247
251
|
|
|
248
252
|
It also reports **depth**: how many of each visited page's contracted elements your tests actually name. Named is not asserted — a test that clicks a button names it without checking anything, and nothing on disk can tell the two apart, so this never says "tested".
|
|
249
253
|
|
|
@@ -261,6 +265,28 @@ Writes `.ia-qa-discovery/coverage-map.json` (committable, diffable) and refreshe
|
|
|
261
265
|
|
|
262
266
|
They stay **counted and named** as out of scope, never silently dropped, and nothing is ever added to that list on its own.
|
|
263
267
|
|
|
268
|
+
### `generate`
|
|
269
|
+
|
|
270
|
+
**A starting suite for the pages nothing tests — written so healing can repair it.**
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
npx ia-qa-discover generate
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Writing test code is not the scarce thing; `playwright codegen` has done it for years. What it cannot do is decide *what* to record, and what nobody does is make the result survive the app changing. With no argument this generates one spec per page in the coverage gap — the pages that exist and that no test visits — and every locator it writes names an element the healing contract holds. So when a label changes, the file is **rewritten** instead of going red.
|
|
277
|
+
|
|
278
|
+
That loop is not a claim: it is executed on every commit, on this verb's own output — generated, drifted, repaired, resolving again, with no human edit.
|
|
279
|
+
|
|
280
|
+
**What it refuses to write, and why that is the feature.** Healing rewrites calls that state a role — `getByRole`, `getByLabel`, `getByPlaceholder`, `By.linkText` — and nothing else. `getByText` names a *string*, not an element, so nothing can prove the test meant the renamed button rather than a heading that never moved. And one such line does not merely stay unrepaired: it holds the verdict at BLOCK, which stops the repairable lines *beside* it from being repaired too. So a field that cannot be named by role, label or placeholder is left out, never written in a weaker form.
|
|
281
|
+
|
|
282
|
+
Left out for the same reason: a field with no test id, id or name. Its only selector is positional, and it addresses a different element the moment a field is inserted above it — untestable by anyone, not only by this tool. Each omission is named at the top of the file it was omitted from, because a silent gap reads as a finished test.
|
|
283
|
+
|
|
284
|
+
**The assertions are yours.** An application declares what it *accepts* — required, type, min, max — and never what it *promises*. "A refused card shows the right message" lives in someone's head, so the navigation and the actions are generated and what the app should *do* is a TODO with the question written out.
|
|
285
|
+
|
|
286
|
+
Files land in `.ia-qa-discovery/generated/` and nothing is added to your suite. An existing file is never replaced without `--force`. If the output directory is outside healing's `testPaths`, the verb says so and names both remedies — out of reach means never repaired, which would quietly make the whole promise false.
|
|
287
|
+
|
|
288
|
+
`--out <dir>` to write them where your suite lives, `--json` for a machine-readable summary, or name pages explicitly to generate for them anyway.
|
|
289
|
+
|
|
264
290
|
### `history`
|
|
265
291
|
|
|
266
292
|
The trend no single scan can reconstruct: pages, forms, API surface, and what changed run over run.
|
package/ROADMAP.md
CHANGED
|
@@ -245,6 +245,17 @@ by the classifier's intent", so the first half is not blocked on the second.
|
|
|
245
245
|
|
|
246
246
|
## Scenario generation (F3)
|
|
247
247
|
|
|
248
|
+
**Shipped, narrowly: `ia-qa-discover generate`.** Not scenario generation — *spec scaffolding
|
|
249
|
+
from the observed surface*. It writes one Playwright file per uncovered page, naming only
|
|
250
|
+
elements the healing contract holds, so the file is repaired rather than deleted when the app
|
|
251
|
+
moves; that loop runs on every commit against the verb's own output. It emits only calls
|
|
252
|
+
healing can rewrite (one `getByText` holds the verdict at BLOCK and blocks the repairable
|
|
253
|
+
lines beside it), skips fields with no durable identity and says so in the file, and writes no
|
|
254
|
+
assertion beyond what the app itself declares. What a submission *produces* stays a TODO.
|
|
255
|
+
|
|
256
|
+
That is the deterministic floor. Everything below — deriving *intent* rather than structure —
|
|
257
|
+
is still net-new, and still needs the grid.
|
|
258
|
+
|
|
248
259
|
**The audit found the assumed inputs do not exist.** There is no closed grid of test
|
|
249
260
|
patterns anywhere in the ecosystem to derive from — `QA_AGENT_RECIPES` and
|
|
250
261
|
`AUGMENTED_QA_PLAYBOOK` are editorial content, not structured data. So the intent→scenario
|
package/dist/cli/args.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.KNOWN_FLAGS = {
|
|
|
23
23
|
login: ['--url', '--session'],
|
|
24
24
|
history: ['--json', '--limit'],
|
|
25
25
|
coverage: ['--json', '--report', '--open'],
|
|
26
|
+
generate: ['--json', '--out', '--force'],
|
|
26
27
|
};
|
|
27
28
|
function rejectUnknownFlags(command, args) {
|
|
28
29
|
const known = exports.KNOWN_FLAGS[command];
|
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":";;;AAwBA,gDAQC;AAhCD,sGAAsG;AACzF,QAAA,WAAW,GAAsC;IAC5D,IAAI,EAAE;QACJ,SAAS;QACT,OAAO;QACP,aAAa;QACb,eAAe;QACf,WAAW;QACX,cAAc;QACd,cAAc;QACd,QAAQ;QACR,eAAe;QACf,qBAAqB;QACrB,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,QAAQ;KACT;IACD,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC;IAC7B,OAAO,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9B,QAAQ,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC;IAC1C,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;CACzC,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,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,yBAAyB,OAAO,kDAAkD,CAAC,CAAC;IACxH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PageMapping } from '@ia-qa/self-healing';
|
|
2
|
+
/**
|
|
3
|
+
* `ia-qa-discover generate` — a starting suite for the pages nothing tests.
|
|
4
|
+
*
|
|
5
|
+
* **Why it lives here and not in `@ia-qa/self-healing`.** It needs both halves: the contract,
|
|
6
|
+
* to name elements the way healing will repair them, and the capture, to know which fields a
|
|
7
|
+
* page takes and which of them carry a durable identity. Discovery already depends on
|
|
8
|
+
* healing; the reverse would invert a publish order that exists to be enforced. So the
|
|
9
|
+
* package that already holds both is the one that gets the verb.
|
|
10
|
+
*
|
|
11
|
+
* **What it proposes, never applies.** Files land in their own directory and an existing one
|
|
12
|
+
* is never overwritten without `--force` — the same restraint as `discover --apply` and
|
|
13
|
+
* `heal skill`. A generated test is a proposal a human reads, and nothing here adds it to
|
|
14
|
+
* anyone's suite.
|
|
15
|
+
*
|
|
16
|
+
* **Default target is the gap, not the app.** With no arguments it generates for the pages
|
|
17
|
+
* the coverage map says nothing tests, because generating over pages already covered is how
|
|
18
|
+
* a suite doubles in size without covering one more thing. Named pages override that.
|
|
19
|
+
*/
|
|
20
|
+
export declare const GENERATED_DIRNAME = "generated";
|
|
21
|
+
export declare function generatedDir(cwd?: string): string;
|
|
22
|
+
export declare function runGenerate(args: string[]): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Whether healing would ever read what was just written.
|
|
25
|
+
*
|
|
26
|
+
* This fires on the default, and that is the point. Measured on a real project: the specs
|
|
27
|
+
* landed in `.ia-qa-discovery/generated/` while healing's `testPaths` was `["tests/"]`, so
|
|
28
|
+
* `ingest` inventoried nothing from them — no name drift would escalate, `fix` would never
|
|
29
|
+
* rewrite them, and "born already maintained" was simply false. The first check of it looked
|
|
30
|
+
* green for the wrong reason: the placeholders were in `usage.json` because the project's own
|
|
31
|
+
* suite already used them, and every recorded site was in `tests/`.
|
|
32
|
+
*
|
|
33
|
+
* Writing into someone's suite by default would be the other error — this verb produces
|
|
34
|
+
* proposals, and a proposal does not install itself. So the default stays out of the way and
|
|
35
|
+
* says so, with both remedies, only when it is actually out of reach.
|
|
36
|
+
*/
|
|
37
|
+
export declare function healingReach(outDir: string, cwd?: string): {
|
|
38
|
+
covered: boolean;
|
|
39
|
+
why: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The contract for a captured page, by name and then by URL.
|
|
43
|
+
*
|
|
44
|
+
* The URL fallback is not belt-and-braces, it is the ordinary case on a configured project.
|
|
45
|
+
* Both packages resolve a page name through the same `pageNameFromUrl`, but healing's
|
|
46
|
+
* `config.pages` may *declare* a name, and that declaration wins there and is invisible here:
|
|
47
|
+
* measured on a real app, discovery captured `/` as `home` while the contract for the very
|
|
48
|
+
* same URL was filed as `login`. Matching on the name alone found nothing, so the generator
|
|
49
|
+
* fell back to the page's own placeholder text — a locator that works, but not the one the
|
|
50
|
+
* contract holds and therefore not one healing will rewrite. Silently producing the weaker
|
|
51
|
+
* form is exactly the failure this whole feature is built to avoid.
|
|
52
|
+
*
|
|
53
|
+
* A page with no contract at all is still generated, from its capture alone — thinner, and
|
|
54
|
+
* never fatal.
|
|
55
|
+
*/
|
|
56
|
+
export declare function contractOf(name: string, url: string, mappingPath: string): PageMapping | null;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GENERATED_DIRNAME = void 0;
|
|
37
|
+
exports.generatedDir = generatedDir;
|
|
38
|
+
exports.runGenerate = runGenerate;
|
|
39
|
+
exports.healingReach = healingReach;
|
|
40
|
+
exports.contractOf = contractOf;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const self_healing_1 = require("@ia-qa/self-healing");
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const coverage_1 = require("../coverage");
|
|
46
|
+
const generate_1 = require("../generate");
|
|
47
|
+
/** `--out <dir>`, and the remaining arguments with it removed. */
|
|
48
|
+
function takeValueFlag(args, flag, fallback) {
|
|
49
|
+
const at = args.indexOf(flag);
|
|
50
|
+
if (at === -1)
|
|
51
|
+
return { value: fallback, rest: args };
|
|
52
|
+
const value = args[at + 1];
|
|
53
|
+
if (value === undefined || value.startsWith('-')) {
|
|
54
|
+
throw new Error(`${flag} needs a directory, e.g. \`${flag} tests/generated\`.`);
|
|
55
|
+
}
|
|
56
|
+
return { value: path.resolve(process.cwd(), value), rest: [...args.slice(0, at), ...args.slice(at + 2)] };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `ia-qa-discover generate` — a starting suite for the pages nothing tests.
|
|
60
|
+
*
|
|
61
|
+
* **Why it lives here and not in `@ia-qa/self-healing`.** It needs both halves: the contract,
|
|
62
|
+
* to name elements the way healing will repair them, and the capture, to know which fields a
|
|
63
|
+
* page takes and which of them carry a durable identity. Discovery already depends on
|
|
64
|
+
* healing; the reverse would invert a publish order that exists to be enforced. So the
|
|
65
|
+
* package that already holds both is the one that gets the verb.
|
|
66
|
+
*
|
|
67
|
+
* **What it proposes, never applies.** Files land in their own directory and an existing one
|
|
68
|
+
* is never overwritten without `--force` — the same restraint as `discover --apply` and
|
|
69
|
+
* `heal skill`. A generated test is a proposal a human reads, and nothing here adds it to
|
|
70
|
+
* anyone's suite.
|
|
71
|
+
*
|
|
72
|
+
* **Default target is the gap, not the app.** With no arguments it generates for the pages
|
|
73
|
+
* the coverage map says nothing tests, because generating over pages already covered is how
|
|
74
|
+
* a suite doubles in size without covering one more thing. Named pages override that.
|
|
75
|
+
*/
|
|
76
|
+
exports.GENERATED_DIRNAME = 'generated';
|
|
77
|
+
function generatedDir(cwd = (0, config_1.baseDir)()) {
|
|
78
|
+
return path.join(cwd, '.ia-qa-discovery', exports.GENERATED_DIRNAME);
|
|
79
|
+
}
|
|
80
|
+
async function runGenerate(args) {
|
|
81
|
+
const json = args.includes('--json');
|
|
82
|
+
const force = args.includes('--force');
|
|
83
|
+
const { value: outDir, rest } = takeValueFlag(args, '--out', generatedDir());
|
|
84
|
+
const named = rest.filter((a) => !a.startsWith('-'));
|
|
85
|
+
const surface = (0, coverage_1.loadSurface)((0, config_1.captureDir)());
|
|
86
|
+
if (!surface) {
|
|
87
|
+
return refuse(json, 'No capture to generate from.', ['Run `ia-qa-discover scan` first — a test cannot be written for a page nobody has looked at.']);
|
|
88
|
+
}
|
|
89
|
+
const byName = new Map(surface.pages.map((p) => [p.page, p]));
|
|
90
|
+
let targets;
|
|
91
|
+
if (named.length > 0) {
|
|
92
|
+
const unknown = named.filter((n) => !byName.has(n));
|
|
93
|
+
if (unknown.length > 0) {
|
|
94
|
+
return refuse(json, `Not in this capture: ${unknown.join(', ')}.`, [
|
|
95
|
+
`Pages captured: ${[...byName.keys()].join(', ')}`,
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
targets = named;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const config = (() => {
|
|
102
|
+
try {
|
|
103
|
+
return (0, config_1.loadConfig)();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
const coverage = (0, coverage_1.computeCoverage)({
|
|
110
|
+
captureDirPath: (0, config_1.captureDir)(),
|
|
111
|
+
healingDirPath: path.join((0, config_1.baseDir)(), coverage_1.HEALING_DIR),
|
|
112
|
+
scope: config?.outOfScope,
|
|
113
|
+
});
|
|
114
|
+
if (!coverage.ok) {
|
|
115
|
+
// Generating for every captured page instead would bury the pages that need a test
|
|
116
|
+
// under the ones already covered — a bigger suite, not a better one. The refusal names
|
|
117
|
+
// the measurement that is missing, and the pages remain reachable by name.
|
|
118
|
+
return refuse(json, `Cannot tell which pages are untested: ${coverage.refusal.message}`, [
|
|
119
|
+
...coverage.refusal.available,
|
|
120
|
+
`Or name the pages yourself: ia-qa-discover generate <page…>`,
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
targets = coverage.map.uncovered.map((u) => u.page);
|
|
124
|
+
if (targets.length === 0) {
|
|
125
|
+
return refuse(json, 'Every page this scan reached is already visited by your suite.', [
|
|
126
|
+
`Name a page to generate for it anyway: ia-qa-discover generate <page…>`,
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const mappingPath = path.join((0, config_1.baseDir)(), coverage_1.HEALING_DIR, 'mapping');
|
|
131
|
+
const specs = targets.map((name) => {
|
|
132
|
+
const captured = byName.get(name);
|
|
133
|
+
return (0, generate_1.buildSpec)({ page: captured, contract: contractOf(name, captured.url, mappingPath) });
|
|
134
|
+
});
|
|
135
|
+
const written = [];
|
|
136
|
+
const kept = [];
|
|
137
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
138
|
+
for (const spec of specs) {
|
|
139
|
+
const file = path.join(outDir, `${spec.page}.spec.js`);
|
|
140
|
+
if (fs.existsSync(file) && !force) {
|
|
141
|
+
kept.push(path.relative(process.cwd(), file) || file);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(file, spec.code, 'utf8');
|
|
145
|
+
written.push({
|
|
146
|
+
page: spec.page,
|
|
147
|
+
file: path.relative(process.cwd(), file) || file,
|
|
148
|
+
locators: spec.locators.length,
|
|
149
|
+
skipped: spec.skipped.length,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (json) {
|
|
153
|
+
console.log(JSON.stringify({ written, kept, specs: specs.map(({ code, ...rest }) => rest) }, null, 2));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
console.log(`\n✍️ Generated ${written.length} spec${written.length === 1 ? '' : 's'} → ${path.relative(process.cwd(), outDir) || outDir}`);
|
|
157
|
+
for (const w of written) {
|
|
158
|
+
const skipped = w.skipped > 0 ? ` · ${w.skipped} field${w.skipped === 1 ? '' : 's'} left out` : '';
|
|
159
|
+
console.log(` · ${w.page} — ${w.locators} locator${w.locators === 1 ? '' : 's'}${skipped}`);
|
|
160
|
+
}
|
|
161
|
+
if (kept.length > 0) {
|
|
162
|
+
console.log(`\n Left untouched (already there, --force to replace):`);
|
|
163
|
+
for (const k of kept)
|
|
164
|
+
console.log(` · ${k}`);
|
|
165
|
+
}
|
|
166
|
+
const totalSkipped = specs.reduce((n, s) => n + s.skipped.length, 0);
|
|
167
|
+
if (totalSkipped > 0) {
|
|
168
|
+
console.log(`\n ${totalSkipped} field(s) were not generated — each says why at the top of its file.\n` +
|
|
169
|
+
` Most often: no test id, id or name, so the only selector is positional. That is a\n` +
|
|
170
|
+
` finding about the app, not about this tool: such a field cannot be tested reliably\n` +
|
|
171
|
+
` by anyone, and healing could not repair it either.`);
|
|
172
|
+
}
|
|
173
|
+
const reach = healingReach(outDir);
|
|
174
|
+
if (written.length > 0 && !reach.covered) {
|
|
175
|
+
console.log(`\n ⚠ Healing cannot see these files, so it will never repair them.\n` +
|
|
176
|
+
` ${reach.why}\n` +
|
|
177
|
+
` The whole point of generating them this way is that a label change is REWRITTEN\n` +
|
|
178
|
+
` instead of turning the suite red — and that only happens for files \`ingest\`\n` +
|
|
179
|
+
` inventories. Either write them where your suite lives:\n` +
|
|
180
|
+
` ia-qa-discover generate --out <a path under your testPaths>\n` +
|
|
181
|
+
` or add this directory to "testPaths" in .ia-qa/config.json, then run:\n` +
|
|
182
|
+
` ia-qa-heal ingest`);
|
|
183
|
+
}
|
|
184
|
+
console.log(`\n These are proposals. Nothing was added to your suite, and the assertions are yours:\n` +
|
|
185
|
+
` the navigation and the actions are generated, what the app should DO is a TODO in\n` +
|
|
186
|
+
` each file. An app declares what it accepts, never what it promises.`);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Whether healing would ever read what was just written.
|
|
190
|
+
*
|
|
191
|
+
* This fires on the default, and that is the point. Measured on a real project: the specs
|
|
192
|
+
* landed in `.ia-qa-discovery/generated/` while healing's `testPaths` was `["tests/"]`, so
|
|
193
|
+
* `ingest` inventoried nothing from them — no name drift would escalate, `fix` would never
|
|
194
|
+
* rewrite them, and "born already maintained" was simply false. The first check of it looked
|
|
195
|
+
* green for the wrong reason: the placeholders were in `usage.json` because the project's own
|
|
196
|
+
* suite already used them, and every recorded site was in `tests/`.
|
|
197
|
+
*
|
|
198
|
+
* Writing into someone's suite by default would be the other error — this verb produces
|
|
199
|
+
* proposals, and a proposal does not install itself. So the default stays out of the way and
|
|
200
|
+
* says so, with both remedies, only when it is actually out of reach.
|
|
201
|
+
*/
|
|
202
|
+
function healingReach(outDir, cwd = (0, config_1.baseDir)()) {
|
|
203
|
+
let testPaths;
|
|
204
|
+
try {
|
|
205
|
+
// Healing's own resolver, never a copy of its rule: it resolves against the project root
|
|
206
|
+
// rather than the config directory, and getting that wrong points the check at a
|
|
207
|
+
// directory nobody has.
|
|
208
|
+
testPaths = (0, self_healing_1.configuredTestPaths)(cwd);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return { covered: false, why: `Healing's config could not be read (${err.message}).` };
|
|
212
|
+
}
|
|
213
|
+
if (testPaths.length === 0) {
|
|
214
|
+
return { covered: false, why: `Healing declares no "testPaths", so \`ingest\` reads nothing.` };
|
|
215
|
+
}
|
|
216
|
+
const target = path.resolve(outDir);
|
|
217
|
+
const covered = testPaths.some((abs) => {
|
|
218
|
+
const rel = path.relative(abs, target);
|
|
219
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
covered,
|
|
223
|
+
why: covered
|
|
224
|
+
? ''
|
|
225
|
+
: `They are outside "testPaths" (${testPaths.map((p) => path.relative(cwd, p) || p).join(', ')}), which is all \`ingest\` reads.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The contract for a captured page, by name and then by URL.
|
|
230
|
+
*
|
|
231
|
+
* The URL fallback is not belt-and-braces, it is the ordinary case on a configured project.
|
|
232
|
+
* Both packages resolve a page name through the same `pageNameFromUrl`, but healing's
|
|
233
|
+
* `config.pages` may *declare* a name, and that declaration wins there and is invisible here:
|
|
234
|
+
* measured on a real app, discovery captured `/` as `home` while the contract for the very
|
|
235
|
+
* same URL was filed as `login`. Matching on the name alone found nothing, so the generator
|
|
236
|
+
* fell back to the page's own placeholder text — a locator that works, but not the one the
|
|
237
|
+
* contract holds and therefore not one healing will rewrite. Silently producing the weaker
|
|
238
|
+
* form is exactly the failure this whole feature is built to avoid.
|
|
239
|
+
*
|
|
240
|
+
* A page with no contract at all is still generated, from its capture alone — thinner, and
|
|
241
|
+
* never fatal.
|
|
242
|
+
*/
|
|
243
|
+
function contractOf(name, url, mappingPath) {
|
|
244
|
+
try {
|
|
245
|
+
return (0, self_healing_1.loadMapping)(name, mappingPath);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* not filed under this name — try the URL */
|
|
249
|
+
}
|
|
250
|
+
const target = normalizeUrl(url);
|
|
251
|
+
if (!target)
|
|
252
|
+
return null;
|
|
253
|
+
let files;
|
|
254
|
+
try {
|
|
255
|
+
files = fs.readdirSync(mappingPath).filter((f) => f.endsWith('.json') && !f.startsWith('_'));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
try {
|
|
262
|
+
const mapping = JSON.parse(fs.readFileSync(path.join(mappingPath, file), 'utf8'));
|
|
263
|
+
if (normalizeUrl(mapping.url) === target)
|
|
264
|
+
return mapping;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// One unreadable contract costs one candidate, never the generation.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
/** Host and path only: a trailing slash or a query string does not make it another page. */
|
|
273
|
+
function normalizeUrl(raw) {
|
|
274
|
+
if (!raw)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
const u = new URL(raw);
|
|
278
|
+
return `${u.host.replace(/^www\./, '')}${u.pathname.replace(/\/$/, '')}`;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function refuse(json, message, available) {
|
|
285
|
+
if (json) {
|
|
286
|
+
console.log(JSON.stringify({ generated: false, reason: message, available }, null, 2));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
console.log(`\nℹ️ ${message}`);
|
|
290
|
+
for (const a of available)
|
|
291
|
+
console.log(`\n ${a}`);
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=generate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.js","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
declare const HELP = "ia-qa-discover \u2014 deterministic surface reconnaissance for a web app\n\nF1 of QA Discovery: crawl an app's same-origin pages and capture what each one\nis structurally about \u2014 title, headings, forms, and observed API calls. No\nLLM, no verdict: this is recon, not a gate. Runs entirely on your machine;\nnothing is uploaded anywhere.\n\n[FOR AI AGENTS] You are driving this for a human. Run `scan` to capture the\napp's surface, then read .ia-qa-discovery/capture/surface.json (the index) and\nthe per-page files it points at \u2014 that is the evidence for anything you\nreport about this app. Never assert a page's purpose or risk from the URL\nalone; ground it in the captured meta/headings/forms/apiCalls. Never read a\npage count as coverage: if scan warns that pages still show a login form, you\nmeasured the wall, not the app. You CANNOT run `login` \u2014 it waits for a human\nat a browser; ask them to run it, then scan again.\n`coverage` answers \"what does their suite NOT test\" \u2014 but only from a suite that was\nwatched running (one `ia-qa-heal run`, or one `ia-qa-pal tour --suite`). If it says there is no\nmeasurement, report that, never \"the suite covers nothing\": a contract means someone\nconfigured a page, only a watched run means a test went there.\n\nUsage:\n ia-qa-discover login Open a visible browser, log in by hand, keep the\n session (.ia-qa-discovery/session.json) for scan\n to reuse. The general answer to a login wall: it\n models nothing, so SSO, MFA, a consent screen or\n a magic link all work \u2014 you do them.\n Needs a terminal, refuses under CI. The file holds\n live cookies: gitignored, stays on this machine.\n --url <path> page to open (default: your baseUrl)\n --session <f> where to write it\n Already have a storageState (a Playwright globalSetup\n writes one)? Skip this verb: point scan at it with\n --session <file> or \"session\" in config.json.\n ia-qa-discover scan [url] Crawl same-origin pages from <url> (or the saved\n baseUrl) and capture each page's structure.\n Writes .ia-qa-discovery/capture/, diffs against\n the previous run, appends history.jsonl.\n --depth <n> crawl link-depth (default 2)\n --max <n> page cap (default 60)\n --deep also open menus, tabs and dialogs one level\n down and capture the fields they reveal. Every\n non-GET request is blocked for the duration,\n so a click cannot mutate anything server-side.\n --deep-budget <n> max controls clicked per page (default 60)\n --no-sitemap do not read sitemap.xml / robots.txt\n --no-reveal do not open menus/dropdowns for hidden links\n --strict-host treat www./apex as different hosts\n --session <file> reuse an existing storageState (login) file\n --no-network skip API/network-call capture this run\n --network-threshold <n> shared-call promotion threshold 0-1 (default 0.6)\n --save persist this baseUrl to config.json\n --json machine-readable summary on stdout\n --report [file.html] branded ia-qa.com HTML dossier of the\n app (default ia-qa-discover-report.html)\n --open open that report in the browser\n ia-qa-discover coverage What your test suite does NOT test. Compares the\n pages this scan found against the pages your suite\n was OBSERVED visiting, ranks the gap by what each\n page takes as input, and writes coverage-map.json.\n Reads local files only \u2014 no browser, no network.\n Needs one watched run of your suite \u2014\n \"ia-qa-heal run\" or \"ia-qa-pal tour --suite\":\n without a watched run there is no measurement, and\n it says so instead of showing a number.\n Declare pages you deliberately do not test in\n \"outOfScope\" in config.json \u2014 they stay counted\n and named, never silently dropped.\n --json machine-readable map on stdout\n --report [file.html] branded HTML dossier with the gap\n (default ia-qa-discover-coverage.html)\n --open open that report in the browser\n ia-qa-discover history Recorded scan runs \u2014 pages/forms/API-surface trend.\n --json --limit <n> (default 500)\n ia-qa-discover version Print the installed version (also --version, -v)\n ia-qa-discover help Show this help\n\nGlobal:\n --config <dir> Resolve .ia-qa-discovery/ under <dir> instead of the\n current directory. Also IAQA_DISCOVERY_CONFIG_DIR=<dir>.\n";
|
|
2
|
+
declare const HELP = "ia-qa-discover \u2014 deterministic surface reconnaissance for a web app\n\nF1 of QA Discovery: crawl an app's same-origin pages and capture what each one\nis structurally about \u2014 title, headings, forms, and observed API calls. No\nLLM, no verdict: this is recon, not a gate. Runs entirely on your machine;\nnothing is uploaded anywhere.\n\n[FOR AI AGENTS] You are driving this for a human. Run `scan` to capture the\napp's surface, then read .ia-qa-discovery/capture/surface.json (the index) and\nthe per-page files it points at \u2014 that is the evidence for anything you\nreport about this app. Never assert a page's purpose or risk from the URL\nalone; ground it in the captured meta/headings/forms/apiCalls. Never read a\npage count as coverage: if scan warns that pages still show a login form, you\nmeasured the wall, not the app. You CANNOT run `login` \u2014 it waits for a human\nat a browser; ask them to run it, then scan again.\n`coverage` answers \"what does their suite NOT test\" \u2014 but only from a suite that was\nwatched running (one `ia-qa-heal run`, or one `ia-qa-pal tour --suite`). If it says there is no\nmeasurement, report that, never \"the suite covers nothing\": a contract means someone\nconfigured a page, only a watched run means a test went there.\n\nUsage:\n ia-qa-discover login Open a visible browser, log in by hand, keep the\n session (.ia-qa-discovery/session.json) for scan\n to reuse. The general answer to a login wall: it\n models nothing, so SSO, MFA, a consent screen or\n a magic link all work \u2014 you do them.\n Needs a terminal, refuses under CI. The file holds\n live cookies: gitignored, stays on this machine.\n --url <path> page to open (default: your baseUrl)\n --session <f> where to write it\n Already have a storageState (a Playwright globalSetup\n writes one)? Skip this verb: point scan at it with\n --session <file> or \"session\" in config.json.\n ia-qa-discover scan [url] Crawl same-origin pages from <url> (or the saved\n baseUrl) and capture each page's structure.\n Writes .ia-qa-discovery/capture/, diffs against\n the previous run, appends history.jsonl.\n --depth <n> crawl link-depth (default 2)\n --max <n> page cap (default 60)\n --deep also open menus, tabs and dialogs one level\n down and capture the fields they reveal. Every\n non-GET request is blocked for the duration,\n so a click cannot mutate anything server-side.\n --deep-budget <n> max controls clicked per page (default 60)\n --no-sitemap do not read sitemap.xml / robots.txt\n --no-reveal do not open menus/dropdowns for hidden links\n --strict-host treat www./apex as different hosts\n --session <file> reuse an existing storageState (login) file\n --no-network skip API/network-call capture this run\n --network-threshold <n> shared-call promotion threshold 0-1 (default 0.6)\n --save persist this baseUrl to config.json\n --json machine-readable summary on stdout\n --report [file.html] branded ia-qa.com HTML dossier of the\n app (default ia-qa-discover-report.html)\n --open open that report in the browser\n ia-qa-discover coverage What your test suite does NOT test. Compares the\n pages this scan found against the pages your suite\n was OBSERVED visiting, ranks the gap by what each\n page takes as input, and writes coverage-map.json.\n Reads local files only \u2014 no browser, no network.\n Needs one watched run of your suite \u2014\n \"ia-qa-heal run\" or \"ia-qa-pal tour --suite\":\n without a watched run there is no measurement, and\n it says so instead of showing a number.\n Declare pages you deliberately do not test in\n \"outOfScope\" in config.json \u2014 they stay counted\n and named, never silently dropped.\n --json machine-readable map on stdout\n --report [file.html] branded HTML dossier with the gap\n (default ia-qa-discover-coverage.html)\n --open open that report in the browser\n ia-qa-discover generate [page\u2026] A starting Playwright suite for the pages nothing\n tests \u2014 one file per page, into its own directory.\n Every locator names an element the healing contract\n holds, so the file is REPAIRED when the app moves\n instead of going red. A field with no test id, id or\n name is left out and says so: its selector would be\n positional, which nobody can test reliably.\n Proposals \u2014 nothing is added to your suite, and the\n assertions stay yours (the app declares what it\n accepts, never what it promises).\n With no page, it generates for the coverage gap.\n --out <dir> where to write (default\n .ia-qa-discovery/generated/)\n --force replace files already there\n --json machine-readable summary on stdout\n ia-qa-discover history Recorded scan runs \u2014 pages/forms/API-surface trend.\n --json --limit <n> (default 500)\n ia-qa-discover version Print the installed version (also --version, -v)\n ia-qa-discover help Show this help\n\nGlobal:\n --config <dir> Resolve .ia-qa-discovery/ under <dir> instead of the\n current directory. Also IAQA_DISCOVERY_CONFIG_DIR=<dir>.\n";
|
|
3
3
|
declare function verbHelp(command: string | undefined): string | null;
|
|
4
4
|
declare function main(): Promise<void>;
|
|
5
5
|
export { HELP, verbHelp, main };
|
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const scan_1 = require("./scan");
|
|
|
10
10
|
const login_1 = require("./login");
|
|
11
11
|
const history_1 = require("./history");
|
|
12
12
|
const coverage_1 = require("./coverage");
|
|
13
|
+
const generate_1 = require("./generate");
|
|
13
14
|
// dist/cli/ → package root; npm ships package.json in every tarball.
|
|
14
15
|
const VERSION = require('../../package.json').version;
|
|
15
16
|
const HELP = `ia-qa-discover — deterministic surface reconnaissance for a web app
|
|
@@ -83,6 +84,21 @@ Usage:
|
|
|
83
84
|
--report [file.html] branded HTML dossier with the gap
|
|
84
85
|
(default ia-qa-discover-coverage.html)
|
|
85
86
|
--open open that report in the browser
|
|
87
|
+
ia-qa-discover generate [page…] A starting Playwright suite for the pages nothing
|
|
88
|
+
tests — one file per page, into its own directory.
|
|
89
|
+
Every locator names an element the healing contract
|
|
90
|
+
holds, so the file is REPAIRED when the app moves
|
|
91
|
+
instead of going red. A field with no test id, id or
|
|
92
|
+
name is left out and says so: its selector would be
|
|
93
|
+
positional, which nobody can test reliably.
|
|
94
|
+
Proposals — nothing is added to your suite, and the
|
|
95
|
+
assertions stay yours (the app declares what it
|
|
96
|
+
accepts, never what it promises).
|
|
97
|
+
With no page, it generates for the coverage gap.
|
|
98
|
+
--out <dir> where to write (default
|
|
99
|
+
.ia-qa-discovery/generated/)
|
|
100
|
+
--force replace files already there
|
|
101
|
+
--json machine-readable summary on stdout
|
|
86
102
|
ia-qa-discover history Recorded scan runs — pages/forms/API-surface trend.
|
|
87
103
|
--json --limit <n> (default 500)
|
|
88
104
|
ia-qa-discover version Print the installed version (also --version, -v)
|
|
@@ -158,6 +174,9 @@ async function main() {
|
|
|
158
174
|
case 'coverage':
|
|
159
175
|
await (0, coverage_1.runCoverage)(rest);
|
|
160
176
|
break;
|
|
177
|
+
case 'generate':
|
|
178
|
+
await (0, generate_1.runGenerate)(rest);
|
|
179
|
+
break;
|
|
161
180
|
case 'history':
|
|
162
181
|
await (0, history_1.runHistory)(rest);
|
|
163
182
|
break;
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/coverage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExercisedRecord, TestDepthReport } from '@ia-qa/self-healing';
|
|
1
|
+
import type { ExercisedRecord, TestDepthReport, PageChurn } from '@ia-qa/self-healing';
|
|
2
2
|
import type { PageCapture } from './capture/page';
|
|
3
3
|
import type { SurfaceManifest } from './scan';
|
|
4
4
|
/**
|
|
@@ -38,6 +38,12 @@ export interface UncoveredPage {
|
|
|
38
38
|
page: string;
|
|
39
39
|
url: string;
|
|
40
40
|
factors: string[];
|
|
41
|
+
/**
|
|
42
|
+
* What the page holds, shown beside the factors and never counted with them — see
|
|
43
|
+
* `rankFactors`. Describing a page and ranking it are two jobs, and merging them let
|
|
44
|
+
* "1 input · 1 API call" out-rank a page that had actually changed.
|
|
45
|
+
*/
|
|
46
|
+
context: string[];
|
|
41
47
|
fields: number;
|
|
42
48
|
passwordFields: number;
|
|
43
49
|
/** Forms the page declares with `method="post"` — the app's own statement that it writes. */
|
|
@@ -48,6 +54,15 @@ export interface UncoveredPage {
|
|
|
48
54
|
*/
|
|
49
55
|
writeCalls: number;
|
|
50
56
|
apiCalls: number;
|
|
57
|
+
/**
|
|
58
|
+
* How many of the measured runs this page drifted in, from `@ia-qa/self-healing`'s history.
|
|
59
|
+
*
|
|
60
|
+
* The one factor neither package can compute alone, and the strongest one here: a page that
|
|
61
|
+
* changes every week and that no test visits is where bugs are born, while a page untouched
|
|
62
|
+
* for six months does not need a new test. Absent when there is no history, or too few runs
|
|
63
|
+
* to separate anything — an absent factor stays absent.
|
|
64
|
+
*/
|
|
65
|
+
churnRuns?: number;
|
|
51
66
|
}
|
|
52
67
|
export interface CoverageMap {
|
|
53
68
|
schema: typeof COVERAGE_SCHEMA;
|
|
@@ -68,6 +83,17 @@ export interface CoverageMap {
|
|
|
68
83
|
notMeasured: SurfaceManifest['coverage'];
|
|
69
84
|
/** How much of each visited page's contract the suite names. */
|
|
70
85
|
depth: TestDepthReport;
|
|
86
|
+
/**
|
|
87
|
+
* What the drift factor was computed over, and what it had to leave out to mean anything.
|
|
88
|
+
* Absent when no history was available. A filter nobody can see is a blind spot, so this
|
|
89
|
+
* travels with the map rather than being recomputed by whoever renders it.
|
|
90
|
+
*/
|
|
91
|
+
churn?: {
|
|
92
|
+
measuredRuns: number;
|
|
93
|
+
excludedRuns: number;
|
|
94
|
+
ranked: boolean;
|
|
95
|
+
reason?: string;
|
|
96
|
+
};
|
|
71
97
|
suite: {
|
|
72
98
|
exitCode: number | null;
|
|
73
99
|
recordedAt: string;
|
|
@@ -82,6 +108,12 @@ export interface CoverageInput {
|
|
|
82
108
|
depth: TestDepthReport;
|
|
83
109
|
scope?: string[];
|
|
84
110
|
baseUrl: string;
|
|
111
|
+
/**
|
|
112
|
+
* Which pages actually move, from `@ia-qa/self-healing`'s run history. Optional: a project
|
|
113
|
+
* that has never run `diff` or `run` has none, and the gap is then ranked on structure
|
|
114
|
+
* alone — which is what a QA arriving on day one does too.
|
|
115
|
+
*/
|
|
116
|
+
churn?: PageChurn;
|
|
85
117
|
}
|
|
86
118
|
/**
|
|
87
119
|
* Why this page would hurt, from facts the scan observed.
|
|
@@ -109,12 +141,30 @@ export interface CoverageInput {
|
|
|
109
141
|
* React app that submits in JS and states nothing — and an absent factor is the honest
|
|
110
142
|
* outcome there, not a reason to go back to guessing from traffic.
|
|
111
143
|
*/
|
|
112
|
-
export declare function rankFactors(page: PageCapture
|
|
144
|
+
export declare function rankFactors(page: PageCapture,
|
|
145
|
+
/**
|
|
146
|
+
* This page's entry in the drift history, when there is one worth ranking on. The caller
|
|
147
|
+
* decides whether there is — `pageChurn` observes, the policy of "enough runs to mean
|
|
148
|
+
* something" belongs here, with the ranking it feeds.
|
|
149
|
+
*/
|
|
150
|
+
churn?: {
|
|
151
|
+
runs: number;
|
|
152
|
+
measuredRuns: number;
|
|
153
|
+
}): {
|
|
113
154
|
factors: string[];
|
|
155
|
+
context: string[];
|
|
114
156
|
passwordFields: number;
|
|
115
157
|
postForms: number;
|
|
116
158
|
writeCalls: number;
|
|
159
|
+
churnRuns?: number;
|
|
117
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* Below this, "changed in N of M runs" separates nothing: with two runs every page that ever
|
|
163
|
+
* drifted carries the factor, which is the inert-factor failure `writeCalls` was removed for.
|
|
164
|
+
* The history is not wrong there — it is just too short to rank on yet, and saying so beats
|
|
165
|
+
* ranking on it.
|
|
166
|
+
*/
|
|
167
|
+
export declare const MIN_CHURN_RUNS = 3;
|
|
118
168
|
/** Pages the human declared out of scope. One dialect: the same globs as `volatile`. */
|
|
119
169
|
export declare function matchesScope(page: string, scope: string[] | undefined): boolean;
|
|
120
170
|
export declare function buildCoverageMap(input: CoverageInput): CoverageMap;
|
package/dist/coverage.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.HEALING_DIR = exports.COVERAGE_SCHEMA = void 0;
|
|
36
|
+
exports.MIN_CHURN_RUNS = exports.HEALING_DIR = exports.COVERAGE_SCHEMA = void 0;
|
|
37
37
|
exports.rankFactors = rankFactors;
|
|
38
38
|
exports.matchesScope = matchesScope;
|
|
39
39
|
exports.buildCoverageMap = buildCoverageMap;
|
|
@@ -91,7 +91,13 @@ function allFields(page) {
|
|
|
91
91
|
* React app that submits in JS and states nothing — and an absent factor is the honest
|
|
92
92
|
* outcome there, not a reason to go back to guessing from traffic.
|
|
93
93
|
*/
|
|
94
|
-
function rankFactors(page
|
|
94
|
+
function rankFactors(page,
|
|
95
|
+
/**
|
|
96
|
+
* This page's entry in the drift history, when there is one worth ranking on. The caller
|
|
97
|
+
* decides whether there is — `pageChurn` observes, the policy of "enough runs to mean
|
|
98
|
+
* something" belongs here, with the ranking it feeds.
|
|
99
|
+
*/
|
|
100
|
+
churn) {
|
|
95
101
|
const fields = allFields(page);
|
|
96
102
|
const passwordFields = fields.filter((f) => f.type === 'password').length;
|
|
97
103
|
const postForms = page.forms.filter((f) => (f.method ?? '').toLowerCase() === 'post').length;
|
|
@@ -103,12 +109,34 @@ function rankFactors(page) {
|
|
|
103
109
|
factors.push('submits a form');
|
|
104
110
|
if (fields.some((f) => f.required))
|
|
105
111
|
factors.push('has required input');
|
|
112
|
+
if (churn)
|
|
113
|
+
factors.push(`changed in ${churn.runs} of ${churn.measuredRuns} recent runs`);
|
|
114
|
+
/**
|
|
115
|
+
* How much a page holds, which describes it without ranking it.
|
|
116
|
+
*
|
|
117
|
+
* Measured on ia-qa.com: four uncovered pages read `1 input · 1 API call`,
|
|
118
|
+
* `1 input · 3 API calls`, `1 input · 3 API calls`, `4 inputs · 2 API calls` — indis-
|
|
119
|
+
* tinguishable, and between them they out-counted a page that had actually changed three
|
|
120
|
+
* times. Counting them is the `writeCalls` failure once removed: a factor nearly every
|
|
121
|
+
* page carries ranks nothing, and here it ranked *against* the one signal that meant
|
|
122
|
+
* something. Deleting them would be the opposite error — `4 inputs` is a real fact about
|
|
123
|
+
* a page, and dropping it would print `no input, no observed call` over four fields. So
|
|
124
|
+
* they are shown and not counted.
|
|
125
|
+
*/
|
|
126
|
+
const context = [];
|
|
106
127
|
if (fields.length > 0)
|
|
107
|
-
|
|
128
|
+
context.push(`${fields.length} input${fields.length === 1 ? '' : 's'}`);
|
|
108
129
|
if (page.apiCalls.length > 0)
|
|
109
|
-
|
|
110
|
-
return { factors, passwordFields, postForms, writeCalls };
|
|
130
|
+
context.push(`${page.apiCalls.length} API call${page.apiCalls.length === 1 ? '' : 's'}`);
|
|
131
|
+
return { factors, context, passwordFields, postForms, writeCalls, churnRuns: churn?.runs };
|
|
111
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Below this, "changed in N of M runs" separates nothing: with two runs every page that ever
|
|
135
|
+
* drifted carries the factor, which is the inert-factor failure `writeCalls` was removed for.
|
|
136
|
+
* The history is not wrong there — it is just too short to rank on yet, and saying so beats
|
|
137
|
+
* ranking on it.
|
|
138
|
+
*/
|
|
139
|
+
exports.MIN_CHURN_RUNS = 3;
|
|
112
140
|
/** Pages the human declared out of scope. One dialect: the same globs as `volatile`. */
|
|
113
141
|
function matchesScope(page, scope) {
|
|
114
142
|
if (!scope?.length)
|
|
@@ -118,6 +146,14 @@ function matchesScope(page, scope) {
|
|
|
118
146
|
function buildCoverageMap(input) {
|
|
119
147
|
const visited = new Set(input.exercised.visited);
|
|
120
148
|
const scanned = new Map(input.pages.map((p) => [p.page, p]));
|
|
149
|
+
// A short history is not a wrong one, it is one that cannot rank yet — so the factor is
|
|
150
|
+
// simply absent rather than computed on two runs and presented as a signal.
|
|
151
|
+
const usableChurn = input.churn && input.churn.measuredRuns >= exports.MIN_CHURN_RUNS ? input.churn : undefined;
|
|
152
|
+
const churnByPage = new Map((usableChurn?.pages ?? []).map((p) => [p.name, p]));
|
|
153
|
+
const churnOf = (name) => {
|
|
154
|
+
const hit = churnByPage.get(name);
|
|
155
|
+
return hit ? { runs: hit.runs, measuredRuns: usableChurn.measuredRuns } : undefined;
|
|
156
|
+
};
|
|
121
157
|
const outOfScope = [];
|
|
122
158
|
const uncovered = [];
|
|
123
159
|
for (const page of input.pages) {
|
|
@@ -127,22 +163,33 @@ function buildCoverageMap(input) {
|
|
|
127
163
|
outOfScope.push(page.page);
|
|
128
164
|
continue;
|
|
129
165
|
}
|
|
130
|
-
const { factors, passwordFields, postForms, writeCalls } = rankFactors(page);
|
|
166
|
+
const { factors, context, passwordFields, postForms, writeCalls, churnRuns } = rankFactors(page, churnOf(page.page));
|
|
131
167
|
uncovered.push({
|
|
132
168
|
page: page.page,
|
|
133
169
|
url: page.url,
|
|
134
170
|
factors,
|
|
171
|
+
context,
|
|
135
172
|
fields: allFields(page).length,
|
|
136
173
|
passwordFields,
|
|
137
174
|
postForms,
|
|
138
175
|
writeCalls,
|
|
139
176
|
apiCalls: page.apiCalls.length,
|
|
177
|
+
churnRuns,
|
|
140
178
|
});
|
|
141
179
|
}
|
|
142
180
|
// Ranked by how many factors a page carries. `takes credentials` and `submits a form` are
|
|
143
181
|
// the two that change what a break costs, so they break ties before the name does.
|
|
144
182
|
// `writeCalls` is NOT one of them — see `rankFactors` for what it turned out to measure.
|
|
145
|
-
|
|
183
|
+
// Drift joins them: what a break costs, and how likely one is, are the two halves of the
|
|
184
|
+
// same question, and this is the half no scan can see.
|
|
185
|
+
// Context breaks a tie and never more: between two pages nothing observed distinguishes,
|
|
186
|
+
// the one that at least holds something goes first — but it can never reach past a page
|
|
187
|
+
// carrying a real factor, which is the inversion this split exists to stop.
|
|
188
|
+
const weight = (u) => u.factors.length * 10 +
|
|
189
|
+
(u.passwordFields > 0 ? 5 : 0) +
|
|
190
|
+
(u.churnRuns ? 4 : 0) +
|
|
191
|
+
(u.postForms > 0 ? 3 : 0) +
|
|
192
|
+
(u.context.length > 0 ? 1 : 0);
|
|
146
193
|
uncovered.sort((a, b) => weight(b) - weight(a) || a.page.localeCompare(b.page));
|
|
147
194
|
return {
|
|
148
195
|
schema: exports.COVERAGE_SCHEMA,
|
|
@@ -154,6 +201,14 @@ function buildCoverageMap(input) {
|
|
|
154
201
|
contractedNotFound: [...visited, ...input.exercised.notVisited].filter((p) => !scanned.has(p)).sort(),
|
|
155
202
|
notMeasured: input.manifest.coverage,
|
|
156
203
|
depth: input.depth,
|
|
204
|
+
churn: input.churn
|
|
205
|
+
? {
|
|
206
|
+
measuredRuns: input.churn.measuredRuns,
|
|
207
|
+
excludedRuns: input.churn.excludedRuns,
|
|
208
|
+
ranked: Boolean(usableChurn),
|
|
209
|
+
reason: input.churn.reason,
|
|
210
|
+
}
|
|
211
|
+
: undefined,
|
|
157
212
|
suite: {
|
|
158
213
|
exitCode: input.exercised.suiteExitCode,
|
|
159
214
|
recordedAt: input.exercised.recordedAt,
|
|
@@ -252,6 +307,10 @@ function computeCoverage(opts) {
|
|
|
252
307
|
const exercised = (0, self_healing_1.loadExercised)(mappingPath);
|
|
253
308
|
if (!exercised)
|
|
254
309
|
return { ok: false, refusal: refusal('no-run') };
|
|
310
|
+
// `healingDirPath` is the project's `.ia-qa/`, and `readHistory` resolves it from the
|
|
311
|
+
// project root — so the parent. No history is an ordinary state (nothing has run `diff`
|
|
312
|
+
// yet), never a refusal: the gap is then ranked on structure alone.
|
|
313
|
+
const churn = (0, self_healing_1.pageChurn)((0, self_healing_1.readHistory)(undefined, path.dirname(opts.healingDirPath)));
|
|
255
314
|
return {
|
|
256
315
|
ok: true,
|
|
257
316
|
map: buildCoverageMap({
|
|
@@ -261,6 +320,7 @@ function computeCoverage(opts) {
|
|
|
261
320
|
depth: (0, self_healing_1.readTestDepth)(mappingPath, path.join(mappingPath, '_resolved')),
|
|
262
321
|
scope: opts.scope,
|
|
263
322
|
baseUrl: surface.manifest.baseUrl,
|
|
323
|
+
churn: churn.pages.length > 0 ? churn : undefined,
|
|
264
324
|
}),
|
|
265
325
|
};
|
|
266
326
|
}
|
package/dist/coverage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"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"}
|
package/dist/coverageView.js
CHANGED
|
@@ -28,7 +28,14 @@ function coverageHeadline(map) {
|
|
|
28
28
|
(map.outOfScope.length > 0 ? ` · ${map.outOfScope.length} declared out of scope` : ''));
|
|
29
29
|
}
|
|
30
30
|
function factorsOf(u) {
|
|
31
|
-
|
|
31
|
+
// Factors first — they are why the page is where it is in the list. Context follows, in
|
|
32
|
+
// brackets, so a reader can tell at a glance what earned the rank from what merely
|
|
33
|
+
// describes the page. A page with context but no factor is not silent: it holds something,
|
|
34
|
+
// nothing observed says it matters yet.
|
|
35
|
+
const ctx = u.context?.length ? ` (${u.context.join(' · ')})` : '';
|
|
36
|
+
if (u.factors.length > 0)
|
|
37
|
+
return `${u.factors.join(' · ')}${ctx}`;
|
|
38
|
+
return u.context?.length ? `nothing observed says it matters${ctx}` : 'no input, no observed call';
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* The caveats that keep the headline honest, as sentences.
|
|
@@ -59,6 +66,15 @@ function coverageCaveats(map) {
|
|
|
59
66
|
out.push(`${map.contractedNotFound.length} page(s) your suite knows were not reached by this scan — ` +
|
|
60
67
|
`a limit of the scan (--max, crawl reach, session), never evidence they are gone.`);
|
|
61
68
|
}
|
|
69
|
+
// How often a page changes is a ranking factor here, so what the history could not say is
|
|
70
|
+
// as much a caveat as what the scan could not see. Silent on the good case: a history long
|
|
71
|
+
// enough to rank on, with nothing held out, says nothing at all.
|
|
72
|
+
if (map.churn && !map.churn.ranked) {
|
|
73
|
+
out.push(`Only ${map.churn.measuredRuns} comparable run(s) of drift history — too few to rank on, ` +
|
|
74
|
+
`so how often a page changes is not part of the order above. It sharpens as runs accumulate.`);
|
|
75
|
+
}
|
|
76
|
+
if (map.churn?.reason)
|
|
77
|
+
out.push(map.churn.reason);
|
|
62
78
|
if (map.depth.measurable === false) {
|
|
63
79
|
out.push(`Depth is NOT measured: no locator inventory. Run \`ia-qa-heal ingest\` to see how much of each ` +
|
|
64
80
|
`visited page your tests actually name.`);
|
package/dist/coverageView.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coverageView.js","sourceRoot":"","sources":["../src/coverageView.ts"],"names":[],"mappings":";;AAkBA,4CAQC;
|
|
1
|
+
{"version":3,"file":"coverageView.js","sourceRoot":"","sources":["../src/coverageView.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PageMapping } from '@ia-qa/self-healing';
|
|
2
|
+
import type { PageCapture } from './capture/page';
|
|
3
|
+
/**
|
|
4
|
+
* Turn a captured page into a test file that `@ia-qa/self-healing` can repair.
|
|
5
|
+
*
|
|
6
|
+
* The promise this exists to keep is not "we write your tests" — `playwright codegen` has
|
|
7
|
+
* done that for years — but **the tests we write are already maintained**: every locator
|
|
8
|
+
* names an element the contract holds, so when the app moves the file is *repaired* instead
|
|
9
|
+
* of deleted. That loop is proven end to end in
|
|
10
|
+
* `packages/self-healing/tests/corpus/generated.test.js`; this module produces the artifact
|
|
11
|
+
* that test validates by hand.
|
|
12
|
+
*
|
|
13
|
+
* Three rules are load-bearing, and each was paid for by a measurement rather than a
|
|
14
|
+
* preference.
|
|
15
|
+
*
|
|
16
|
+
* **1. Only calls that state a role.** `fix` rewrites `*ByRole`, `*ByLabel`,
|
|
17
|
+
* `*ByPlaceholder` and `By.linkText`, and nothing else: `getByText` names a *string*, not an
|
|
18
|
+
* element, so nothing can prove the test meant the renamed button rather than a heading that
|
|
19
|
+
* never moved. The corpus measured the consequence and it is worse than "that line will not
|
|
20
|
+
* be repaired" — **one** unrepairable line holds the whole verdict at BLOCK, which stops
|
|
21
|
+
* `fix` from repairing the repairable lines *beside* it. So this is not a preference order:
|
|
22
|
+
* a field that cannot be named by role, label or placeholder is **skipped**, never written
|
|
23
|
+
* in a weaker form.
|
|
24
|
+
*
|
|
25
|
+
* **2. Only fields carrying a durable identity.** `stableSelector: false` means the capture
|
|
26
|
+
* fell back to a positional selector, which addresses a different element the moment a field
|
|
27
|
+
* is inserted above it. Measured across both corpora: where a field has a `name`/`id`/
|
|
28
|
+
* test-id, discovery and healing independently synthesise the *same* selector (2 of 2 on a
|
|
29
|
+
* real login form); where it has none, they never agree (0 of 17). The join and the
|
|
30
|
+
* testability fail on exactly the same population, so refusing there costs nothing that
|
|
31
|
+
* could have worked.
|
|
32
|
+
*
|
|
33
|
+
* **3. The assertion is the human's.** An application declares what it *accepts* — required,
|
|
34
|
+
* type, min, max — and never what it *promises*. "A refused card shows the right message"
|
|
35
|
+
* exists in someone's head, and inventing it is how a generator produces a suite nobody
|
|
36
|
+
* trusts. What the app declares is asserted; the rest is a TODO with the question written
|
|
37
|
+
* out, because a silent gap reads as a finished test.
|
|
38
|
+
*/
|
|
39
|
+
export declare const GENERATED_HEADER = "Generated by ia-qa-discover \u2014 every locator below is under contract";
|
|
40
|
+
export interface GeneratedLocator {
|
|
41
|
+
/** The field as a human reads it, for the report. */
|
|
42
|
+
field: string;
|
|
43
|
+
/** The Playwright expression, exactly as written into the file. */
|
|
44
|
+
locator: string;
|
|
45
|
+
/** True when the name came from the healing contract rather than the capture's own label. */
|
|
46
|
+
fromContract: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface SkippedField {
|
|
49
|
+
field: string;
|
|
50
|
+
why: string;
|
|
51
|
+
}
|
|
52
|
+
export interface GeneratedSpec {
|
|
53
|
+
page: string;
|
|
54
|
+
url: string;
|
|
55
|
+
code: string;
|
|
56
|
+
locators: GeneratedLocator[];
|
|
57
|
+
skipped: SkippedField[];
|
|
58
|
+
}
|
|
59
|
+
export declare function buildSpec(input: {
|
|
60
|
+
page: PageCapture;
|
|
61
|
+
contract: PageMapping | null;
|
|
62
|
+
}): GeneratedSpec;
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GENERATED_HEADER = void 0;
|
|
4
|
+
exports.buildSpec = buildSpec;
|
|
5
|
+
/**
|
|
6
|
+
* Turn a captured page into a test file that `@ia-qa/self-healing` can repair.
|
|
7
|
+
*
|
|
8
|
+
* The promise this exists to keep is not "we write your tests" — `playwright codegen` has
|
|
9
|
+
* done that for years — but **the tests we write are already maintained**: every locator
|
|
10
|
+
* names an element the contract holds, so when the app moves the file is *repaired* instead
|
|
11
|
+
* of deleted. That loop is proven end to end in
|
|
12
|
+
* `packages/self-healing/tests/corpus/generated.test.js`; this module produces the artifact
|
|
13
|
+
* that test validates by hand.
|
|
14
|
+
*
|
|
15
|
+
* Three rules are load-bearing, and each was paid for by a measurement rather than a
|
|
16
|
+
* preference.
|
|
17
|
+
*
|
|
18
|
+
* **1. Only calls that state a role.** `fix` rewrites `*ByRole`, `*ByLabel`,
|
|
19
|
+
* `*ByPlaceholder` and `By.linkText`, and nothing else: `getByText` names a *string*, not an
|
|
20
|
+
* element, so nothing can prove the test meant the renamed button rather than a heading that
|
|
21
|
+
* never moved. The corpus measured the consequence and it is worse than "that line will not
|
|
22
|
+
* be repaired" — **one** unrepairable line holds the whole verdict at BLOCK, which stops
|
|
23
|
+
* `fix` from repairing the repairable lines *beside* it. So this is not a preference order:
|
|
24
|
+
* a field that cannot be named by role, label or placeholder is **skipped**, never written
|
|
25
|
+
* in a weaker form.
|
|
26
|
+
*
|
|
27
|
+
* **2. Only fields carrying a durable identity.** `stableSelector: false` means the capture
|
|
28
|
+
* fell back to a positional selector, which addresses a different element the moment a field
|
|
29
|
+
* is inserted above it. Measured across both corpora: where a field has a `name`/`id`/
|
|
30
|
+
* test-id, discovery and healing independently synthesise the *same* selector (2 of 2 on a
|
|
31
|
+
* real login form); where it has none, they never agree (0 of 17). The join and the
|
|
32
|
+
* testability fail on exactly the same population, so refusing there costs nothing that
|
|
33
|
+
* could have worked.
|
|
34
|
+
*
|
|
35
|
+
* **3. The assertion is the human's.** An application declares what it *accepts* — required,
|
|
36
|
+
* type, min, max — and never what it *promises*. "A refused card shows the right message"
|
|
37
|
+
* exists in someone's head, and inventing it is how a generator produces a suite nobody
|
|
38
|
+
* trusts. What the app declares is asserted; the rest is a TODO with the question written
|
|
39
|
+
* out, because a silent gap reads as a finished test.
|
|
40
|
+
*/
|
|
41
|
+
exports.GENERATED_HEADER = 'Generated by ia-qa-discover — every locator below is under contract';
|
|
42
|
+
/** A label good enough to name an element by, and where it came from. */
|
|
43
|
+
function namedBy(field) {
|
|
44
|
+
if (!field.label)
|
|
45
|
+
return null;
|
|
46
|
+
switch (field.labelSource) {
|
|
47
|
+
case 'aria-label':
|
|
48
|
+
case 'aria-labelledby':
|
|
49
|
+
case 'label-for':
|
|
50
|
+
case 'label-wrapping':
|
|
51
|
+
return { call: 'getByLabel', text: field.label };
|
|
52
|
+
case 'placeholder':
|
|
53
|
+
return { call: 'getByPlaceholder', text: field.label };
|
|
54
|
+
// `title`, and text merely sitting near the field, name a string and not an element —
|
|
55
|
+
// the same reason `getByText` is refused. `proximity` is explicitly a guess.
|
|
56
|
+
default:
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const quote = (s) => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
61
|
+
/** Every field the page takes, whether or not the app wrapped it in a `<form>`. */
|
|
62
|
+
function allFields(page) {
|
|
63
|
+
return [...page.forms.flatMap((f) => f.fields), ...page.looseFields];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Pair a captured field with the contract element for it.
|
|
67
|
+
*
|
|
68
|
+
* By selector equality, which is not the fragile join it looks like: both packages derive a
|
|
69
|
+
* stable selector from the same identity attributes in the same priority, so on a field that
|
|
70
|
+
* has one they agree exactly. On a field that has none they never agree — and rule 2 has
|
|
71
|
+
* already skipped it.
|
|
72
|
+
*/
|
|
73
|
+
function contractFor(field, contract) {
|
|
74
|
+
return contract?.elements?.find((e) => e.selector === field.selector);
|
|
75
|
+
}
|
|
76
|
+
function buildSpec(input) {
|
|
77
|
+
const { page, contract } = input;
|
|
78
|
+
const locators = [];
|
|
79
|
+
const skipped = [];
|
|
80
|
+
for (const field of allFields(page)) {
|
|
81
|
+
const name = field.label || field.name || field.selector;
|
|
82
|
+
if (field.ambiguousSelector) {
|
|
83
|
+
skipped.push({ field: name, why: 'its selector matches more than one element — a locator coin flip' });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!field.stableSelector) {
|
|
87
|
+
skipped.push({
|
|
88
|
+
field: name,
|
|
89
|
+
why: 'no test id, id or name, so the only selector is positional — it would break on the next field inserted above it, and healing cannot repair it',
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (field.via?.length) {
|
|
94
|
+
// Reaching it needs the click path recorded at capture, and a generated test that
|
|
95
|
+
// navigates on its own is a second guess stacked on the first.
|
|
96
|
+
skipped.push({ field: name, why: `only reachable after: ${field.via.join(' → ')}` });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const element = contractFor(field, contract);
|
|
100
|
+
if (element?.name) {
|
|
101
|
+
locators.push({
|
|
102
|
+
field: name,
|
|
103
|
+
locator: `page.getByRole(${quote(element.role)}, { name: ${quote(element.name)} })`,
|
|
104
|
+
fromContract: true,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const byLabel = namedBy(field);
|
|
109
|
+
if (byLabel) {
|
|
110
|
+
locators.push({ field: name, locator: `page.${byLabel.call}(${quote(byLabel.text)})`, fromContract: false });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
skipped.push({
|
|
114
|
+
field: name,
|
|
115
|
+
why: contract
|
|
116
|
+
? 'the contract holds no name for it and the app labels it nowhere — nothing can name it by role'
|
|
117
|
+
: 'no contract for this page yet, and the app labels the field nowhere',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return { page: page.page, url: page.url, code: render(page, locators, skipped), locators, skipped };
|
|
121
|
+
}
|
|
122
|
+
function render(page, locators, skipped) {
|
|
123
|
+
const required = new Set(allFields(page)
|
|
124
|
+
.filter((f) => f.required)
|
|
125
|
+
.map((f) => f.label || f.name || f.selector));
|
|
126
|
+
const out = [];
|
|
127
|
+
out.push(`// ${exports.GENERATED_HEADER}, so healing repairs this file when the app moves.`);
|
|
128
|
+
out.push(`// Page: ${page.page} · ${page.url}`);
|
|
129
|
+
out.push(`//`);
|
|
130
|
+
out.push(`// Review before committing. The navigation and the actions are generated; the`);
|
|
131
|
+
out.push(`// assertions are yours — an app declares what it accepts, never what it promises.`);
|
|
132
|
+
if (skipped.length > 0) {
|
|
133
|
+
out.push(`//`);
|
|
134
|
+
out.push(`// Not generated (${skipped.length}):`);
|
|
135
|
+
for (const s of skipped)
|
|
136
|
+
out.push(`// · ${s.field} — ${s.why}`);
|
|
137
|
+
}
|
|
138
|
+
out.push(`import { test, expect } from '@playwright/test';`);
|
|
139
|
+
out.push('');
|
|
140
|
+
out.push(`test(${quote(`${page.page} — the page loads and takes its input`)}, async ({ page }) => {`);
|
|
141
|
+
out.push(` await page.goto(${quote(page.url)});`);
|
|
142
|
+
if (locators.length === 0) {
|
|
143
|
+
out.push('');
|
|
144
|
+
out.push(` // Nothing on this page could be named by role, label or placeholder.`);
|
|
145
|
+
out.push(` // TODO: what should this page show when it loads?`);
|
|
146
|
+
out.push(` await expect(page).toHaveURL(${quote(page.url)});`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
out.push('');
|
|
150
|
+
for (const l of locators) {
|
|
151
|
+
const star = required.has(l.field) ? ' (required)' : '';
|
|
152
|
+
out.push(` // ${l.field}${star}`);
|
|
153
|
+
out.push(` await expect(${l.locator}).toBeVisible();`);
|
|
154
|
+
}
|
|
155
|
+
out.push('');
|
|
156
|
+
out.push(` // TODO: fill these in and assert what the app should do.`);
|
|
157
|
+
out.push(` // What a valid submission produces, and what an invalid one says, is the one`);
|
|
158
|
+
out.push(` // thing no scan of this page could tell us.`);
|
|
159
|
+
}
|
|
160
|
+
out.push(`});`);
|
|
161
|
+
return out.join('\n') + '\n';
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=generate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.js","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ia-qa/qa-discovery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deterministic surface reconnaissance for an unknown web app — crawl, capture, diff. No LLM, no hosting: runs entirely on your machine, with a local MCP server for agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qa",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=18.0.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ia-qa/self-healing": "^1.
|
|
52
|
+
"@ia-qa/self-healing": "^1.17.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"playwright": ">=1.40.0"
|