@ia-qa/qa-discovery 0.2.3 → 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/TUTORIAL.md +457 -433
- 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 +22 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/coverage.d.ts +52 -2
- package/dist/coverage.js +72 -8
- 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/dist/mcp/server.js +1 -1
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -2
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 (
|
|
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
|
|
@@ -28,7 +29,7 @@ page count as coverage: if scan warns that pages still show a login form, you
|
|
|
28
29
|
measured the wall, not the app. You CANNOT run \`login\` — it waits for a human
|
|
29
30
|
at a browser; ask them to run it, then scan again.
|
|
30
31
|
\`coverage\` answers "what does their suite NOT test" — but only from a suite that was
|
|
31
|
-
watched running (
|
|
32
|
+
watched running (one \`ia-qa-heal run\`, or one \`ia-qa-pal tour --suite\`). If it says there is no
|
|
32
33
|
measurement, report that, never "the suite covers nothing": a contract means someone
|
|
33
34
|
configured a page, only a watched run means a test went there.
|
|
34
35
|
|
|
@@ -72,7 +73,8 @@ Usage:
|
|
|
72
73
|
was OBSERVED visiting, ranks the gap by what each
|
|
73
74
|
page takes as input, and writes coverage-map.json.
|
|
74
75
|
Reads local files only — no browser, no network.
|
|
75
|
-
Needs
|
|
76
|
+
Needs one watched run of your suite —
|
|
77
|
+
"ia-qa-heal run" or "ia-qa-pal tour --suite":
|
|
76
78
|
without a watched run there is no measurement, and
|
|
77
79
|
it says so instead of showing a number.
|
|
78
80
|
Declare pages you deliberately do not test in
|
|
@@ -82,6 +84,21 @@ Usage:
|
|
|
82
84
|
--report [file.html] branded HTML dossier with the gap
|
|
83
85
|
(default ia-qa-discover-coverage.html)
|
|
84
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
|
|
85
102
|
ia-qa-discover history Recorded scan runs — pages/forms/API-surface trend.
|
|
86
103
|
--json --limit <n> (default 500)
|
|
87
104
|
ia-qa-discover version Print the installed version (also --version, -v)
|
|
@@ -157,6 +174,9 @@ async function main() {
|
|
|
157
174
|
case 'coverage':
|
|
158
175
|
await (0, coverage_1.runCoverage)(rest);
|
|
159
176
|
break;
|
|
177
|
+
case 'generate':
|
|
178
|
+
await (0, generate_1.runGenerate)(rest);
|
|
179
|
+
break;
|
|
160
180
|
case 'history':
|
|
161
181
|
await (0, history_1.runHistory)(rest);
|
|
162
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;
|