@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
package/README.md
ADDED
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
# @mandujs/ate - Automation Test Engine
|
|
2
|
+
|
|
3
|
+
**자동화 테스트 엔진**: Extract → Generate → Run → Report → Heal → Impact 전체 파이프라인을 하나의 패키지로 제공합니다.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MPL-2.0)
|
|
6
|
+
[](https://bun.sh)
|
|
7
|
+
[](https://playwright.dev)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 📖 Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Quick Start](#-quick-start)
|
|
14
|
+
- [Architecture](#-architecture)
|
|
15
|
+
- [API Reference](#-api-reference)
|
|
16
|
+
- [Examples](#-examples)
|
|
17
|
+
- [Oracle Levels](#-oracle-levels)
|
|
18
|
+
- [Roadmap](#-roadmap)
|
|
19
|
+
- [Troubleshooting](#-troubleshooting)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🚀 Quick Start
|
|
24
|
+
|
|
25
|
+
### Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun add -d @mandujs/ate @playwright/test playwright
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### First Test in 5 Minutes
|
|
32
|
+
|
|
33
|
+
**1. Extract Interaction Graph**
|
|
34
|
+
|
|
35
|
+
ATE는 코드베이스를 정적 분석하여 라우트, 네비게이션, 모달, 액션 관계를 추출합니다.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { ateExtract } from "@mandujs/ate";
|
|
39
|
+
|
|
40
|
+
await ateExtract({
|
|
41
|
+
repoRoot: process.cwd(),
|
|
42
|
+
routeGlobs: ["app/**/page.tsx"],
|
|
43
|
+
buildSalt: "dev",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Output: .mandu/interaction-graph.json
|
|
47
|
+
// {
|
|
48
|
+
// "nodes": [{ kind: "route", id: "/", file: "app/page.tsx", path: "/" }],
|
|
49
|
+
// "edges": [{ kind: "navigate", from: "/", to: "/about", ... }]
|
|
50
|
+
// }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**2. Generate Test Scenarios**
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { ateGenerate } from "@mandujs/ate";
|
|
57
|
+
|
|
58
|
+
ateGenerate({
|
|
59
|
+
repoRoot: process.cwd(),
|
|
60
|
+
oracleLevel: "L1", // L0 | L1 | L2 | L3
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Output:
|
|
64
|
+
// - .mandu/scenarios.json
|
|
65
|
+
// - tests/e2e/auto/*.spec.ts (Playwright test files)
|
|
66
|
+
// - tests/e2e/playwright.config.ts
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**3. Run Tests**
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { ateRun } from "@mandujs/ate";
|
|
73
|
+
|
|
74
|
+
const result = await ateRun({
|
|
75
|
+
repoRoot: process.cwd(),
|
|
76
|
+
baseURL: "http://localhost:3333",
|
|
77
|
+
ci: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(result.exitCode); // 0 = pass, 1 = fail
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**4. Generate Report**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { ateReport } from "@mandujs/ate";
|
|
87
|
+
|
|
88
|
+
const report = await ateReport({
|
|
89
|
+
repoRoot: process.cwd(),
|
|
90
|
+
runId: result.runId,
|
|
91
|
+
startedAt: result.startedAt,
|
|
92
|
+
finishedAt: result.finishedAt,
|
|
93
|
+
exitCode: result.exitCode,
|
|
94
|
+
oracleLevel: "L1",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log(report.summaryPath);
|
|
98
|
+
// .mandu/reports/run-1234567890/summary.json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**5. Heal Failed Tests (Optional)**
|
|
102
|
+
|
|
103
|
+
테스트가 실패하면 ATE가 자동으로 대체 셀렉터를 제안합니다.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { ateHeal } from "@mandujs/ate";
|
|
107
|
+
|
|
108
|
+
const healing = ateHeal({
|
|
109
|
+
repoRoot: process.cwd(),
|
|
110
|
+
runId: result.runId,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
healing.suggestions.forEach((s) => {
|
|
114
|
+
console.log(s.title);
|
|
115
|
+
console.log(s.diff); // unified diff format
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🏗️ Architecture
|
|
122
|
+
|
|
123
|
+
ATE는 6개의 핵심 모듈로 구성되어 있습니다.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
┌──────────────┐
|
|
127
|
+
│ Extractor │ Static analysis (ts-morph) → Interaction Graph
|
|
128
|
+
└──────┬───────┘
|
|
129
|
+
↓
|
|
130
|
+
┌──────────────┐
|
|
131
|
+
│ Generator │ Graph → Scenarios → Playwright Specs (codegen)
|
|
132
|
+
└──────┬───────┘
|
|
133
|
+
↓
|
|
134
|
+
┌──────────────┐
|
|
135
|
+
│ Runner │ Execute Playwright tests (bunx playwright test)
|
|
136
|
+
└──────┬───────┘
|
|
137
|
+
↓
|
|
138
|
+
┌──────────────┐
|
|
139
|
+
│ Reporter │ Compose summary.json (oracle results + metadata)
|
|
140
|
+
└──────┬───────┘
|
|
141
|
+
↓
|
|
142
|
+
┌──────────────┐
|
|
143
|
+
│ Healer │ Parse failures → Generate selector alternatives
|
|
144
|
+
└──────────────┘
|
|
145
|
+
|
|
146
|
+
┌──────────────┐
|
|
147
|
+
│ Impact │ git diff → Affected routes (subset testing)
|
|
148
|
+
└──────────────┘
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Module Responsibilities
|
|
152
|
+
|
|
153
|
+
| Module | Input | Output | Purpose |
|
|
154
|
+
|--------|-------|--------|---------|
|
|
155
|
+
| **Extractor** | Route files (.tsx) | interaction-graph.json | 정적 분석으로 네비게이션 관계 추출 |
|
|
156
|
+
| **Generator** | Interaction graph | Playwright specs | 테스트 시나리오 및 코드 생성 |
|
|
157
|
+
| **Runner** | Playwright config | Test results | Playwright 실행 래퍼 |
|
|
158
|
+
| **Reporter** | Run metadata | summary.json | Oracle 결과 및 메타데이터 집계 |
|
|
159
|
+
| **Healer** | Failed test traces | Selector suggestions | 실패한 테스트 복구 제안 |
|
|
160
|
+
| **Impact** | git diff | Affected routes | 변경 영향 분석 (subset test) |
|
|
161
|
+
|
|
162
|
+
### File Structure
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
.mandu/
|
|
166
|
+
├── interaction-graph.json # Extracted navigation graph
|
|
167
|
+
├── selector-map.json # Stable selectors + fallbacks
|
|
168
|
+
├── scenarios.json # Generated test scenarios
|
|
169
|
+
└── reports/
|
|
170
|
+
├── latest/ # Symlink to most recent run
|
|
171
|
+
│ ├── playwright-html/
|
|
172
|
+
│ ├── playwright-report.json
|
|
173
|
+
│ └── junit.xml
|
|
174
|
+
└── run-1234567890/
|
|
175
|
+
├── summary.json # ATE summary (oracle + metadata)
|
|
176
|
+
└── run.json # Run metadata
|
|
177
|
+
|
|
178
|
+
tests/e2e/
|
|
179
|
+
├── playwright.config.ts
|
|
180
|
+
├── auto/ # Auto-generated specs
|
|
181
|
+
│ ├── route___.spec.ts
|
|
182
|
+
│ └── route__about.spec.ts
|
|
183
|
+
└── manual/ # User-written specs
|
|
184
|
+
└── custom.spec.ts
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 📚 API Reference
|
|
190
|
+
|
|
191
|
+
### `ateExtract(input: ExtractInput)`
|
|
192
|
+
|
|
193
|
+
코드베이스를 정적 분석하여 인터랙션 그래프를 추출합니다.
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
interface ExtractInput {
|
|
199
|
+
repoRoot: string; // 프로젝트 루트 경로
|
|
200
|
+
tsconfigPath?: string; // tsconfig.json 경로 (선택)
|
|
201
|
+
routeGlobs?: string[]; // 라우트 파일 glob 패턴 (기본: ["app/**/page.tsx"])
|
|
202
|
+
buildSalt?: string; // Build ID (기본: "dev")
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Returns:**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
Promise<{
|
|
210
|
+
ok: true;
|
|
211
|
+
graphPath: string; // .mandu/interaction-graph.json
|
|
212
|
+
summary: {
|
|
213
|
+
nodes: number; // 추출된 노드 수 (route, modal, action)
|
|
214
|
+
edges: number; // 추출된 엣지 수 (navigate, openModal, runAction)
|
|
215
|
+
};
|
|
216
|
+
}>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Example:**
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const result = await ateExtract({
|
|
223
|
+
repoRoot: "/path/to/project",
|
|
224
|
+
routeGlobs: ["app/**/page.tsx", "routes/**/page.tsx"],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
console.log(`Extracted ${result.summary.nodes} nodes, ${result.summary.edges} edges`);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Supported Patterns:**
|
|
231
|
+
|
|
232
|
+
- `<Link href="/path">` (Next.js Link)
|
|
233
|
+
- `<ManduLink to="/path">` (Mandu Link)
|
|
234
|
+
- `mandu.navigate("/path")`
|
|
235
|
+
- `mandu.modal.open("modalName")`
|
|
236
|
+
- `mandu.action.run("actionName")`
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### `ateGenerate(input: GenerateInput)`
|
|
241
|
+
|
|
242
|
+
인터랙션 그래프로부터 테스트 시나리오와 Playwright 스펙을 생성합니다.
|
|
243
|
+
|
|
244
|
+
**Parameters:**
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
interface GenerateInput {
|
|
248
|
+
repoRoot: string;
|
|
249
|
+
oracleLevel?: OracleLevel; // "L0" | "L1" | "L2" | "L3" (기본: "L1")
|
|
250
|
+
onlyRoutes?: string[]; // 특정 라우트만 생성 (선택)
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Returns:**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
{
|
|
258
|
+
ok: true;
|
|
259
|
+
scenariosPath: string; // .mandu/scenarios.json
|
|
260
|
+
generatedSpecs: string[]; // tests/e2e/auto/*.spec.ts
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Example:**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const result = ateGenerate({
|
|
268
|
+
repoRoot: process.cwd(),
|
|
269
|
+
oracleLevel: "L1",
|
|
270
|
+
onlyRoutes: ["/", "/about"], // Optional: 특정 라우트만
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
console.log(`Generated ${result.generatedSpecs.length} test specs`);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Generated Test Example (L1 Oracle):**
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// tests/e2e/auto/route___.spec.ts
|
|
280
|
+
import { test, expect } from "@playwright/test";
|
|
281
|
+
|
|
282
|
+
test.describe("route:/", () => {
|
|
283
|
+
test("smoke /", async ({ page, baseURL }) => {
|
|
284
|
+
const url = (baseURL ?? "http://localhost:3333") + "/";
|
|
285
|
+
|
|
286
|
+
// L0: no console.error / uncaught exception / 5xx
|
|
287
|
+
const errors: string[] = [];
|
|
288
|
+
page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); });
|
|
289
|
+
page.on("pageerror", (err) => errors.push(String(err)));
|
|
290
|
+
|
|
291
|
+
await page.goto(url);
|
|
292
|
+
|
|
293
|
+
// L1: structure signals
|
|
294
|
+
await expect(page.locator("main")).toHaveCount(1);
|
|
295
|
+
expect(errors, "console/page errors").toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### `ateRun(input: RunInput)`
|
|
303
|
+
|
|
304
|
+
생성된 Playwright 테스트를 실행합니다.
|
|
305
|
+
|
|
306
|
+
**Parameters:**
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
interface RunInput {
|
|
310
|
+
repoRoot: string;
|
|
311
|
+
baseURL?: string; // 기본: "http://localhost:3333"
|
|
312
|
+
ci?: boolean; // CI 모드 (trace, video 설정)
|
|
313
|
+
headless?: boolean; // Headless 브라우저 (기본: true)
|
|
314
|
+
browsers?: Array<"chromium" | "firefox" | "webkit">;
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Returns:**
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
Promise<{
|
|
322
|
+
ok: boolean; // exitCode === 0
|
|
323
|
+
runId: string; // run-1234567890
|
|
324
|
+
reportDir: string; // .mandu/reports/run-1234567890
|
|
325
|
+
exitCode: number; // 0 = pass, 1 = fail
|
|
326
|
+
jsonReportPath?: string;
|
|
327
|
+
junitPath?: string;
|
|
328
|
+
startedAt: string; // ISO 8601
|
|
329
|
+
finishedAt: string;
|
|
330
|
+
}>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Example:**
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
const result = await ateRun({
|
|
337
|
+
repoRoot: process.cwd(),
|
|
338
|
+
baseURL: "http://localhost:3000",
|
|
339
|
+
ci: process.env.CI === "true",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (result.exitCode !== 0) {
|
|
343
|
+
console.error("Tests failed!");
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### `ateReport(params)`
|
|
351
|
+
|
|
352
|
+
테스트 실행 결과를 요약하여 리포트를 생성합니다.
|
|
353
|
+
|
|
354
|
+
**Parameters:**
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
{
|
|
358
|
+
repoRoot: string;
|
|
359
|
+
runId: string;
|
|
360
|
+
startedAt: string;
|
|
361
|
+
finishedAt: string;
|
|
362
|
+
exitCode: number;
|
|
363
|
+
oracleLevel: OracleLevel;
|
|
364
|
+
impact?: {
|
|
365
|
+
changedFiles: string[];
|
|
366
|
+
selectedRoutes: string[];
|
|
367
|
+
mode: "full" | "subset";
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Returns:**
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
Promise<{
|
|
376
|
+
ok: true;
|
|
377
|
+
summaryPath: string; // .mandu/reports/run-XXX/summary.json
|
|
378
|
+
summary: SummaryJson;
|
|
379
|
+
}>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Example:**
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
const report = await ateReport({
|
|
386
|
+
repoRoot: process.cwd(),
|
|
387
|
+
runId: result.runId,
|
|
388
|
+
startedAt: result.startedAt,
|
|
389
|
+
finishedAt: result.finishedAt,
|
|
390
|
+
exitCode: result.exitCode,
|
|
391
|
+
oracleLevel: "L1",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
console.log(report.summary.ok ? "✅ All tests passed" : "❌ Tests failed");
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### `ateHeal(input: HealInput)`
|
|
400
|
+
|
|
401
|
+
실패한 테스트의 trace를 분석하여 대체 셀렉터를 제안합니다.
|
|
402
|
+
|
|
403
|
+
**Parameters:**
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
interface HealInput {
|
|
407
|
+
repoRoot: string;
|
|
408
|
+
runId: string; // ateRun의 runId
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Returns:**
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
{
|
|
416
|
+
ok: true;
|
|
417
|
+
attempted: true;
|
|
418
|
+
suggestions: HealSuggestion[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
interface HealSuggestion {
|
|
422
|
+
kind: "selector-map" | "test-code" | "note";
|
|
423
|
+
title: string;
|
|
424
|
+
diff: string; // Unified diff format
|
|
425
|
+
metadata?: {
|
|
426
|
+
selector?: string;
|
|
427
|
+
alternatives?: string[];
|
|
428
|
+
testFile?: string;
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Example:**
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const healing = ateHeal({
|
|
437
|
+
repoRoot: process.cwd(),
|
|
438
|
+
runId: "run-1234567890",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
healing.suggestions.forEach((s) => {
|
|
442
|
+
console.log(`[${s.kind}] ${s.title}`);
|
|
443
|
+
console.log(s.diff);
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Sample Output:**
|
|
448
|
+
|
|
449
|
+
```diff
|
|
450
|
+
[selector-map] Update selector-map for: button.submit
|
|
451
|
+
--- a/.mandu/selector-map.json
|
|
452
|
+
+++ b/.mandu/selector-map.json
|
|
453
|
+
@@ -1,3 +1,8 @@
|
|
454
|
+
{
|
|
455
|
+
+ "button.submit": {
|
|
456
|
+
+ "fallbacks": [
|
|
457
|
+
+ "button[type='submit']",
|
|
458
|
+
+ "[data-testid='submit-button']"
|
|
459
|
+
+ ]
|
|
460
|
+
+ },
|
|
461
|
+
"version": "1.0.0"
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
### `ateImpact(input: ImpactInput)`
|
|
468
|
+
|
|
469
|
+
git diff를 분석하여 영향받는 라우트를 계산합니다. (Subset Testing)
|
|
470
|
+
|
|
471
|
+
**Parameters:**
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
interface ImpactInput {
|
|
475
|
+
repoRoot: string;
|
|
476
|
+
base?: string; // git base ref (기본: "HEAD~1")
|
|
477
|
+
head?: string; // git head ref (기본: "HEAD")
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Returns:**
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
{
|
|
485
|
+
ok: true;
|
|
486
|
+
changedFiles: string[]; // 변경된 파일 목록
|
|
487
|
+
selectedRoutes: string[]; // 영향받는 라우트 ID
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Example:**
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const impact = ateImpact({
|
|
495
|
+
repoRoot: process.cwd(),
|
|
496
|
+
base: "main",
|
|
497
|
+
head: "feature-branch",
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
console.log(`Changed files: ${impact.changedFiles.length}`);
|
|
501
|
+
console.log(`Affected routes: ${impact.selectedRoutes.join(", ")}`);
|
|
502
|
+
|
|
503
|
+
// 영향받는 라우트만 테스트
|
|
504
|
+
ateGenerate({
|
|
505
|
+
repoRoot: process.cwd(),
|
|
506
|
+
onlyRoutes: impact.selectedRoutes,
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 💡 Examples
|
|
513
|
+
|
|
514
|
+
### Example 1: Basic Pipeline
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import { ateExtract, ateGenerate, ateRun, ateReport } from "@mandujs/ate";
|
|
518
|
+
|
|
519
|
+
async function runFullPipeline() {
|
|
520
|
+
// 1. Extract
|
|
521
|
+
await ateExtract({
|
|
522
|
+
repoRoot: process.cwd(),
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// 2. Generate
|
|
526
|
+
ateGenerate({
|
|
527
|
+
repoRoot: process.cwd(),
|
|
528
|
+
oracleLevel: "L1",
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// 3. Run
|
|
532
|
+
const result = await ateRun({
|
|
533
|
+
repoRoot: process.cwd(),
|
|
534
|
+
baseURL: "http://localhost:3000",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// 4. Report
|
|
538
|
+
await ateReport({
|
|
539
|
+
repoRoot: process.cwd(),
|
|
540
|
+
runId: result.runId,
|
|
541
|
+
startedAt: result.startedAt,
|
|
542
|
+
finishedAt: result.finishedAt,
|
|
543
|
+
exitCode: result.exitCode,
|
|
544
|
+
oracleLevel: "L1",
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
runFullPipeline();
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
### Example 2: CI/CD Integration
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// ci-test.ts
|
|
557
|
+
import { ateExtract, ateGenerate, ateRun, ateReport, ateHeal } from "@mandujs/ate";
|
|
558
|
+
|
|
559
|
+
async function ciPipeline() {
|
|
560
|
+
const repoRoot = process.cwd();
|
|
561
|
+
const oracleLevel = "L1";
|
|
562
|
+
|
|
563
|
+
// Extract & Generate
|
|
564
|
+
await ateExtract({ repoRoot });
|
|
565
|
+
ateGenerate({ repoRoot, oracleLevel });
|
|
566
|
+
|
|
567
|
+
// Run in CI mode
|
|
568
|
+
const result = await ateRun({
|
|
569
|
+
repoRoot,
|
|
570
|
+
baseURL: process.env.BASE_URL ?? "http://localhost:3333",
|
|
571
|
+
ci: true,
|
|
572
|
+
headless: true,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Report
|
|
576
|
+
const report = await ateReport({
|
|
577
|
+
repoRoot,
|
|
578
|
+
runId: result.runId,
|
|
579
|
+
startedAt: result.startedAt,
|
|
580
|
+
finishedAt: result.finishedAt,
|
|
581
|
+
exitCode: result.exitCode,
|
|
582
|
+
oracleLevel,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Heal if failed
|
|
586
|
+
if (result.exitCode !== 0) {
|
|
587
|
+
const healing = ateHeal({ repoRoot, runId: result.runId });
|
|
588
|
+
console.log("Healing suggestions:", healing.suggestions);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
process.exit(result.exitCode);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
ciPipeline();
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**GitHub Actions:**
|
|
598
|
+
|
|
599
|
+
```yaml
|
|
600
|
+
name: ATE Tests
|
|
601
|
+
|
|
602
|
+
on: [push, pull_request]
|
|
603
|
+
|
|
604
|
+
jobs:
|
|
605
|
+
test:
|
|
606
|
+
runs-on: ubuntu-latest
|
|
607
|
+
steps:
|
|
608
|
+
- uses: actions/checkout@v3
|
|
609
|
+
- uses: oven-sh/setup-bun@v1
|
|
610
|
+
|
|
611
|
+
- name: Install dependencies
|
|
612
|
+
run: bun install
|
|
613
|
+
|
|
614
|
+
- name: Start server
|
|
615
|
+
run: bun run dev &
|
|
616
|
+
env:
|
|
617
|
+
PORT: 3333
|
|
618
|
+
|
|
619
|
+
- name: Run ATE tests
|
|
620
|
+
run: bun run ci-test.ts
|
|
621
|
+
env:
|
|
622
|
+
BASE_URL: http://localhost:3333
|
|
623
|
+
CI: true
|
|
624
|
+
|
|
625
|
+
- name: Upload Playwright Report
|
|
626
|
+
if: always()
|
|
627
|
+
uses: actions/upload-artifact@v3
|
|
628
|
+
with:
|
|
629
|
+
name: playwright-report
|
|
630
|
+
path: .mandu/reports/latest/
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
### Example 3: Custom Oracle
|
|
636
|
+
|
|
637
|
+
기본 Oracle L0-L1 외에 프로젝트별 assertion을 추가할 수 있습니다.
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
// custom-oracle.ts
|
|
641
|
+
import { generatePlaywrightSpecs } from "@mandujs/ate/codegen";
|
|
642
|
+
import { getAtePaths, readJson, writeFile } from "@mandujs/ate/fs";
|
|
643
|
+
import type { ScenarioBundle } from "@mandujs/ate/scenario";
|
|
644
|
+
|
|
645
|
+
function customOracleAssertions(route: string): string {
|
|
646
|
+
// 프로젝트별 도메인 로직
|
|
647
|
+
if (route === "/dashboard") {
|
|
648
|
+
return `
|
|
649
|
+
// Custom: Dashboard must have user info
|
|
650
|
+
await expect(page.locator('[data-testid="user-name"]')).toBeVisible();
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (route.startsWith("/admin")) {
|
|
655
|
+
return `
|
|
656
|
+
// Custom: Admin pages must have sidebar
|
|
657
|
+
await expect(page.locator('aside.sidebar')).toBeVisible();
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return "";
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function generateWithCustomOracle(repoRoot: string) {
|
|
665
|
+
const paths = getAtePaths(repoRoot);
|
|
666
|
+
const bundle = readJson<ScenarioBundle>(paths.scenariosPath);
|
|
667
|
+
|
|
668
|
+
for (const scenario of bundle.scenarios) {
|
|
669
|
+
const customAssertions = customOracleAssertions(scenario.route);
|
|
670
|
+
|
|
671
|
+
const code = `
|
|
672
|
+
import { test, expect } from "@playwright/test";
|
|
673
|
+
|
|
674
|
+
test.describe("${scenario.id}", () => {
|
|
675
|
+
test("smoke ${scenario.route}", async ({ page, baseURL }) => {
|
|
676
|
+
const url = (baseURL ?? "http://localhost:3333") + "${scenario.route}";
|
|
677
|
+
|
|
678
|
+
const errors: string[] = [];
|
|
679
|
+
page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); });
|
|
680
|
+
page.on("pageerror", (err) => errors.push(String(err)));
|
|
681
|
+
|
|
682
|
+
await page.goto(url);
|
|
683
|
+
|
|
684
|
+
// L1 baseline
|
|
685
|
+
await expect(page.locator("main")).toHaveCount(1);
|
|
686
|
+
|
|
687
|
+
${customAssertions}
|
|
688
|
+
|
|
689
|
+
expect(errors, "console/page errors").toEqual([]);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
`;
|
|
693
|
+
|
|
694
|
+
writeFile(`${paths.autoE2eDir}/${scenario.id.replace(/:/g, "_")}.spec.ts`, code);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
generateWithCustomOracle(process.cwd());
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
### Example 4: Impact-Based Subset Testing
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// subset-test.ts
|
|
707
|
+
import { ateImpact, ateGenerate, ateRun } from "@mandujs/ate";
|
|
708
|
+
|
|
709
|
+
async function subsetTest() {
|
|
710
|
+
const repoRoot = process.cwd();
|
|
711
|
+
|
|
712
|
+
// 1. Compute impact
|
|
713
|
+
const impact = ateImpact({
|
|
714
|
+
repoRoot,
|
|
715
|
+
base: "main",
|
|
716
|
+
head: "HEAD",
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
console.log(`Changed files: ${impact.changedFiles.length}`);
|
|
720
|
+
console.log(`Affected routes: ${impact.selectedRoutes.join(", ")}`);
|
|
721
|
+
|
|
722
|
+
if (impact.selectedRoutes.length === 0) {
|
|
723
|
+
console.log("No routes affected, skipping tests");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 2. Generate tests for affected routes only
|
|
728
|
+
ateGenerate({
|
|
729
|
+
repoRoot,
|
|
730
|
+
oracleLevel: "L1",
|
|
731
|
+
onlyRoutes: impact.selectedRoutes,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// 3. Run subset tests
|
|
735
|
+
const result = await ateRun({ repoRoot });
|
|
736
|
+
|
|
737
|
+
if (result.exitCode !== 0) {
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
subsetTest();
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
### Example 5: Programmatic Test Healing
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
// auto-heal.ts
|
|
751
|
+
import { ateHeal } from "@mandujs/ate";
|
|
752
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
753
|
+
|
|
754
|
+
function applyHealingSuggestions(repoRoot: string, runId: string) {
|
|
755
|
+
const healing = ateHeal({ repoRoot, runId });
|
|
756
|
+
|
|
757
|
+
for (const suggestion of healing.suggestions) {
|
|
758
|
+
if (suggestion.kind === "selector-map") {
|
|
759
|
+
console.log(`Applying: ${suggestion.title}`);
|
|
760
|
+
|
|
761
|
+
// Parse diff and apply (simplified example)
|
|
762
|
+
const selectorMapPath = `${repoRoot}/.mandu/selector-map.json`;
|
|
763
|
+
const current = JSON.parse(readFileSync(selectorMapPath, "utf8"));
|
|
764
|
+
|
|
765
|
+
if (suggestion.metadata?.selector && suggestion.metadata?.alternatives) {
|
|
766
|
+
current[suggestion.metadata.selector] = {
|
|
767
|
+
fallbacks: suggestion.metadata.alternatives,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
writeFileSync(selectorMapPath, JSON.stringify(current, null, 2));
|
|
772
|
+
console.log(`✅ Updated ${selectorMapPath}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
applyHealingSuggestions(process.cwd(), "run-1234567890");
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## 🎯 Oracle Levels
|
|
783
|
+
|
|
784
|
+
ATE는 4단계 Oracle Level을 지원합니다. 레벨이 높을수록 더 많은 assertion을 수행합니다.
|
|
785
|
+
|
|
786
|
+
| Level | Description | Assertions |
|
|
787
|
+
|-------|-------------|------------|
|
|
788
|
+
| **L0** | Baseline | ✅ No `console.error`<br>✅ No uncaught exceptions<br>✅ No 5xx responses |
|
|
789
|
+
| **L1** | Structure | L0 + ✅ `<main>` element exists |
|
|
790
|
+
| **L2** | Behavior | L1 + ✅ URL matches expected pattern<br>✅ (Placeholder for custom assertions) |
|
|
791
|
+
| **L3** | Domain | L2 + ✅ (Placeholder for domain-specific assertions) |
|
|
792
|
+
|
|
793
|
+
### Choosing Oracle Level
|
|
794
|
+
|
|
795
|
+
- **L0**: 빠른 스모크 테스트, CI에서 모든 PR 실행
|
|
796
|
+
- **L1**: 기본 구조 검증, 대부분의 프로젝트 권장
|
|
797
|
+
- **L2**: 행동 검증, 중요 페이지에 사용
|
|
798
|
+
- **L3**: 도메인 로직 검증, 수동 assertion 추가 필요
|
|
799
|
+
|
|
800
|
+
### Future Enhancements (L2-L3)
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
// L2 예정: Accessibility, Performance
|
|
804
|
+
await expect(page).toPassAxe(); // Accessibility violations
|
|
805
|
+
expect(await page.metrics().FCP).toBeLessThan(2000); // First Contentful Paint
|
|
806
|
+
|
|
807
|
+
// L3 예정: Visual Regression
|
|
808
|
+
await expect(page).toHaveScreenshot("homepage.png", { maxDiffPixels: 100 });
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
## 🗺️ Roadmap
|
|
814
|
+
|
|
815
|
+
### Current (v0.1.0)
|
|
816
|
+
|
|
817
|
+
- ✅ L0-L1 Oracle
|
|
818
|
+
- ✅ Route smoke tests
|
|
819
|
+
- ✅ Interaction graph extraction
|
|
820
|
+
- ✅ Playwright spec generation
|
|
821
|
+
- ✅ Basic healing (selector suggestions)
|
|
822
|
+
- ✅ Impact analysis (git diff)
|
|
823
|
+
|
|
824
|
+
### Near Future (v0.2.0)
|
|
825
|
+
|
|
826
|
+
- 🔄 L2 Oracle: Accessibility (axe-core), Performance (Web Vitals)
|
|
827
|
+
- 🔄 Selector stability scoring
|
|
828
|
+
- 🔄 Parallel test execution optimization
|
|
829
|
+
- 🔄 Enhanced healing with DOM snapshot analysis
|
|
830
|
+
|
|
831
|
+
### Long Term (v0.3.0+)
|
|
832
|
+
|
|
833
|
+
- 🔮 L3 Oracle: Visual regression (Playwright screenshots)
|
|
834
|
+
- 🔮 Modal/Action interaction tests
|
|
835
|
+
- 🔮 Cross-browser compatibility matrix
|
|
836
|
+
- 🔮 AI-powered test generation (LLM integration)
|
|
837
|
+
- 🔮 Real user monitoring (RUM) integration
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## 🛠️ Troubleshooting
|
|
842
|
+
|
|
843
|
+
### Common Errors
|
|
844
|
+
|
|
845
|
+
#### 1. `No interaction graph found`
|
|
846
|
+
|
|
847
|
+
**Problem:** `ateGenerate` 또는 `ateRun`을 실행했지만 `interaction-graph.json`이 없습니다.
|
|
848
|
+
|
|
849
|
+
**Solution:**
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
// 먼저 extract 실행
|
|
853
|
+
await ateExtract({ repoRoot: process.cwd() });
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
#### 2. `Playwright not found`
|
|
859
|
+
|
|
860
|
+
**Problem:** `bunx playwright test` 실행 시 Playwright가 설치되지 않았습니다.
|
|
861
|
+
|
|
862
|
+
**Solution:**
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
bun add -d @playwright/test playwright
|
|
866
|
+
bunx playwright install chromium
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
#### 3. `Base URL not responding`
|
|
872
|
+
|
|
873
|
+
**Problem:** 테스트 실행 시 서버가 실행되지 않아 `ERR_CONNECTION_REFUSED`가 발생합니다.
|
|
874
|
+
|
|
875
|
+
**Solution:**
|
|
876
|
+
|
|
877
|
+
```bash
|
|
878
|
+
# 서버를 먼저 실행
|
|
879
|
+
bun run dev &
|
|
880
|
+
|
|
881
|
+
# 서버가 준비될 때까지 대기
|
|
882
|
+
sleep 5
|
|
883
|
+
|
|
884
|
+
# 테스트 실행
|
|
885
|
+
bun run ate:test
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
또는 `wait-on` 사용:
|
|
889
|
+
|
|
890
|
+
```json
|
|
891
|
+
{
|
|
892
|
+
"scripts": {
|
|
893
|
+
"ate:test": "wait-on http://localhost:3333 && bun run ci-test.ts"
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
#### 4. `Tests fail with selector timeout`
|
|
901
|
+
|
|
902
|
+
**Problem:** 생성된 셀렉터가 변경되어 테스트가 실패합니다.
|
|
903
|
+
|
|
904
|
+
**Solution:**
|
|
905
|
+
|
|
906
|
+
```typescript
|
|
907
|
+
// 1. Healing 실행
|
|
908
|
+
const healing = ateHeal({ repoRoot: process.cwd(), runId: "run-XXX" });
|
|
909
|
+
|
|
910
|
+
// 2. 제안 확인
|
|
911
|
+
healing.suggestions.forEach((s) => {
|
|
912
|
+
console.log(s.diff);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// 3. 수동으로 selector-map.json 업데이트
|
|
916
|
+
// 또는 자동 적용 스크립트 사용 (Example 5 참고)
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
#### 5. `Empty interaction graph`
|
|
922
|
+
|
|
923
|
+
**Problem:** `routeGlobs`가 파일을 찾지 못했습니다.
|
|
924
|
+
|
|
925
|
+
**Solution:**
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
// glob 패턴 확인
|
|
929
|
+
await ateExtract({
|
|
930
|
+
repoRoot: process.cwd(),
|
|
931
|
+
routeGlobs: [
|
|
932
|
+
"app/**/page.tsx", // Next.js App Router
|
|
933
|
+
"routes/**/page.tsx", // Custom routes
|
|
934
|
+
"src/pages/**/*.tsx", // Pages directory
|
|
935
|
+
],
|
|
936
|
+
});
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
### Performance Tips
|
|
942
|
+
|
|
943
|
+
#### 1. Subset Testing for Large Projects
|
|
944
|
+
|
|
945
|
+
```typescript
|
|
946
|
+
// 전체 테스트 대신 변경된 라우트만 실행
|
|
947
|
+
const impact = ateImpact({ repoRoot: process.cwd() });
|
|
948
|
+
ateGenerate({ repoRoot: process.cwd(), onlyRoutes: impact.selectedRoutes });
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
#### 2. Parallel Execution
|
|
952
|
+
|
|
953
|
+
```typescript
|
|
954
|
+
// playwright.config.ts
|
|
955
|
+
export default defineConfig({
|
|
956
|
+
workers: process.env.CI ? 2 : 4, // CI에서는 2개, 로컬에서는 4개 worker
|
|
957
|
+
});
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
#### 3. Headless Mode
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
await ateRun({
|
|
964
|
+
repoRoot: process.cwd(),
|
|
965
|
+
headless: true, // 브라우저 UI 없이 실행 (빠름)
|
|
966
|
+
});
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
#### 4. Cache Interaction Graph
|
|
970
|
+
|
|
971
|
+
```bash
|
|
972
|
+
# Extract는 한 번만 실행, generate/run은 반복 가능
|
|
973
|
+
bun run ate:extract # 한 번만
|
|
974
|
+
bun run ate:generate # 여러 번
|
|
975
|
+
bun run ate:run # 여러 번
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
### Debugging Tips
|
|
981
|
+
|
|
982
|
+
#### 1. Enable Playwright Debug Mode
|
|
983
|
+
|
|
984
|
+
```bash
|
|
985
|
+
PWDEBUG=1 bunx playwright test
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
#### 2. View Playwright HTML Report
|
|
989
|
+
|
|
990
|
+
```bash
|
|
991
|
+
bunx playwright show-report .mandu/reports/latest/playwright-html
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
#### 3. Inspect Interaction Graph
|
|
995
|
+
|
|
996
|
+
```bash
|
|
997
|
+
cat .mandu/interaction-graph.json | jq .
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
#### 4. Verbose Logging
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
import { ateExtract } from "@mandujs/ate";
|
|
1004
|
+
|
|
1005
|
+
const result = await ateExtract({ repoRoot: process.cwd() });
|
|
1006
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
---
|
|
1012
|
+
|
|
1013
|
+
## 📊 HTML Reports
|
|
1014
|
+
|
|
1015
|
+
ATE는 테스트 결과를 시각화하는 HTML 대시보드를 자동으로 생성합니다.
|
|
1016
|
+
|
|
1017
|
+
### 사용법
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import { generateHtmlReport, generateReport } from "@mandujs/ate";
|
|
1021
|
+
|
|
1022
|
+
// 단독 HTML 생성
|
|
1023
|
+
const result = await generateHtmlReport({
|
|
1024
|
+
repoRoot: process.cwd(),
|
|
1025
|
+
runId: "run-2026-02-15-04-30-00",
|
|
1026
|
+
includeScreenshots: true,
|
|
1027
|
+
includeTraces: true,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
console.log(`HTML report: ${result.path}`);
|
|
1031
|
+
|
|
1032
|
+
// JSON + HTML 동시 생성
|
|
1033
|
+
const reports = await generateReport({
|
|
1034
|
+
repoRoot: process.cwd(),
|
|
1035
|
+
runId: "run-2026-02-15-04-30-00",
|
|
1036
|
+
format: "both", // 'json' | 'html' | 'both'
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
console.log(`JSON: ${reports.json}`);
|
|
1040
|
+
console.log(`HTML: ${reports.html}`);
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### MCP 도구
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
// MCP를 통한 리포트 생성
|
|
1047
|
+
await mcp.callTool("mandu.ate.report", {
|
|
1048
|
+
repoRoot: process.cwd(),
|
|
1049
|
+
runId: "run-xxx",
|
|
1050
|
+
startedAt: "2026-02-15T04:00:00.000Z",
|
|
1051
|
+
finishedAt: "2026-02-15T04:00:10.000Z",
|
|
1052
|
+
exitCode: 0,
|
|
1053
|
+
format: "both", // HTML + JSON 생성
|
|
1054
|
+
});
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### 리포트 구성
|
|
1058
|
+
|
|
1059
|
+
HTML 리포트는 다음을 포함합니다:
|
|
1060
|
+
|
|
1061
|
+
- **테스트 결과 요약**: Pass/Fail/Skip 카드
|
|
1062
|
+
- **Oracle 검증**: L0~L3 레벨별 상세 결과
|
|
1063
|
+
- **Impact Analysis**: 변경된 파일 및 영향받은 라우트
|
|
1064
|
+
- **Heal 제안**: 자동 복구 제안 및 diff
|
|
1065
|
+
- **스크린샷 갤러리**: 테스트 스크린샷 (선택)
|
|
1066
|
+
- **Playwright 링크**: 상세 리포트 및 trace 연결
|
|
1067
|
+
|
|
1068
|
+
### 예제
|
|
1069
|
+
|
|
1070
|
+
예제 리포트를 생성하려면:
|
|
1071
|
+
|
|
1072
|
+
```bash
|
|
1073
|
+
bun run packages/ate/examples/generate-sample-report.ts
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
생성된 `packages/ate/examples/sample-report.html`을 브라우저에서 열어보세요.
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
## 📄 License
|
|
1081
|
+
|
|
1082
|
+
[MPL-2.0](https://opensource.org/licenses/MPL-2.0)
|
|
1083
|
+
|
|
1084
|
+
수정한 ATE 소스 코드는 공개 필수, ATE를 import하여 만든 테스트는 자유롭게 사용 가능합니다.
|
|
1085
|
+
|
|
1086
|
+
---
|
|
1087
|
+
|
|
1088
|
+
## 🤝 Contributing
|
|
1089
|
+
|
|
1090
|
+
Issues and PRs welcome at [github.com/mandujs/mandu](https://github.com/mandujs/mandu)
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
## 📚 Related Documentation
|
|
1095
|
+
|
|
1096
|
+
- [Mandu Framework Guide](../../README.md)
|
|
1097
|
+
- [Playwright Documentation](https://playwright.dev)
|
|
1098
|
+
- [Interaction Graph Schema](./docs/interaction-graph.md) (TBD)
|
|
1099
|
+
- [Oracle Levels Spec](./docs/oracle-levels.md) (TBD)
|
|
1100
|
+
|
|
1101
|
+
---
|
|
1102
|
+
|
|
1103
|
+
**Built with ❤️ by the Mandu team**
|