@tyndall/test-playwright 0.0.1
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 +30 -0
- package/dist/app/AppInstance.d.ts +24 -0
- package/dist/app/AppInstance.d.ts.map +1 -0
- package/dist/app/AppInstance.js +30 -0
- package/dist/app/AppLauncher.d.ts +32 -0
- package/dist/app/AppLauncher.d.ts.map +1 -0
- package/dist/app/AppLauncher.js +147 -0
- package/dist/dynamic/ManifestObserver.d.ts +24 -0
- package/dist/dynamic/ManifestObserver.d.ts.map +1 -0
- package/dist/dynamic/ManifestObserver.js +71 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/extensibility/EnvironmentScriptProvider.d.ts +21 -0
- package/dist/extensibility/EnvironmentScriptProvider.d.ts.map +1 -0
- package/dist/extensibility/EnvironmentScriptProvider.js +20 -0
- package/dist/extensibility/ExpectRegistry.d.ts +11 -0
- package/dist/extensibility/ExpectRegistry.d.ts.map +1 -0
- package/dist/extensibility/ExpectRegistry.js +18 -0
- package/dist/extensibility/StepRegistry.d.ts +14 -0
- package/dist/extensibility/StepRegistry.d.ts.map +1 -0
- package/dist/extensibility/StepRegistry.js +29 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/reporter/ConsoleReporter.d.ts +6 -0
- package/dist/reporter/ConsoleReporter.d.ts.map +1 -0
- package/dist/reporter/ConsoleReporter.js +13 -0
- package/dist/reporter/HtmlReporter.d.ts +11 -0
- package/dist/reporter/HtmlReporter.d.ts.map +1 -0
- package/dist/reporter/HtmlReporter.js +36 -0
- package/dist/reporter/JsonReporter.d.ts +11 -0
- package/dist/reporter/JsonReporter.d.ts.map +1 -0
- package/dist/reporter/JsonReporter.js +12 -0
- package/dist/reporter/Reporter.d.ts +5 -0
- package/dist/reporter/Reporter.d.ts.map +1 -0
- package/dist/reporter/Reporter.js +1 -0
- package/dist/reporter/ResultCollector.d.ts +7 -0
- package/dist/reporter/ResultCollector.d.ts.map +1 -0
- package/dist/reporter/ResultCollector.js +26 -0
- package/dist/runner/PlaywrightRunner.d.ts +38 -0
- package/dist/runner/PlaywrightRunner.d.ts.map +1 -0
- package/dist/runner/PlaywrightRunner.js +77 -0
- package/dist/runner/StepExecutor.d.ts +16 -0
- package/dist/runner/StepExecutor.d.ts.map +1 -0
- package/dist/runner/StepExecutor.js +53 -0
- package/dist/scenario/ScenarioLoader.d.ts +4 -0
- package/dist/scenario/ScenarioLoader.d.ts.map +1 -0
- package/dist/scenario/ScenarioLoader.js +23 -0
- package/dist/scenario/ScenarioValidator.d.ts +15 -0
- package/dist/scenario/ScenarioValidator.d.ts.map +1 -0
- package/dist/scenario/ScenarioValidator.js +207 -0
- package/dist/scenario/types.d.ts +2 -0
- package/dist/scenario/types.d.ts.map +1 -0
- package/dist/scenario/types.js +1 -0
- package/dist/steps/click.d.ts +6 -0
- package/dist/steps/click.d.ts.map +1 -0
- package/dist/steps/click.js +3 -0
- package/dist/steps/expect.d.ts +7 -0
- package/dist/steps/expect.d.ts.map +1 -0
- package/dist/steps/expect.js +71 -0
- package/dist/steps/goto.d.ts +6 -0
- package/dist/steps/goto.d.ts.map +1 -0
- package/dist/steps/goto.js +6 -0
- package/dist/steps/index.d.ts +16 -0
- package/dist/steps/index.d.ts.map +1 -0
- package/dist/steps/index.js +8 -0
- package/dist/steps/types.d.ts +17 -0
- package/dist/steps/types.d.ts.map +1 -0
- package/dist/steps/types.js +1 -0
- package/dist/types/result.d.ts +25 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +1 -0
- package/dist/types/scenario.d.ts +44 -0
- package/dist/types/scenario.d.ts.map +1 -0
- package/dist/types/scenario.js +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
export class HtmlReporter {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.outputPath = options.outputPath ?? "e2e-report.html";
|
|
6
|
+
}
|
|
7
|
+
async report(result) {
|
|
8
|
+
const resolvedPath = resolve(this.outputPath);
|
|
9
|
+
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
10
|
+
const rows = result.scenarios
|
|
11
|
+
.map((scenario) => `<tr><td>${scenario.scenario.id}</td><td>${scenario.status}</td></tr>`)
|
|
12
|
+
.join("");
|
|
13
|
+
const html = `<!doctype html>
|
|
14
|
+
<html>
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<title>E2E Report</title>
|
|
18
|
+
<style>
|
|
19
|
+
body { font-family: Arial, sans-serif; padding: 16px; }
|
|
20
|
+
table { border-collapse: collapse; width: 100%; }
|
|
21
|
+
th, td { border: 1px solid #ddd; padding: 8px; }
|
|
22
|
+
th { background: #f4f4f4; text-align: left; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<h1>${result.name}</h1>
|
|
27
|
+
<p>Status: ${result.status}</p>
|
|
28
|
+
<table>
|
|
29
|
+
<thead><tr><th>Scenario</th><th>Status</th></tr></thead>
|
|
30
|
+
<tbody>${rows}</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
</body>
|
|
33
|
+
</html>`;
|
|
34
|
+
await writeFile(resolvedPath, html, "utf-8");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AggregatedResult } from "../types/result.js";
|
|
2
|
+
import type { Reporter } from "./Reporter.js";
|
|
3
|
+
export interface JsonReporterOptions {
|
|
4
|
+
outputPath?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class JsonReporter implements Reporter {
|
|
7
|
+
private readonly outputPath;
|
|
8
|
+
constructor(options?: JsonReporterOptions);
|
|
9
|
+
report(result: AggregatedResult): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=JsonReporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JsonReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/JsonReporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,GAAE,mBAAwB;IAIvC,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAKtD"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
export class JsonReporter {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.outputPath = options.outputPath ?? "e2e-report.json";
|
|
6
|
+
}
|
|
7
|
+
async report(result) {
|
|
8
|
+
const resolvedPath = resolve(this.outputPath);
|
|
9
|
+
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
10
|
+
await writeFile(resolvedPath, JSON.stringify(result, null, 2), "utf-8");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/Reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AggregatedResult, ScenarioResult } from "../types/result.js";
|
|
2
|
+
export declare class ResultCollector {
|
|
3
|
+
private readonly results;
|
|
4
|
+
add(result: ScenarioResult): void;
|
|
5
|
+
finalize(name: string): AggregatedResult;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=ResultCollector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResultCollector.d.ts","sourceRoot":"","sources":["../../src/reporter/ResultCollector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAc,MAAM,oBAAoB,CAAC;AAEvF,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAEhD,GAAG,CAAC,MAAM,EAAE,cAAc;IAI1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB;CAoBzC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class ResultCollector {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.results = [];
|
|
4
|
+
}
|
|
5
|
+
add(result) {
|
|
6
|
+
this.results.push(result);
|
|
7
|
+
}
|
|
8
|
+
finalize(name) {
|
|
9
|
+
const status = this.results.some((result) => result.status === "failed")
|
|
10
|
+
? "failed"
|
|
11
|
+
: "passed";
|
|
12
|
+
const startedAt = this.results.length
|
|
13
|
+
? Math.min(...this.results.map((result) => result.startedAt))
|
|
14
|
+
: Date.now();
|
|
15
|
+
const finishedAt = this.results.length
|
|
16
|
+
? Math.max(...this.results.map((result) => result.finishedAt))
|
|
17
|
+
: Date.now();
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
scenarios: [...this.results],
|
|
21
|
+
status,
|
|
22
|
+
startedAt,
|
|
23
|
+
finishedAt,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ScenarioCase } from "../types/scenario.js";
|
|
2
|
+
import type { ScenarioResult } from "../types/result.js";
|
|
3
|
+
import type { PageLike } from "../steps/types.js";
|
|
4
|
+
import type { EnvironmentScriptProvider } from "../extensibility/EnvironmentScriptProvider.js";
|
|
5
|
+
import type { StepRegistry } from "../extensibility/StepRegistry.js";
|
|
6
|
+
import type { ExpectRegistry } from "../extensibility/ExpectRegistry.js";
|
|
7
|
+
export interface PlaywrightLaunchOptions {
|
|
8
|
+
headless: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface BrowserLauncher {
|
|
11
|
+
launch(options: PlaywrightLaunchOptions): Promise<BrowserLike>;
|
|
12
|
+
}
|
|
13
|
+
export interface BrowserLike {
|
|
14
|
+
newPage(): Promise<PageLike>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface PlaywrightRunnerOptions {
|
|
18
|
+
headless?: boolean;
|
|
19
|
+
launcher?: BrowserLauncher;
|
|
20
|
+
environmentScriptPath?: string;
|
|
21
|
+
environmentScriptProvider?: EnvironmentScriptProvider;
|
|
22
|
+
stepRegistry?: StepRegistry;
|
|
23
|
+
expectRegistry?: ExpectRegistry;
|
|
24
|
+
abortOnFailure?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare class PlaywrightRunner {
|
|
27
|
+
private readonly stepExecutor;
|
|
28
|
+
private readonly launcher;
|
|
29
|
+
private readonly headless;
|
|
30
|
+
private readonly environmentScriptPath?;
|
|
31
|
+
private readonly environmentScriptProvider?;
|
|
32
|
+
private readonly expectRegistry?;
|
|
33
|
+
constructor(options?: PlaywrightRunnerOptions);
|
|
34
|
+
runScenario(scenario: ScenarioCase, baseUrl: string): Promise<ScenarioResult>;
|
|
35
|
+
private resolveEnvironmentScript;
|
|
36
|
+
private injectEnvironmentScript;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=PlaywrightRunner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlaywrightRunner.d.ts","sourceRoot":"","sources":["../../src/runner/PlaywrightRunner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,KAAK,EAAe,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE/D,OAAO,KAAK,EAEV,yBAAyB,EAC1B,MAAM,+CAA+C,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAUD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yBAAyB,CAAC,EAAE,yBAAyB,CAAC;IACtD,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAA4B;IACvE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAiB;gBAErC,OAAO,GAAE,uBAA4B;IAY3C,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;YAmCrE,wBAAwB;YAUxB,uBAAuB;CAWtC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { StepExecutor } from "./StepExecutor.js";
|
|
2
|
+
import { ManifestObserver } from "../dynamic/ManifestObserver.js";
|
|
3
|
+
import { BrowserError } from "../errors.js";
|
|
4
|
+
const defaultLauncher = {
|
|
5
|
+
async launch(options) {
|
|
6
|
+
const { chromium } = await import("playwright");
|
|
7
|
+
const browser = await chromium.launch({ headless: options.headless });
|
|
8
|
+
return browser;
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export class PlaywrightRunner {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.stepExecutor = new StepExecutor({
|
|
14
|
+
registry: options.stepRegistry,
|
|
15
|
+
abortOnFailure: options.abortOnFailure,
|
|
16
|
+
});
|
|
17
|
+
this.launcher = options.launcher ?? defaultLauncher;
|
|
18
|
+
this.headless = options.headless ?? true;
|
|
19
|
+
this.environmentScriptPath = options.environmentScriptPath;
|
|
20
|
+
this.environmentScriptProvider = options.environmentScriptProvider;
|
|
21
|
+
this.expectRegistry = options.expectRegistry;
|
|
22
|
+
}
|
|
23
|
+
async runScenario(scenario, baseUrl) {
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
let browser;
|
|
26
|
+
try {
|
|
27
|
+
browser = await this.launcher.launch({ headless: this.headless });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new BrowserError("Failed to launch browser", { cause: error });
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const page = await browser.newPage();
|
|
34
|
+
await this.injectEnvironmentScript(page);
|
|
35
|
+
const manifestObserver = new ManifestObserver();
|
|
36
|
+
manifestObserver.attach(page);
|
|
37
|
+
const context = {
|
|
38
|
+
page,
|
|
39
|
+
baseUrl,
|
|
40
|
+
manifestObserver,
|
|
41
|
+
expectRegistry: this.expectRegistry,
|
|
42
|
+
};
|
|
43
|
+
const steps = await this.stepExecutor.executeAll(scenario.steps, context);
|
|
44
|
+
const status = steps.some((step) => step.status === "failed") ? "failed" : "passed";
|
|
45
|
+
return {
|
|
46
|
+
scenario,
|
|
47
|
+
steps,
|
|
48
|
+
status,
|
|
49
|
+
startedAt,
|
|
50
|
+
finishedAt: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await browser.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async resolveEnvironmentScript() {
|
|
58
|
+
if (this.environmentScriptProvider) {
|
|
59
|
+
return this.environmentScriptProvider.getEnvironmentScript();
|
|
60
|
+
}
|
|
61
|
+
if (this.environmentScriptPath) {
|
|
62
|
+
return { path: this.environmentScriptPath };
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
async injectEnvironmentScript(page) {
|
|
67
|
+
const script = await this.resolveEnvironmentScript();
|
|
68
|
+
if (!script) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!page.addInitScript) {
|
|
72
|
+
throw new Error("Environment script injection requires addInitScript support on the page.");
|
|
73
|
+
}
|
|
74
|
+
// Inject before any navigation to simulate native webview bridges.
|
|
75
|
+
await page.addInitScript({ path: script.path });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Step } from "../types/scenario.js";
|
|
2
|
+
import type { StepResult } from "../types/result.js";
|
|
3
|
+
import type { StepContext } from "../steps/index.js";
|
|
4
|
+
import { type StepRegistry } from "../extensibility/StepRegistry.js";
|
|
5
|
+
export interface StepExecutorOptions {
|
|
6
|
+
registry?: StepRegistry;
|
|
7
|
+
abortOnFailure?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class StepExecutor {
|
|
10
|
+
private readonly registry;
|
|
11
|
+
private readonly abortOnFailure;
|
|
12
|
+
constructor(options?: StepExecutorOptions);
|
|
13
|
+
execute(step: Step, context: StepContext): Promise<StepResult>;
|
|
14
|
+
executeAll(steps: Step[], context: StepContext): Promise<StepResult[]>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=StepExecutor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StepExecutor.d.ts","sourceRoot":"","sources":["../../src/runner/StepExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,kCAAkC,CAAC;AAG1C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;gBAE7B,OAAO,GAAE,mBAAwB;IAKvC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAyB9D,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAqB7E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defaultStepRegistry, } from "../extensibility/StepRegistry.js";
|
|
2
|
+
import { SchemaError } from "../errors.js";
|
|
3
|
+
export class StepExecutor {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.registry = options.registry ?? defaultStepRegistry;
|
|
6
|
+
this.abortOnFailure = options.abortOnFailure ?? false;
|
|
7
|
+
}
|
|
8
|
+
async execute(step, context) {
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
try {
|
|
11
|
+
const stepType = Object.keys(step)[0];
|
|
12
|
+
const handler = this.registry.get(stepType);
|
|
13
|
+
if (!handler) {
|
|
14
|
+
throw new SchemaError(`Unknown step type: ${String(stepType)}`);
|
|
15
|
+
}
|
|
16
|
+
await handler(step, context);
|
|
17
|
+
return {
|
|
18
|
+
status: "passed",
|
|
19
|
+
startedAt,
|
|
20
|
+
finishedAt: Date.now(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
status: "failed",
|
|
26
|
+
startedAt,
|
|
27
|
+
finishedAt: Date.now(),
|
|
28
|
+
error: error instanceof Error ? error.message : String(error),
|
|
29
|
+
errorType: error instanceof Error ? error.name : undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async executeAll(steps, context) {
|
|
34
|
+
const results = [];
|
|
35
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
36
|
+
const step = steps[index];
|
|
37
|
+
const result = await this.execute(step, context);
|
|
38
|
+
results.push(result);
|
|
39
|
+
if (result.status === "failed" && this.abortOnFailure) {
|
|
40
|
+
for (let remaining = index + 1; remaining < steps.length; remaining += 1) {
|
|
41
|
+
const timestamp = Date.now();
|
|
42
|
+
results.push({
|
|
43
|
+
status: "skipped",
|
|
44
|
+
startedAt: timestamp,
|
|
45
|
+
finishedAt: timestamp,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScenarioLoader.d.ts","sourceRoot":"","sources":["../../src/scenario/ScenarioLoader.ts"],"names":[],"mappings":"AAKA,qBAAa,cAAc;IACnB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqB/C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
export class ScenarioLoader {
|
|
6
|
+
async load(filePath) {
|
|
7
|
+
const absolutePath = resolve(filePath);
|
|
8
|
+
const extension = extname(absolutePath).toLowerCase();
|
|
9
|
+
if (extension === ".yaml" || extension === ".yml") {
|
|
10
|
+
const raw = await readFile(absolutePath, "utf-8");
|
|
11
|
+
return parseYaml(raw);
|
|
12
|
+
}
|
|
13
|
+
if (extension === ".json") {
|
|
14
|
+
const raw = await readFile(absolutePath, "utf-8");
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".js") {
|
|
18
|
+
const module = await import(pathToFileURL(absolutePath).href);
|
|
19
|
+
return module.default ?? module;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Unsupported scenario format: ${extension || "(no extension)"}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScenarioDocument } from "../types/scenario.js";
|
|
2
|
+
import { SchemaError } from "../errors.js";
|
|
3
|
+
export declare class ScenarioValidationError extends SchemaError {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class ScenarioValidator {
|
|
7
|
+
validate(document: unknown): ScenarioDocument;
|
|
8
|
+
private validateScenario;
|
|
9
|
+
private validateStep;
|
|
10
|
+
private validateExpect;
|
|
11
|
+
private validateNetwork;
|
|
12
|
+
private validateDynamicManifest;
|
|
13
|
+
private stepPath;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=ScenarioValidator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScenarioValidator.d.ts","sourceRoot":"","sources":["../../src/scenario/ScenarioValidator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKV,gBAAgB,EAGjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAqC3C,qBAAa,uBAAwB,SAAQ,WAAW;gBAC1C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,gBAAgB;IAwD7C,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,YAAY;IAkCpB,OAAO,CAAC,cAAc;IA8DtB,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,uBAAuB;IAoB/B,OAAO,CAAC,QAAQ;CAGjB"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { SchemaError } from "../errors.js";
|
|
2
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
const ensureString = (value, path) => {
|
|
4
|
+
if (typeof value !== "string") {
|
|
5
|
+
throw new ScenarioValidationError(`Expected string at ${path}`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
};
|
|
9
|
+
const ensureBoolean = (value, path) => {
|
|
10
|
+
if (typeof value !== "boolean") {
|
|
11
|
+
throw new ScenarioValidationError(`Expected boolean at ${path}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
const ensureNumber = (value, path) => {
|
|
16
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
17
|
+
throw new ScenarioValidationError(`Expected number at ${path}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
const ensureEnum = (value, allowed, path) => {
|
|
22
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
23
|
+
throw new ScenarioValidationError(`Expected ${allowed.join(" | ")} at ${path}`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
export class ScenarioValidationError extends SchemaError {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "ScenarioValidationError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class ScenarioValidator {
|
|
34
|
+
validate(document) {
|
|
35
|
+
if (!isRecord(document)) {
|
|
36
|
+
throw new ScenarioValidationError("Scenario document must be an object.");
|
|
37
|
+
}
|
|
38
|
+
const name = ensureString(document.name, "name");
|
|
39
|
+
const mode = "mode" in document
|
|
40
|
+
? ensureEnum(document.mode, ["dev", "ssg", "ssr"], "mode")
|
|
41
|
+
: undefined;
|
|
42
|
+
const baseUrl = "baseUrl" in document ? ensureString(document.baseUrl, "baseUrl") : undefined;
|
|
43
|
+
let setup;
|
|
44
|
+
if ("setup" in document) {
|
|
45
|
+
if (!isRecord(document.setup)) {
|
|
46
|
+
throw new ScenarioValidationError("setup must be an object");
|
|
47
|
+
}
|
|
48
|
+
const setupRaw = document.setup;
|
|
49
|
+
const allowed = new Set(["env"]);
|
|
50
|
+
for (const key of Object.keys(setupRaw)) {
|
|
51
|
+
if (!allowed.has(key)) {
|
|
52
|
+
throw new ScenarioValidationError(`Unknown setup key: ${key}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if ("env" in setupRaw) {
|
|
56
|
+
if (!isRecord(setupRaw.env)) {
|
|
57
|
+
throw new ScenarioValidationError("setup.env must be an object");
|
|
58
|
+
}
|
|
59
|
+
const envEntries = {};
|
|
60
|
+
for (const [key, value] of Object.entries(setupRaw.env)) {
|
|
61
|
+
envEntries[key] = ensureString(value, `setup.env.${key}`);
|
|
62
|
+
}
|
|
63
|
+
setup = { env: envEntries };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(document.scenarios)) {
|
|
67
|
+
throw new ScenarioValidationError("scenarios must be an array");
|
|
68
|
+
}
|
|
69
|
+
if (document.scenarios.length === 0) {
|
|
70
|
+
throw new ScenarioValidationError("scenarios must contain at least one scenario");
|
|
71
|
+
}
|
|
72
|
+
const scenarios = document.scenarios.map((scenario, index) => this.validateScenario(scenario, index));
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
mode: mode,
|
|
76
|
+
baseUrl,
|
|
77
|
+
setup,
|
|
78
|
+
scenarios,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
validateScenario(value, index) {
|
|
82
|
+
if (!isRecord(value)) {
|
|
83
|
+
throw new ScenarioValidationError(`Scenario at index ${index} must be an object`);
|
|
84
|
+
}
|
|
85
|
+
const id = ensureString(value.id, `scenarios[${index}].id`);
|
|
86
|
+
const name = "name" in value ? ensureString(value.name, `scenarios[${index}].name`) : undefined;
|
|
87
|
+
if (!Array.isArray(value.steps)) {
|
|
88
|
+
throw new ScenarioValidationError(`scenarios[${index}].steps must be an array`);
|
|
89
|
+
}
|
|
90
|
+
if (value.steps.length === 0) {
|
|
91
|
+
throw new ScenarioValidationError(`scenarios[${index}].steps must not be empty`);
|
|
92
|
+
}
|
|
93
|
+
const steps = value.steps.map((step, stepIndex) => this.validateStep(step, index, stepIndex));
|
|
94
|
+
return { id, name, steps };
|
|
95
|
+
}
|
|
96
|
+
validateStep(value, scenarioIndex, stepIndex) {
|
|
97
|
+
if (!isRecord(value)) {
|
|
98
|
+
throw new ScenarioValidationError(`scenarios[${scenarioIndex}].steps[${stepIndex}] must be an object`);
|
|
99
|
+
}
|
|
100
|
+
const keys = Object.keys(value);
|
|
101
|
+
if (keys.length !== 1) {
|
|
102
|
+
throw new ScenarioValidationError(`scenarios[${scenarioIndex}].steps[${stepIndex}] must have exactly one step type`);
|
|
103
|
+
}
|
|
104
|
+
if ("goto" in value) {
|
|
105
|
+
return { goto: ensureString(value.goto, this.stepPath(scenarioIndex, stepIndex, "goto")) };
|
|
106
|
+
}
|
|
107
|
+
if ("click" in value) {
|
|
108
|
+
return { click: ensureString(value.click, this.stepPath(scenarioIndex, stepIndex, "click")) };
|
|
109
|
+
}
|
|
110
|
+
if ("expect" in value) {
|
|
111
|
+
return {
|
|
112
|
+
expect: this.validateExpect(value.expect, this.stepPath(scenarioIndex, stepIndex, "expect")),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
validateExpect(value, path) {
|
|
118
|
+
if (!isRecord(value)) {
|
|
119
|
+
throw new ScenarioValidationError(`${path} must be an object`);
|
|
120
|
+
}
|
|
121
|
+
const expectSpec = {};
|
|
122
|
+
const customExpectations = {};
|
|
123
|
+
const knownKeys = new Set([
|
|
124
|
+
"selector",
|
|
125
|
+
"text",
|
|
126
|
+
"contains",
|
|
127
|
+
"exists",
|
|
128
|
+
"network",
|
|
129
|
+
"dynamicManifest",
|
|
130
|
+
"custom",
|
|
131
|
+
]);
|
|
132
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
133
|
+
if (!knownKeys.has(key)) {
|
|
134
|
+
customExpectations[key] = entry;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if ("selector" in value) {
|
|
138
|
+
expectSpec.selector = ensureString(value.selector, `${path}.selector`);
|
|
139
|
+
}
|
|
140
|
+
if ("text" in value) {
|
|
141
|
+
expectSpec.text = ensureString(value.text, `${path}.text`);
|
|
142
|
+
}
|
|
143
|
+
if ("contains" in value) {
|
|
144
|
+
expectSpec.contains = ensureString(value.contains, `${path}.contains`);
|
|
145
|
+
}
|
|
146
|
+
if ("exists" in value) {
|
|
147
|
+
expectSpec.exists = ensureBoolean(value.exists, `${path}.exists`);
|
|
148
|
+
}
|
|
149
|
+
if ("network" in value) {
|
|
150
|
+
expectSpec.network = this.validateNetwork(value.network, `${path}.network`);
|
|
151
|
+
}
|
|
152
|
+
if ("dynamicManifest" in value) {
|
|
153
|
+
expectSpec.dynamicManifest = this.validateDynamicManifest(value.dynamicManifest, `${path}.dynamicManifest`);
|
|
154
|
+
}
|
|
155
|
+
if ("custom" in value) {
|
|
156
|
+
if (!isRecord(value.custom)) {
|
|
157
|
+
throw new ScenarioValidationError(`${path}.custom must be an object`);
|
|
158
|
+
}
|
|
159
|
+
Object.assign(customExpectations, value.custom);
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(customExpectations).length > 0) {
|
|
162
|
+
expectSpec.custom = customExpectations;
|
|
163
|
+
}
|
|
164
|
+
if (Object.keys(expectSpec).length === 0) {
|
|
165
|
+
throw new ScenarioValidationError(`${path} must define at least one expectation`);
|
|
166
|
+
}
|
|
167
|
+
return expectSpec;
|
|
168
|
+
}
|
|
169
|
+
validateNetwork(value, path) {
|
|
170
|
+
if (!isRecord(value)) {
|
|
171
|
+
throw new ScenarioValidationError(`${path} must be an object`);
|
|
172
|
+
}
|
|
173
|
+
const expectation = {};
|
|
174
|
+
if ("url" in value) {
|
|
175
|
+
expectation.url = ensureString(value.url, `${path}.url`);
|
|
176
|
+
}
|
|
177
|
+
if ("method" in value) {
|
|
178
|
+
expectation.method = ensureString(value.method, `${path}.method`);
|
|
179
|
+
}
|
|
180
|
+
if ("status" in value) {
|
|
181
|
+
expectation.status = ensureNumber(value.status, `${path}.status`);
|
|
182
|
+
}
|
|
183
|
+
if ("count" in value) {
|
|
184
|
+
expectation.count = ensureNumber(value.count, `${path}.count`);
|
|
185
|
+
}
|
|
186
|
+
return expectation;
|
|
187
|
+
}
|
|
188
|
+
validateDynamicManifest(value, path) {
|
|
189
|
+
if (!isRecord(value)) {
|
|
190
|
+
throw new ScenarioValidationError(`${path} must be an object`);
|
|
191
|
+
}
|
|
192
|
+
const expectation = {};
|
|
193
|
+
if ("mode" in value) {
|
|
194
|
+
expectation.mode = ensureEnum(value.mode, ["meta", "program"], `${path}.mode`);
|
|
195
|
+
}
|
|
196
|
+
if ("pageId" in value) {
|
|
197
|
+
expectation.pageId = ensureString(value.pageId, `${path}.pageId`);
|
|
198
|
+
}
|
|
199
|
+
if ("version" in value) {
|
|
200
|
+
expectation.version = ensureString(value.version, `${path}.version`);
|
|
201
|
+
}
|
|
202
|
+
return expectation;
|
|
203
|
+
}
|
|
204
|
+
stepPath(scenarioIndex, stepIndex, key) {
|
|
205
|
+
return `scenarios[${scenarioIndex}].steps[${stepIndex}].${key}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scenario/types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,IAAI,EACJ,UAAU,EACV,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../src/steps/click.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,YAAY,GAAU,MAAM,SAAS,EAAE,SAAS,WAAW,kBAEvE,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExpectSpec } from "../types/scenario.js";
|
|
2
|
+
import type { StepContext } from "./types.js";
|
|
3
|
+
export interface ExpectStep {
|
|
4
|
+
expect: ExpectSpec;
|
|
5
|
+
}
|
|
6
|
+
export declare const runExpectStep: (step: ExpectStep, context: StepContext) => Promise<void>;
|
|
7
|
+
//# sourceMappingURL=expect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/steps/expect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,UAAU,CAAC;CACpB;AAsDD,eAAO,MAAM,aAAa,GAAU,MAAM,UAAU,EAAE,SAAS,WAAW,kBA4BzE,CAAC"}
|