@ia-qa/qa-discovery 0.1.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 +378 -0
- package/ROADMAP.md +321 -0
- package/dist/ai/classify.d.ts +170 -0
- package/dist/ai/classify.js +431 -0
- package/dist/ai/classify.js.map +1 -0
- package/dist/browser/extractPage.d.ts +41 -0
- package/dist/browser/extractPage.js +427 -0
- package/dist/browser/extractPage.js.map +1 -0
- package/dist/browser/openables.d.ts +19 -0
- package/dist/browser/openables.js +127 -0
- package/dist/browser/openables.js.map +1 -0
- package/dist/capture/page.d.ts +128 -0
- package/dist/capture/page.js +78 -0
- package/dist/capture/page.js.map +1 -0
- package/dist/citations.d.ts +80 -0
- package/dist/citations.js +197 -0
- package/dist/citations.js.map +1 -0
- package/dist/classificationView.d.ts +118 -0
- package/dist/classificationView.js +178 -0
- package/dist/classificationView.js.map +1 -0
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +38 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/history.d.ts +1 -0
- package/dist/cli/history.js +35 -0
- package/dist/cli/history.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +172 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/login.d.ts +29 -0
- package/dist/cli/login.js +179 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/scan.d.ts +11 -0
- package/dist/cli/scan.js +246 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli-ai/index.d.ts +7 -0
- package/dist/cli-ai/index.js +360 -0
- package/dist/cli-ai/index.js.map +1 -0
- package/dist/config.d.ts +112 -0
- package/dist/config.js +178 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +12 -0
- package/dist/diff.js +43 -0
- package/dist/diff.js.map +1 -0
- package/dist/explore.d.ts +59 -0
- package/dist/explore.js +119 -0
- package/dist/explore.js.map +1 -0
- package/dist/healingLink.d.ts +30 -0
- package/dist/healingLink.js +127 -0
- package/dist/healingLink.js.map +1 -0
- package/dist/history.d.ts +38 -0
- package/dist/history.js +120 -0
- package/dist/history.js.map +1 -0
- package/dist/htmlReport.d.ts +2 -0
- package/dist/htmlReport.js +371 -0
- package/dist/htmlReport.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/sandbox.d.ts +9 -0
- package/dist/mcp/sandbox.js +82 -0
- package/dist/mcp/sandbox.js.map +1 -0
- package/dist/mcp/server.d.ts +120 -0
- package/dist/mcp/server.js +315 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/network.d.ts +60 -0
- package/dist/network.js +99 -0
- package/dist/network.js.map +1 -0
- package/dist/overview.d.ts +40 -0
- package/dist/overview.js +370 -0
- package/dist/overview.js.map +1 -0
- package/dist/overviewFile.d.ts +31 -0
- package/dist/overviewFile.js +101 -0
- package/dist/overviewFile.js.map +1 -0
- package/dist/scan.d.ts +159 -0
- package/dist/scan.js +402 -0
- package/dist/scan.js.map +1 -0
- package/dist/sharedCalls.d.ts +28 -0
- package/dist/sharedCalls.js +61 -0
- package/dist/sharedCalls.js.map +1 -0
- package/dist/taxonomy.d.ts +77 -0
- package/dist/taxonomy.js +153 -0
- package/dist/taxonomy.js.map +1 -0
- package/package.json +61 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
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.SESSION_FILENAME = exports.HISTORY_FILENAME = exports.CAPTURE_DIRNAME = exports.CONFIG_DIR = void 0;
|
|
37
|
+
exports.setBaseDir = setBaseDir;
|
|
38
|
+
exports.baseDir = baseDir;
|
|
39
|
+
exports.configPath = configPath;
|
|
40
|
+
exports.captureDir = captureDir;
|
|
41
|
+
exports.historyPath = historyPath;
|
|
42
|
+
exports.loadConfig = loadConfig;
|
|
43
|
+
exports.saveConfig = saveConfig;
|
|
44
|
+
exports.configExists = configExists;
|
|
45
|
+
exports.sessionPath = sessionPath;
|
|
46
|
+
exports.sessionTarget = sessionTarget;
|
|
47
|
+
exports.resolveSession = resolveSession;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
exports.CONFIG_DIR = '.ia-qa-discovery';
|
|
51
|
+
exports.CAPTURE_DIRNAME = 'capture';
|
|
52
|
+
exports.HISTORY_FILENAME = 'history.jsonl';
|
|
53
|
+
/**
|
|
54
|
+
* A browser session saved by `ia-qa-discover login`, in Playwright
|
|
55
|
+
* `storageState` shape. It holds live cookies: whoever has this file is logged
|
|
56
|
+
* in as that user. It is the one artefact in `.ia-qa-discovery/` that is a
|
|
57
|
+
* secret ā never uploaded anywhere, and `login` writes
|
|
58
|
+
* `.ia-qa-discovery/.gitignore` so a commit cannot carry it.
|
|
59
|
+
*/
|
|
60
|
+
exports.SESSION_FILENAME = 'session.json';
|
|
61
|
+
let _baseDir = null;
|
|
62
|
+
/** Set the base dir (the `--config <dir>` flag). `null` clears it back to the default. */
|
|
63
|
+
function setBaseDir(dir) {
|
|
64
|
+
_baseDir = dir === null ? null : path.resolve(dir);
|
|
65
|
+
}
|
|
66
|
+
function baseDir() {
|
|
67
|
+
if (_baseDir)
|
|
68
|
+
return _baseDir;
|
|
69
|
+
const fromEnv = process.env.IAQA_DISCOVERY_CONFIG_DIR;
|
|
70
|
+
return fromEnv ? path.resolve(fromEnv) : process.cwd();
|
|
71
|
+
}
|
|
72
|
+
function configPath(cwd = baseDir()) {
|
|
73
|
+
return path.join(cwd, exports.CONFIG_DIR, 'config.json');
|
|
74
|
+
}
|
|
75
|
+
function captureDir(cwd = baseDir()) {
|
|
76
|
+
return path.join(cwd, exports.CONFIG_DIR, exports.CAPTURE_DIRNAME);
|
|
77
|
+
}
|
|
78
|
+
function historyPath(cwd = baseDir()) {
|
|
79
|
+
return path.join(cwd, exports.CONFIG_DIR, exports.HISTORY_FILENAME);
|
|
80
|
+
}
|
|
81
|
+
/** Strip a leading UTF-8 BOM before `JSON.parse` ā Windows editors/PowerShell commonly inject one (same trap self-healing's config.ts documents). */
|
|
82
|
+
function stripBom(text) {
|
|
83
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
84
|
+
}
|
|
85
|
+
function loadConfig(cwd = baseDir()) {
|
|
86
|
+
const file = configPath(cwd);
|
|
87
|
+
if (!fs.existsSync(file)) {
|
|
88
|
+
throw new Error(`No ${exports.CONFIG_DIR}/config.json found in ${cwd}. Run \`ia-qa-discover scan <url> --save\` first.`);
|
|
89
|
+
}
|
|
90
|
+
const raw = JSON.parse(stripBom(fs.readFileSync(file, 'utf8')));
|
|
91
|
+
if (!raw.baseUrl) {
|
|
92
|
+
throw new Error(`${file} is malformed: "baseUrl" is required.`);
|
|
93
|
+
}
|
|
94
|
+
// A malformed entry here silently scans nothing, which reads as "the app has
|
|
95
|
+
// no such page" ā fail at load instead, like every other field.
|
|
96
|
+
if (raw.pages !== undefined) {
|
|
97
|
+
if (!Array.isArray(raw.pages)) {
|
|
98
|
+
throw new Error(`${file} is malformed: "pages" must be an array of { url, name? } when present.`);
|
|
99
|
+
}
|
|
100
|
+
raw.pages.forEach((p, i) => {
|
|
101
|
+
if (!p || typeof p.url !== 'string' || p.url.trim() === '') {
|
|
102
|
+
throw new Error(`${file} is malformed: pages[${i}] needs a non-empty "url" (a path like "/billing", or an absolute URL).`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
function saveConfig(config, cwd = baseDir()) {
|
|
109
|
+
const file = configPath(cwd);
|
|
110
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
111
|
+
fs.mkdirSync(captureDir(cwd), { recursive: true });
|
|
112
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
113
|
+
return file;
|
|
114
|
+
}
|
|
115
|
+
function configExists(cwd = baseDir()) {
|
|
116
|
+
return fs.existsSync(configPath(cwd));
|
|
117
|
+
}
|
|
118
|
+
function sessionPath(cwd = baseDir()) {
|
|
119
|
+
return path.join(cwd, exports.CONFIG_DIR, exports.SESSION_FILENAME);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Which session file to *write* ā `login`'s target. Same precedence as
|
|
123
|
+
* reading, minus the existence check: pointing `session` at a path is also a
|
|
124
|
+
* way of saying where you want it kept, so `login` refreshes THAT file rather
|
|
125
|
+
* than leaving a second one beside it for `scan` to have to choose between.
|
|
126
|
+
*/
|
|
127
|
+
function sessionTarget(config, explicit, cwd = baseDir()) {
|
|
128
|
+
if (explicit)
|
|
129
|
+
return { file: path.resolve(cwd, explicit), source: 'flag' };
|
|
130
|
+
const fromEnv = process.env.IAQA_DISCOVERY_SESSION;
|
|
131
|
+
if (fromEnv)
|
|
132
|
+
return { file: path.resolve(cwd, fromEnv), source: 'env' };
|
|
133
|
+
if (config?.session)
|
|
134
|
+
return { file: path.resolve(cwd, config.session), source: 'config' };
|
|
135
|
+
return { file: sessionPath(cwd), source: 'login' };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The session to *read*, or null when there is none to read.
|
|
139
|
+
*
|
|
140
|
+
* A session the user NAMED ā a flag, an env var, a config field ā that cannot
|
|
141
|
+
* be used is a hard error, never a silent fall-through. Ignoring it would
|
|
142
|
+
* capture the login page under the names of their real pages and exit 0, which
|
|
143
|
+
* is the false green this whole package must avoid. The default location is
|
|
144
|
+
* different: absent simply means nobody has run `login` yet.
|
|
145
|
+
*
|
|
146
|
+
* The shape check is deliberately shallow ā `cookies` is enough to tell a
|
|
147
|
+
* storageState from a config file pasted by mistake. The file is a secret: it
|
|
148
|
+
* is never read for anything else, never logged, never sent anywhere.
|
|
149
|
+
*/
|
|
150
|
+
function resolveSession(config, explicit, cwd = baseDir()) {
|
|
151
|
+
const choice = sessionTarget(config, explicit, cwd);
|
|
152
|
+
const named = choice.source === 'flag'
|
|
153
|
+
? '--session'
|
|
154
|
+
: choice.source === 'env'
|
|
155
|
+
? 'IAQA_DISCOVERY_SESSION'
|
|
156
|
+
: '"session" in config.json';
|
|
157
|
+
if (!fs.existsSync(choice.file)) {
|
|
158
|
+
if (choice.source === 'login')
|
|
159
|
+
return null;
|
|
160
|
+
throw new Error(`${named} points at ${choice.file}, which does not exist.\n` +
|
|
161
|
+
`A session that cannot be read is not a session: the scan would capture the login page ` +
|
|
162
|
+
`under your pages' names. Fix the path, or run \`ia-qa-discover login\` to make one.`);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(stripBom(fs.readFileSync(choice.file, 'utf8')));
|
|
166
|
+
if (!parsed || !Array.isArray(parsed.cookies)) {
|
|
167
|
+
throw new Error('no "cookies" array ā this is not a Playwright storageState file');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
172
|
+
throw new Error(`${named} points at ${choice.file}, which cannot be used: ${why}.\n` +
|
|
173
|
+
`Expected a Playwright storageState ā the file a \`globalSetup\` writes with ` +
|
|
174
|
+
`\`context.storageState({ path })\`, or the one \`ia-qa-discover login\` produces.`);
|
|
175
|
+
}
|
|
176
|
+
return choice;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PageCapture } from './capture/page';
|
|
2
|
+
/**
|
|
3
|
+
* Structural diff between two capture runs. No verdict ā F1 is purely
|
|
4
|
+
* descriptive (see plan §3): there is nothing here to PASS/FAIL, only a
|
|
5
|
+
* report of what changed, feeding `history.jsonl`.
|
|
6
|
+
*/
|
|
7
|
+
export interface CaptureDiff {
|
|
8
|
+
added: string[];
|
|
9
|
+
removed: string[];
|
|
10
|
+
changed: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function compareCaptures(previous: PageCapture[], current: PageCapture[]): CaptureDiff;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compareCaptures = compareCaptures;
|
|
4
|
+
/**
|
|
5
|
+
* A page's comparable content: meta, headings, forms, and the SET of API
|
|
6
|
+
* calls (method/urlPattern/sameOrigin only ā `count`/`status` are excluded as
|
|
7
|
+
* volatile, so a call firing once instead of twice, or a 200 instead of a
|
|
8
|
+
* cached 304, does not read as structural drift).
|
|
9
|
+
*/
|
|
10
|
+
function pageSignature(p) {
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
meta: p.meta,
|
|
13
|
+
headings: p.headings,
|
|
14
|
+
forms: p.forms,
|
|
15
|
+
looseFields: p.looseFields,
|
|
16
|
+
apiCalls: p.apiCalls.map((c) => ({ method: c.method, urlPattern: c.urlPattern, sameOrigin: c.sameOrigin })),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function compareCaptures(previous, current) {
|
|
20
|
+
const prevByName = new Map(previous.map((p) => [p.page, p]));
|
|
21
|
+
const curByName = new Map(current.map((p) => [p.page, p]));
|
|
22
|
+
const added = [];
|
|
23
|
+
const removed = [];
|
|
24
|
+
const changed = [];
|
|
25
|
+
for (const name of curByName.keys()) {
|
|
26
|
+
if (!prevByName.has(name))
|
|
27
|
+
added.push(name);
|
|
28
|
+
}
|
|
29
|
+
for (const name of prevByName.keys()) {
|
|
30
|
+
if (!curByName.has(name))
|
|
31
|
+
removed.push(name);
|
|
32
|
+
}
|
|
33
|
+
for (const [name, cur] of curByName) {
|
|
34
|
+
const prev = prevByName.get(name);
|
|
35
|
+
if (prev && pageSignature(prev) !== pageSignature(cur))
|
|
36
|
+
changed.push(name);
|
|
37
|
+
}
|
|
38
|
+
added.sort();
|
|
39
|
+
removed.sort();
|
|
40
|
+
changed.sort();
|
|
41
|
+
return { added, removed, changed };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=diff.js.map
|
package/dist/diff.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":";;AA8BA,0CAuBC;AAvCD;;;;;GAKG;AACH,SAAS,aAAa,CAAC,CAAc;IACnC,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;KAC5G,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,eAAe,CAAC,QAAuB,EAAE,OAAsB;IAC7E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAE3D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;QACpC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QACrC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,aAAa,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7E,CAAC;IAED,KAAK,CAAC,IAAI,EAAE,CAAC;IACb,OAAO,CAAC,IAAI,EAAE,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,CAAC;IACf,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type { FormField } from './capture/page';
|
|
3
|
+
/**
|
|
4
|
+
* One level of state exploration ā open what the page hides, capture the input
|
|
5
|
+
* fields it reveals.
|
|
6
|
+
*
|
|
7
|
+
* A loaded page is ONE state of a page. Measured on a real app: `/feature-manager`
|
|
8
|
+
* carried 64 buttons and 2 visible fields, so a load-only capture described about
|
|
9
|
+
* 3% of the input surface while looking complete. Since the input surface is where
|
|
10
|
+
* a test suite starts, that is the most expensive thing this package can get wrong.
|
|
11
|
+
*
|
|
12
|
+
* Four rules, each one a lesson `@ia-qa/self-healing` paid for in its own `--deep`:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Depth 1 only.** Their measurement: depth 1 finished in 59s over 43 clicks;
|
|
15
|
+
* depth 2 took 183s over 150 clicks and NEVER completed at any budget. A
|
|
16
|
+
* permanently partial capture is this package's own failure mode wearing a
|
|
17
|
+
* feature's clothes.
|
|
18
|
+
* 2. **Safety lives at the network layer.** Every non-GET request is aborted for
|
|
19
|
+
* the duration, routed on the PAGE (never the shared context), so a click
|
|
20
|
+
* cannot mutate anything server-side. The name denylist in `openables.js` is
|
|
21
|
+
* the second layer, for handlers that issue no request at all.
|
|
22
|
+
* 3. **It runs LAST**, after the normal capture and the network observation ā
|
|
23
|
+
* it leaves the page in whatever state the walk ended on.
|
|
24
|
+
* 4. **A selector may not name two elements.** Exploration unions captures taken
|
|
25
|
+
* in DIFFERENT states, and a positional selector addresses one element in
|
|
26
|
+
* state A and another in state B. Self-healing shipped this and got 78
|
|
27
|
+
* duplicate selectors on one page and 241 phantom renames on an unchanged
|
|
28
|
+
* app. A revealed field whose selector is already taken is dropped ā and
|
|
29
|
+
* counted, because reached-but-not-captured is a coverage loss and this
|
|
30
|
+
* package states those.
|
|
31
|
+
*/
|
|
32
|
+
export interface ExploreStats {
|
|
33
|
+
/** Controls that looked openable before any click. */
|
|
34
|
+
openable: number;
|
|
35
|
+
clicked: number;
|
|
36
|
+
/** Fields captured that were not visible at load. */
|
|
37
|
+
revealed: number;
|
|
38
|
+
/** Revealed fields dropped because their selector already named another element. */
|
|
39
|
+
collided: number;
|
|
40
|
+
/** Clicks that navigated ā the control was a link in disguise; we went back. */
|
|
41
|
+
navigated: number;
|
|
42
|
+
/** Non-GET requests refused during the walk. */
|
|
43
|
+
blockedMutations: number;
|
|
44
|
+
/** True when the budget ran out before every control was tried. */
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface ExploreResult {
|
|
48
|
+
revealed: FormField[];
|
|
49
|
+
stats: ExploreStats;
|
|
50
|
+
}
|
|
51
|
+
/** Count the openable controls without clicking any of them. */
|
|
52
|
+
export declare function countOpenables(page: Page): Promise<number>;
|
|
53
|
+
export interface ExploreOptions {
|
|
54
|
+
/** Max controls to click on this page. Default 60. */
|
|
55
|
+
budget?: number;
|
|
56
|
+
/** Reload the page to a known state ā used when a cheap restore fails. */
|
|
57
|
+
restore?: () => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export declare function exploreFields(page: Page, options?: ExploreOptions): Promise<ExploreResult>;
|
package/dist/explore.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.countOpenables = countOpenables;
|
|
4
|
+
exports.exploreFields = exploreFields;
|
|
5
|
+
const openables_1 = require("./browser/openables");
|
|
6
|
+
const extractPage_1 = require("./browser/extractPage");
|
|
7
|
+
/** Every field selector currently on the page, whatever container it sits in. */
|
|
8
|
+
function selectorsOf(e) {
|
|
9
|
+
const out = new Set();
|
|
10
|
+
for (const f of e.forms)
|
|
11
|
+
for (const fl of f.fields)
|
|
12
|
+
out.add(fl.selector);
|
|
13
|
+
for (const fl of e.looseFields)
|
|
14
|
+
out.add(fl.selector);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function fieldsOf(e) {
|
|
18
|
+
return [...e.forms.flatMap((f) => f.fields), ...e.looseFields];
|
|
19
|
+
}
|
|
20
|
+
/** Count the openable controls without clicking any of them. */
|
|
21
|
+
async function countOpenables(page) {
|
|
22
|
+
const found = (await page.evaluate(openables_1.findOpenablesInPage));
|
|
23
|
+
return found.length;
|
|
24
|
+
}
|
|
25
|
+
async function exploreFields(page, options = {}) {
|
|
26
|
+
const budget = options.budget ?? 60;
|
|
27
|
+
let blockedMutations = 0;
|
|
28
|
+
await page.route('**/*', (route) => {
|
|
29
|
+
if (route.request().method() === 'GET')
|
|
30
|
+
return route.continue();
|
|
31
|
+
blockedMutations++;
|
|
32
|
+
return route.abort();
|
|
33
|
+
});
|
|
34
|
+
const stats = {
|
|
35
|
+
openable: 0,
|
|
36
|
+
clicked: 0,
|
|
37
|
+
revealed: 0,
|
|
38
|
+
collided: 0,
|
|
39
|
+
navigated: 0,
|
|
40
|
+
blockedMutations: 0,
|
|
41
|
+
truncated: false,
|
|
42
|
+
};
|
|
43
|
+
const revealed = [];
|
|
44
|
+
try {
|
|
45
|
+
const baseline = (await page.evaluate(extractPage_1.extractPageInPage));
|
|
46
|
+
// Selectors already spoken for by the loaded state. A revealed field may not
|
|
47
|
+
// reuse one of these for a DIFFERENT element (rule 4).
|
|
48
|
+
// Keyed by selector, so a field appearing under a selector the loaded page
|
|
49
|
+
// already used can be told apart from that same field still being there ā
|
|
50
|
+
// by what it IS, not just where it sits.
|
|
51
|
+
const baselineBySelector = new Map();
|
|
52
|
+
for (const f of fieldsOf(baseline))
|
|
53
|
+
baselineBySelector.set(f.selector, f);
|
|
54
|
+
const taken = new Set(baselineBySelector.keys());
|
|
55
|
+
const openables = (await page.evaluate(openables_1.findOpenablesInPage));
|
|
56
|
+
stats.openable = openables.length;
|
|
57
|
+
stats.truncated = openables.length > budget;
|
|
58
|
+
for (const control of openables.slice(0, budget)) {
|
|
59
|
+
const urlBefore = page.url();
|
|
60
|
+
try {
|
|
61
|
+
await page.locator(control.selector).first().click({ timeout: 2000 });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue; // not clickable, detached, covered ā skipping is the safe outcome
|
|
65
|
+
}
|
|
66
|
+
stats.clicked++;
|
|
67
|
+
// A disclosure does not navigate. If the URL moved, this was a link in
|
|
68
|
+
// disguise: that page has its own capture, so step back rather than
|
|
69
|
+
// exploring from somewhere unexpected.
|
|
70
|
+
if (page.url() !== urlBefore) {
|
|
71
|
+
stats.navigated++;
|
|
72
|
+
await page.goBack({ waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => { });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
await page.waitForTimeout(200);
|
|
76
|
+
let after;
|
|
77
|
+
try {
|
|
78
|
+
after = (await page.evaluate(extractPage_1.extractPageInPage));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const field of fieldsOf(after)) {
|
|
84
|
+
const atLoad = baselineBySelector.get(field.selector);
|
|
85
|
+
if (atLoad) {
|
|
86
|
+
// The same field, still on screen ā not a discovery, skip silently.
|
|
87
|
+
if (atLoad.type === field.type && atLoad.name === field.name)
|
|
88
|
+
continue;
|
|
89
|
+
// A DIFFERENT field under a selector the loaded page already used.
|
|
90
|
+
// This is the dangerous one: emitting it would put a selector in the
|
|
91
|
+
// capture that names one element at load and another after a click.
|
|
92
|
+
stats.collided++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (taken.has(field.selector)) {
|
|
96
|
+
// A field that is NOT in the loaded page, appearing in an explored
|
|
97
|
+
// state, under a selector already spoken for. Reached and not
|
|
98
|
+
// captured ā a coverage loss, and this package states those rather
|
|
99
|
+
// than risking a selector that names two different elements.
|
|
100
|
+
stats.collided++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
taken.add(field.selector);
|
|
104
|
+
revealed.push({ ...field, via: [control.label || control.selector] });
|
|
105
|
+
stats.revealed++;
|
|
106
|
+
}
|
|
107
|
+
// Close whatever opened, cheaply. Escape dismisses most menus and dialogs;
|
|
108
|
+
// when it does not, the next control's click still works from wherever we
|
|
109
|
+
// are ā a walk, not a star. `restore` is the expensive fallback.
|
|
110
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await page.unroute('**/*').catch(() => { });
|
|
115
|
+
stats.blockedMutations = blockedMutations;
|
|
116
|
+
}
|
|
117
|
+
return { revealed, stats };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=explore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explore.js","sourceRoot":"","sources":["../src/explore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PageCapture } from './capture/page';
|
|
2
|
+
export interface HealingOverlap {
|
|
3
|
+
/** Pages `ia-qa-heal` holds a contract for. */
|
|
4
|
+
mapped: string[];
|
|
5
|
+
/** Pages this scan captured. */
|
|
6
|
+
scanned: string[];
|
|
7
|
+
/** Scanned, with no contract ā what exists and the suite does not hold. */
|
|
8
|
+
uncovered: string[];
|
|
9
|
+
/** Contracted, not reached by this scan ā never read as "gone", see below. */
|
|
10
|
+
unscanned: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compare what healing contracts against what this scan found, or null when
|
|
14
|
+
* this project does not use healing (the common case, and the silent one).
|
|
15
|
+
*/
|
|
16
|
+
export declare function healingOverlap(pages: PageCapture[], cwd?: string): HealingOverlap | null;
|
|
17
|
+
/**
|
|
18
|
+
* The lines the CLI prints. Empty when there is nothing true to say.
|
|
19
|
+
*
|
|
20
|
+
* Two restraints are load-bearing. **`uncovered` is not a verdict** ā a page
|
|
21
|
+
* with no contract may be deliberately out of scope, and calling it a coverage
|
|
22
|
+
* failure would make this the gate that discovery has spent its whole design
|
|
23
|
+
* refusing to be. And **`unscanned` is never reported as a page that vanished**:
|
|
24
|
+
* this scan is bounded by `--max`, by the crawl's reach and by whether a session
|
|
25
|
+
* was used, so a contracted page missing from it is far more likely to be a
|
|
26
|
+
* limit of the scan than a change in the app. Saying otherwise would manufacture
|
|
27
|
+
* exactly the false alarm the login-wall and href-blindness checks exist to
|
|
28
|
+
* prevent.
|
|
29
|
+
*/
|
|
30
|
+
export declare function healingLinkLines(overlap: HealingOverlap | null): string[];
|
|
@@ -0,0 +1,127 @@
|
|
|
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.healingOverlap = healingOverlap;
|
|
37
|
+
exports.healingLinkLines = healingLinkLines;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const config_1 = require("./config");
|
|
41
|
+
/**
|
|
42
|
+
* The one thing neither package can say alone.
|
|
43
|
+
*
|
|
44
|
+
* `@ia-qa/self-healing` knows what a suite *covers* ā it holds a contract per
|
|
45
|
+
* page its tests exercise. Discovery knows what *exists*. The gap between the
|
|
46
|
+
* two is the real coverage of the app, and it has been stated as an intention
|
|
47
|
+
* in both READMEs while nothing computed it: neither tool reads the other's
|
|
48
|
+
* artifacts.
|
|
49
|
+
*
|
|
50
|
+
* It is computable here and nowhere else, because this is the only moment both
|
|
51
|
+
* sets of files are on the same disk. And the two are directly comparable for a
|
|
52
|
+
* reason that was designed in rather than lucky: both resolve page names through
|
|
53
|
+
* the *same* `pageNameFromUrl` from `@ia-qa/self-healing`, so `checkout` in one
|
|
54
|
+
* is `checkout` in the other.
|
|
55
|
+
*
|
|
56
|
+
* **It is a fact, not a suggestion.** It is emitted only when `.ia-qa/mapping/`
|
|
57
|
+
* already exists ā i.e. the user already runs healing on this project. Naming
|
|
58
|
+
* the sibling package to anyone else, at the end of a run that succeeded, would
|
|
59
|
+
* be advertising wearing a finding's clothes, and this package treats a message
|
|
60
|
+
* that fires on the good case as a defect (it is why `nameMask` exists, and why
|
|
61
|
+
* the classification caveat is worded softer than the deterministic one).
|
|
62
|
+
*/
|
|
63
|
+
/** Underscore-prefixed files are healing's own indexes, not pages. */
|
|
64
|
+
function contractPages(dir) {
|
|
65
|
+
if (!fs.existsSync(dir))
|
|
66
|
+
return [];
|
|
67
|
+
return fs
|
|
68
|
+
.readdirSync(dir)
|
|
69
|
+
.filter((e) => e.endsWith('.json') && !e.startsWith('_'))
|
|
70
|
+
.map((e) => e.slice(0, -'.json'.length))
|
|
71
|
+
.sort();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Compare what healing contracts against what this scan found, or null when
|
|
75
|
+
* this project does not use healing (the common case, and the silent one).
|
|
76
|
+
*/
|
|
77
|
+
function healingOverlap(pages, cwd = (0, config_1.baseDir)()) {
|
|
78
|
+
const mapped = contractPages(path.join(cwd, '.ia-qa', 'mapping'));
|
|
79
|
+
if (mapped.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const scanned = pages.map((p) => p.page).sort();
|
|
82
|
+
const mappedSet = new Set(mapped);
|
|
83
|
+
const scannedSet = new Set(scanned);
|
|
84
|
+
return {
|
|
85
|
+
mapped,
|
|
86
|
+
scanned,
|
|
87
|
+
uncovered: scanned.filter((p) => !mappedSet.has(p)),
|
|
88
|
+
unscanned: mapped.filter((p) => !scannedSet.has(p)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The lines the CLI prints. Empty when there is nothing true to say.
|
|
93
|
+
*
|
|
94
|
+
* Two restraints are load-bearing. **`uncovered` is not a verdict** ā a page
|
|
95
|
+
* with no contract may be deliberately out of scope, and calling it a coverage
|
|
96
|
+
* failure would make this the gate that discovery has spent its whole design
|
|
97
|
+
* refusing to be. And **`unscanned` is never reported as a page that vanished**:
|
|
98
|
+
* this scan is bounded by `--max`, by the crawl's reach and by whether a session
|
|
99
|
+
* was used, so a contracted page missing from it is far more likely to be a
|
|
100
|
+
* limit of the scan than a change in the app. Saying otherwise would manufacture
|
|
101
|
+
* exactly the false alarm the login-wall and href-blindness checks exist to
|
|
102
|
+
* prevent.
|
|
103
|
+
*/
|
|
104
|
+
function healingLinkLines(overlap) {
|
|
105
|
+
if (!overlap)
|
|
106
|
+
return [];
|
|
107
|
+
const { mapped, scanned, uncovered, unscanned } = overlap;
|
|
108
|
+
const out = [];
|
|
109
|
+
out.push(`\nš This project also uses \`ia-qa-heal\`: it holds contracts for ${mapped.length} page(s), ` +
|
|
110
|
+
`this scan found ${scanned.length}.`);
|
|
111
|
+
if (uncovered.length > 0) {
|
|
112
|
+
const shown = uncovered.slice(0, 6);
|
|
113
|
+
out.push(` ${uncovered.length} page(s) exist here with no contract: ${shown.map((p) => `\`${p}\``).join(', ')}` +
|
|
114
|
+
(uncovered.length > shown.length ? `, ā¦` : ''));
|
|
115
|
+
out.push(` That is the gap between what the app has and what the suite holds ā not a verdict:`);
|
|
116
|
+
out.push(` a page with no contract may be deliberately out of scope. You decide which.`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
out.push(` Every page this scan reached already has a contract.`);
|
|
120
|
+
}
|
|
121
|
+
if (unscanned.length > 0) {
|
|
122
|
+
out.push(` ${unscanned.length} contracted page(s) were NOT reached by this scan ā a limit of the scan ` +
|
|
123
|
+
`(\`--max\`,\n crawl reach, session), not evidence they are gone.`);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=healingLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"healingLink.js","sourceRoot":"","sources":["../src/healingLink.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only run history ā `.ia-qa-discovery/history.jsonl`. Mirrors
|
|
3
|
+
* `@ia-qa/self-healing`'s `history.ts`: small, deterministic, safe to commit,
|
|
4
|
+
* never influences anything (F1 produces no verdict to influence), a write
|
|
5
|
+
* failure is swallowed (a full disk must not crash a `scan`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const HISTORY_LIMIT = 500;
|
|
8
|
+
export interface ScanHistoryEntry {
|
|
9
|
+
ts: string;
|
|
10
|
+
verb: 'scan';
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
pagesFound: number;
|
|
13
|
+
pagesCaptured: number;
|
|
14
|
+
pagesFailed: number;
|
|
15
|
+
added: string[];
|
|
16
|
+
removed: string[];
|
|
17
|
+
changed: string[];
|
|
18
|
+
formsTotal: number;
|
|
19
|
+
/** Every input field, inside a `<form>` or not ā see PageCapture.looseFields. */
|
|
20
|
+
fieldsTotal?: number;
|
|
21
|
+
apiCallsTotal: number;
|
|
22
|
+
commit?: string;
|
|
23
|
+
branch?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function historyPath(cwd?: string): string;
|
|
26
|
+
export declare function appendHistory(entry: Omit<ScanHistoryEntry, 'ts'> & {
|
|
27
|
+
ts?: string;
|
|
28
|
+
}): void;
|
|
29
|
+
/** Read the history, oldest first. A malformed line is skipped, not fatal. */
|
|
30
|
+
export declare function readHistory(limit?: number, cwd?: string): ScanHistoryEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort git stamp ā never throws, never blocks. No git, no repo,
|
|
33
|
+
* detached HEAD: the entry is written without it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function gitStamp(): {
|
|
36
|
+
commit?: string;
|
|
37
|
+
branch?: string;
|
|
38
|
+
};
|