@mandujs/ate 0.17.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 +1103 -0
- package/package.json +46 -0
- package/src/codegen.ts +140 -0
- package/src/dep-graph.ts +279 -0
- package/src/domain-detector.ts +194 -0
- package/src/extractor.ts +159 -0
- package/src/fs.ts +145 -0
- package/src/heal.ts +427 -0
- package/src/impact.ts +146 -0
- package/src/index.ts +112 -0
- package/src/ir.ts +24 -0
- package/src/oracle.ts +152 -0
- package/src/pipeline.ts +207 -0
- package/src/report.ts +129 -0
- package/src/reporter/html-template.ts +275 -0
- package/src/reporter/html.test.ts +155 -0
- package/src/reporter/html.ts +83 -0
- package/src/runner.ts +100 -0
- package/src/scenario.ts +71 -0
- package/src/selector-map.ts +191 -0
- package/src/trace-parser.ts +270 -0
- package/src/types.ts +106 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getAtePaths, readJson, writeJson } from "./fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Selector Map Schema
|
|
6
|
+
* Maps mandu-id to multiple selector strategies for resilient UI testing
|
|
7
|
+
*/
|
|
8
|
+
export interface SelectorMap {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
generatedAt: string;
|
|
11
|
+
entries: SelectorMapEntry[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectorMapEntry {
|
|
15
|
+
manduId: string;
|
|
16
|
+
file: string;
|
|
17
|
+
element: string; // e.g., "button", "input", "a"
|
|
18
|
+
primary: SelectorStrategy;
|
|
19
|
+
alternatives: SelectorStrategy[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SelectorStrategy {
|
|
23
|
+
type: "mandu-id" | "text" | "class" | "xpath" | "role";
|
|
24
|
+
value: string;
|
|
25
|
+
priority: number; // 0 = highest
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read selector-map.json from .mandu directory
|
|
30
|
+
*/
|
|
31
|
+
export function readSelectorMap(repoRoot: string): SelectorMap | null {
|
|
32
|
+
const paths = getAtePaths(repoRoot);
|
|
33
|
+
if (!existsSync(paths.selectorMapPath)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return readJson<SelectorMap>(paths.selectorMapPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write selector-map.json to .mandu directory
|
|
41
|
+
*/
|
|
42
|
+
export function writeSelectorMap(repoRoot: string, map: SelectorMap): void {
|
|
43
|
+
const paths = getAtePaths(repoRoot);
|
|
44
|
+
writeJson(paths.selectorMapPath, map);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize empty selector map
|
|
49
|
+
*/
|
|
50
|
+
export function initSelectorMap(): SelectorMap {
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
generatedAt: new Date().toISOString(),
|
|
54
|
+
entries: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add or update a selector entry
|
|
60
|
+
*/
|
|
61
|
+
export function addSelectorEntry(
|
|
62
|
+
map: SelectorMap,
|
|
63
|
+
entry: Omit<SelectorMapEntry, "alternatives"> & { alternatives?: SelectorStrategy[] }
|
|
64
|
+
): SelectorMap {
|
|
65
|
+
const existing = map.entries.findIndex((e) => e.manduId === entry.manduId);
|
|
66
|
+
|
|
67
|
+
const fullEntry: SelectorMapEntry = {
|
|
68
|
+
...entry,
|
|
69
|
+
alternatives: entry.alternatives ?? [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (existing >= 0) {
|
|
73
|
+
map.entries[existing] = fullEntry;
|
|
74
|
+
} else {
|
|
75
|
+
map.entries.push(fullEntry);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
map.generatedAt = new Date().toISOString();
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate alternative selectors for a given mandu-id element
|
|
84
|
+
*
|
|
85
|
+
* Fallback chain priority:
|
|
86
|
+
* 0. mandu-id (primary)
|
|
87
|
+
* 1. text-based (exact text match)
|
|
88
|
+
* 2. class-based (CSS class)
|
|
89
|
+
* 3. xpath (structural fallback)
|
|
90
|
+
* 4. role (ARIA role)
|
|
91
|
+
*/
|
|
92
|
+
export function generateAlternatives(opts: {
|
|
93
|
+
manduId: string;
|
|
94
|
+
element: string;
|
|
95
|
+
text?: string;
|
|
96
|
+
className?: string;
|
|
97
|
+
ariaRole?: string;
|
|
98
|
+
}): SelectorStrategy[] {
|
|
99
|
+
const alternatives: SelectorStrategy[] = [];
|
|
100
|
+
|
|
101
|
+
// Text-based selector
|
|
102
|
+
if (opts.text) {
|
|
103
|
+
alternatives.push({
|
|
104
|
+
type: "text",
|
|
105
|
+
value: `:has-text("${opts.text}")`,
|
|
106
|
+
priority: 1,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Class-based selector
|
|
111
|
+
if (opts.className) {
|
|
112
|
+
alternatives.push({
|
|
113
|
+
type: "class",
|
|
114
|
+
value: `.${opts.className}`,
|
|
115
|
+
priority: 2,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ARIA role selector
|
|
120
|
+
if (opts.ariaRole) {
|
|
121
|
+
alternatives.push({
|
|
122
|
+
type: "role",
|
|
123
|
+
value: `role=${opts.ariaRole}`,
|
|
124
|
+
priority: 3,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// XPath fallback (structural)
|
|
129
|
+
const xpathValue = `//${opts.element}[@data-mandu-id="${opts.manduId}"]`;
|
|
130
|
+
alternatives.push({
|
|
131
|
+
type: "xpath",
|
|
132
|
+
value: xpathValue,
|
|
133
|
+
priority: 4,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return alternatives.sort((a, b) => a.priority - b.priority);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get selector entry by mandu-id
|
|
141
|
+
*/
|
|
142
|
+
export function getSelectorEntry(
|
|
143
|
+
map: SelectorMap,
|
|
144
|
+
manduId: string
|
|
145
|
+
): SelectorMapEntry | undefined {
|
|
146
|
+
return map.entries.find((e) => e.manduId === manduId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build Playwright .or() chain from selector entry
|
|
151
|
+
* Returns: page.locator(primary).or(alt1).or(alt2)...
|
|
152
|
+
*/
|
|
153
|
+
export function buildPlaywrightLocatorChain(entry: SelectorMapEntry): string {
|
|
154
|
+
const primaryLocator = `page.locator('[data-mandu-id="${entry.manduId}"]')`;
|
|
155
|
+
|
|
156
|
+
if (entry.alternatives.length === 0) {
|
|
157
|
+
return primaryLocator;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const alternativeChains = entry.alternatives
|
|
161
|
+
.map((alt) => {
|
|
162
|
+
switch (alt.type) {
|
|
163
|
+
case "text":
|
|
164
|
+
return `page.locator('${entry.element}${alt.value}')`;
|
|
165
|
+
case "class":
|
|
166
|
+
return `page.locator('${entry.element}${alt.value}')`;
|
|
167
|
+
case "role":
|
|
168
|
+
return `page.getByRole('${alt.value.replace("role=", "")}')`;
|
|
169
|
+
case "xpath":
|
|
170
|
+
return `page.locator('xpath=${alt.value}')`;
|
|
171
|
+
default:
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.filter(Boolean);
|
|
176
|
+
|
|
177
|
+
if (alternativeChains.length === 0) {
|
|
178
|
+
return primaryLocator;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `${primaryLocator}.or(${alternativeChains.join(").or(")})`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove selector entry by mandu-id
|
|
186
|
+
*/
|
|
187
|
+
export function removeSelectorEntry(map: SelectorMap, manduId: string): SelectorMap {
|
|
188
|
+
map.entries = map.entries.filter((e) => e.manduId !== manduId);
|
|
189
|
+
map.generatedAt = new Date().toISOString();
|
|
190
|
+
return map;
|
|
191
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { gunzipSync } from "node:zlib";
|
|
3
|
+
import type { JsonValue, JsonObject } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface TraceAction {
|
|
6
|
+
type: string;
|
|
7
|
+
selector?: string;
|
|
8
|
+
method?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
params?: JsonObject;
|
|
11
|
+
beforeSnapshot?: string;
|
|
12
|
+
afterSnapshot?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FailedLocator {
|
|
16
|
+
selector: string;
|
|
17
|
+
error: string;
|
|
18
|
+
context?: string;
|
|
19
|
+
actionType?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TraceParseResult {
|
|
23
|
+
actions: TraceAction[];
|
|
24
|
+
failedLocators: FailedLocator[];
|
|
25
|
+
metadata: {
|
|
26
|
+
testFile?: string;
|
|
27
|
+
testName?: string;
|
|
28
|
+
timestamp?: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse Playwright trace.zip file
|
|
34
|
+
* Trace format: ZIP archive containing trace JSON + resources
|
|
35
|
+
*
|
|
36
|
+
* @param tracePath - Path to trace.zip file
|
|
37
|
+
* @returns Parsed trace with failed locators
|
|
38
|
+
*/
|
|
39
|
+
export function parseTrace(tracePath: string): TraceParseResult {
|
|
40
|
+
let content: Buffer;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
content = readFileSync(tracePath);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new Error(`Failed to read trace file: ${tracePath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Playwright trace.zip is a ZIP archive
|
|
49
|
+
// For MVP: assume trace is actually JSON (playwright-report.json)
|
|
50
|
+
// Full ZIP parsing would require JSZip or similar
|
|
51
|
+
|
|
52
|
+
let traceData: JsonValue;
|
|
53
|
+
try {
|
|
54
|
+
const text = content.toString("utf8");
|
|
55
|
+
traceData = JSON.parse(text);
|
|
56
|
+
} catch {
|
|
57
|
+
// Try gunzip if it's compressed
|
|
58
|
+
try {
|
|
59
|
+
const decompressed = gunzipSync(content);
|
|
60
|
+
const text = decompressed.toString("utf8");
|
|
61
|
+
traceData = JSON.parse(text);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new Error(`Failed to parse trace JSON: ${String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result: TraceParseResult = {
|
|
68
|
+
actions: [],
|
|
69
|
+
failedLocators: [],
|
|
70
|
+
metadata: {},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Parse Playwright report structure
|
|
74
|
+
if (typeof traceData !== "object" || !traceData || Array.isArray(traceData)) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const report = traceData as JsonObject;
|
|
79
|
+
|
|
80
|
+
// Extract metadata
|
|
81
|
+
if (typeof report.config === "object" && report.config) {
|
|
82
|
+
const config = report.config as JsonObject;
|
|
83
|
+
result.metadata.testFile = String(config.rootDir || "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse suites and tests
|
|
87
|
+
const suites = Array.isArray(report.suites) ? report.suites : [];
|
|
88
|
+
|
|
89
|
+
for (const suite of suites) {
|
|
90
|
+
if (typeof suite !== "object" || !suite) continue;
|
|
91
|
+
const suiteObj = suite as JsonObject;
|
|
92
|
+
|
|
93
|
+
const tests = Array.isArray(suiteObj.tests) ? suiteObj.tests : [];
|
|
94
|
+
|
|
95
|
+
for (const test of tests) {
|
|
96
|
+
if (typeof test !== "object" || !test) continue;
|
|
97
|
+
const testObj = test as JsonObject;
|
|
98
|
+
|
|
99
|
+
result.metadata.testName = String(testObj.title || "");
|
|
100
|
+
|
|
101
|
+
const results = Array.isArray(testObj.results) ? testObj.results : [];
|
|
102
|
+
|
|
103
|
+
for (const testResult of results) {
|
|
104
|
+
if (typeof testResult !== "object" || !testResult) continue;
|
|
105
|
+
const resultObj = testResult as JsonObject;
|
|
106
|
+
|
|
107
|
+
const steps = Array.isArray(resultObj.steps) ? resultObj.steps : [];
|
|
108
|
+
|
|
109
|
+
for (const step of steps) {
|
|
110
|
+
if (typeof step !== "object" || !step) continue;
|
|
111
|
+
const stepObj = step as JsonObject;
|
|
112
|
+
|
|
113
|
+
const action: TraceAction = {
|
|
114
|
+
type: String(stepObj.title || "unknown"),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Extract error info
|
|
118
|
+
if (stepObj.error) {
|
|
119
|
+
const errorObj = typeof stepObj.error === "object" && stepObj.error ? stepObj.error as JsonObject : {};
|
|
120
|
+
action.error = String(errorObj.message || stepObj.error);
|
|
121
|
+
|
|
122
|
+
// Parse locator from error message
|
|
123
|
+
const errorMsg = action.error;
|
|
124
|
+
const locatorMatch = errorMsg.match(/locator\(['"](.+?)['"]\)/);
|
|
125
|
+
if (locatorMatch) {
|
|
126
|
+
action.selector = locatorMatch[1];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Detect failed locator
|
|
130
|
+
if (errorMsg.includes("not found") || errorMsg.includes("timeout") || errorMsg.includes("failed")) {
|
|
131
|
+
const selector = action.selector || extractSelectorFromTitle(String(stepObj.title || ""));
|
|
132
|
+
|
|
133
|
+
if (selector) {
|
|
134
|
+
result.failedLocators.push({
|
|
135
|
+
selector,
|
|
136
|
+
error: errorMsg,
|
|
137
|
+
context: String(stepObj.title || ""),
|
|
138
|
+
actionType: detectActionType(String(stepObj.title || "")),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
result.actions.push(action);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract selector from step title
|
|
155
|
+
* Examples:
|
|
156
|
+
* - "click getByRole('button', { name: 'Submit' })" → "getByRole('button', { name: 'Submit' })"
|
|
157
|
+
* - "fill #username" → "#username"
|
|
158
|
+
*/
|
|
159
|
+
function extractSelectorFromTitle(title: string): string | null {
|
|
160
|
+
// getByRole, getByText, etc.
|
|
161
|
+
const playwrightMatch = title.match(/get\w+\([^)]+\)/);
|
|
162
|
+
if (playwrightMatch) {
|
|
163
|
+
return playwrightMatch[0];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// CSS selectors
|
|
167
|
+
const cssMatch = title.match(/[#.]\w[\w-]*/);
|
|
168
|
+
if (cssMatch) {
|
|
169
|
+
return cssMatch[0];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// XPath
|
|
173
|
+
const xpathMatch = title.match(/\/\/[\w/[\]@='".\s]+/);
|
|
174
|
+
if (xpathMatch) {
|
|
175
|
+
return xpathMatch[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Detect action type from step title
|
|
183
|
+
*/
|
|
184
|
+
function detectActionType(title: string): string {
|
|
185
|
+
const lower = title.toLowerCase();
|
|
186
|
+
|
|
187
|
+
if (lower.includes("click")) return "click";
|
|
188
|
+
if (lower.includes("fill") || lower.includes("type")) return "fill";
|
|
189
|
+
if (lower.includes("select")) return "select";
|
|
190
|
+
if (lower.includes("check")) return "check";
|
|
191
|
+
if (lower.includes("navigate") || lower.includes("goto")) return "navigate";
|
|
192
|
+
if (lower.includes("wait")) return "wait";
|
|
193
|
+
|
|
194
|
+
return "unknown";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generate alternative selectors for a failed locator
|
|
199
|
+
*
|
|
200
|
+
* @param selector - Original failed selector
|
|
201
|
+
* @param actionType - Type of action (click, fill, etc.)
|
|
202
|
+
* @returns Array of alternative selectors to try
|
|
203
|
+
*/
|
|
204
|
+
export function generateAlternativeSelectors(selector: string, actionType?: string): string[] {
|
|
205
|
+
const alternatives: string[] = [];
|
|
206
|
+
|
|
207
|
+
// CSS ID → alternatives
|
|
208
|
+
if (selector.startsWith("#")) {
|
|
209
|
+
const id = selector.slice(1);
|
|
210
|
+
alternatives.push(
|
|
211
|
+
`[data-testid="${id}"]`,
|
|
212
|
+
`[id="${id}"]`,
|
|
213
|
+
`[name="${id}"]`,
|
|
214
|
+
`[aria-label="${id}"]`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CSS class → alternatives
|
|
219
|
+
if (selector.startsWith(".")) {
|
|
220
|
+
const cls = selector.slice(1);
|
|
221
|
+
alternatives.push(
|
|
222
|
+
`[data-testid="${cls}"]`,
|
|
223
|
+
`.${cls}`,
|
|
224
|
+
`[class*="${cls}"]`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// getByRole → alternatives
|
|
229
|
+
if (selector.includes("getByRole")) {
|
|
230
|
+
const roleMatch = selector.match(/getByRole\(['"](\w+)['"]/);
|
|
231
|
+
const nameMatch = selector.match(/name:\s*['"](.+?)['"]/);
|
|
232
|
+
|
|
233
|
+
if (roleMatch && nameMatch) {
|
|
234
|
+
const role = roleMatch[1];
|
|
235
|
+
const name = nameMatch[1];
|
|
236
|
+
|
|
237
|
+
alternatives.push(
|
|
238
|
+
`getByRole('${role}', { name: /${name}/i })`,
|
|
239
|
+
`getByText('${name}')`,
|
|
240
|
+
`[aria-label="${name}"]`,
|
|
241
|
+
`button:has-text("${name}")`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// getByText → alternatives
|
|
247
|
+
if (selector.includes("getByText")) {
|
|
248
|
+
const textMatch = selector.match(/getByText\(['"](.+?)['"]/);
|
|
249
|
+
if (textMatch) {
|
|
250
|
+
const text = textMatch[1];
|
|
251
|
+
alternatives.push(
|
|
252
|
+
`getByText(/${text}/i)`,
|
|
253
|
+
`text=${text}`,
|
|
254
|
+
`:has-text("${text}")`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Generic fallbacks
|
|
260
|
+
if (actionType === "click" || actionType === "fill") {
|
|
261
|
+
alternatives.push(
|
|
262
|
+
`[data-testid="${selector}"]`,
|
|
263
|
+
`[aria-label="${selector}"]`,
|
|
264
|
+
`[name="${selector}"]`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Remove duplicates
|
|
269
|
+
return [...new Set(alternatives)];
|
|
270
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
2
|
+
export type JsonObject = { [k: string]: JsonValue };
|
|
3
|
+
|
|
4
|
+
export type OracleLevel = "L0" | "L1" | "L2" | "L3";
|
|
5
|
+
|
|
6
|
+
export interface AtePaths {
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
manduDir: string; // .mandu
|
|
9
|
+
interactionGraphPath: string;
|
|
10
|
+
selectorMapPath: string;
|
|
11
|
+
scenariosPath: string;
|
|
12
|
+
reportsDir: string;
|
|
13
|
+
autoE2eDir: string;
|
|
14
|
+
manualE2eDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExtractInput {
|
|
18
|
+
repoRoot: string;
|
|
19
|
+
tsconfigPath?: string;
|
|
20
|
+
routeGlobs?: string[];
|
|
21
|
+
buildSalt?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InteractionGraph {
|
|
25
|
+
schemaVersion: 1;
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
buildSalt: string;
|
|
28
|
+
nodes: InteractionNode[];
|
|
29
|
+
edges: InteractionEdge[];
|
|
30
|
+
stats: {
|
|
31
|
+
routes: number;
|
|
32
|
+
navigations: number;
|
|
33
|
+
modals: number;
|
|
34
|
+
actions: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type InteractionNode =
|
|
39
|
+
| { kind: "route"; id: string; file: string; path: string }
|
|
40
|
+
| { kind: "modal"; id: string; file: string; name: string }
|
|
41
|
+
| { kind: "action"; id: string; file: string; name: string };
|
|
42
|
+
|
|
43
|
+
export type InteractionEdge =
|
|
44
|
+
| { kind: "navigate"; from?: string; to: string; file: string; source: string }
|
|
45
|
+
| { kind: "openModal"; from?: string; modal: string; file: string; source: string }
|
|
46
|
+
| { kind: "runAction"; from?: string; action: string; file: string; source: string };
|
|
47
|
+
|
|
48
|
+
export interface GenerateInput {
|
|
49
|
+
repoRoot: string;
|
|
50
|
+
oracleLevel?: OracleLevel;
|
|
51
|
+
onlyRoutes?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RunInput {
|
|
55
|
+
repoRoot: string;
|
|
56
|
+
baseURL?: string;
|
|
57
|
+
ci?: boolean;
|
|
58
|
+
headless?: boolean;
|
|
59
|
+
browsers?: ("chromium" | "firefox" | "webkit")[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ImpactInput {
|
|
63
|
+
repoRoot: string;
|
|
64
|
+
base?: string;
|
|
65
|
+
head?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface HealInput {
|
|
69
|
+
repoRoot: string;
|
|
70
|
+
runId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SummaryJson {
|
|
74
|
+
schemaVersion: 1;
|
|
75
|
+
runId: string;
|
|
76
|
+
startedAt: string;
|
|
77
|
+
finishedAt: string;
|
|
78
|
+
ok: boolean;
|
|
79
|
+
oracle: {
|
|
80
|
+
level: OracleLevel;
|
|
81
|
+
l0: { ok: boolean; errors: string[] };
|
|
82
|
+
l1: { ok: boolean; signals: string[] };
|
|
83
|
+
l2: { ok: boolean; signals: string[] };
|
|
84
|
+
l3: { ok: boolean; notes: string[] };
|
|
85
|
+
};
|
|
86
|
+
playwright: {
|
|
87
|
+
exitCode: number;
|
|
88
|
+
reportDir: string;
|
|
89
|
+
jsonReportPath?: string;
|
|
90
|
+
junitPath?: string;
|
|
91
|
+
};
|
|
92
|
+
mandu: {
|
|
93
|
+
interactionGraphPath?: string;
|
|
94
|
+
selectorMapPath?: string;
|
|
95
|
+
scenariosPath?: string;
|
|
96
|
+
};
|
|
97
|
+
heal: {
|
|
98
|
+
attempted: boolean;
|
|
99
|
+
suggestions: Array<{ kind: string; title: string; diff: string }>;
|
|
100
|
+
};
|
|
101
|
+
impact: {
|
|
102
|
+
mode: "full" | "subset";
|
|
103
|
+
changedFiles: string[];
|
|
104
|
+
selectedRoutes: string[];
|
|
105
|
+
};
|
|
106
|
+
}
|