@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 ADDED
@@ -0,0 +1,1103 @@
1
+ # @mandujs/ate - Automation Test Engine
2
+
3
+ **자동화 테스트 엔진**: Extract → Generate → Run → Report → Heal → Impact 전체 파이프라인을 하나의 패키지로 제공합니다.
4
+
5
+ [![License: MPL-2.0](https://img.shields.io/badge/License-MPL_2.0-blue.svg)](https://opensource.org/licenses/MPL-2.0)
6
+ [![Bun](https://img.shields.io/badge/Bun-≥1.0.0-orange)](https://bun.sh)
7
+ [![Playwright](https://img.shields.io/badge/Playwright-≥1.40.0-green)](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**