@apica-io/asm-playwright-runner 1.0.0-dev.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/asm-playwright-runner.iml +8 -0
- package/dist/actions.js +47 -0
- package/dist/cli/index.js +97 -0
- package/dist/lib/helper.js +109 -0
- package/dist/lib/parser.js +309 -0
- package/dist/loader.js +5 -0
- package/dist/model/result.js +1 -0
- package/dist/model/runnerConfig.js +27 -0
- package/dist/model/traceModel.js +2 -0
- package/dist/runner.js +280 -0
- package/package.json +47 -0
- package/samples/betsson.spec.ts +86 -0
- package/samples/example.json +10 -0
- package/samples/google.json +6 -0
- package/samples/playwright-multi-script.spec.ts +60 -0
- package/samples/playwright-script.spec.ts +9 -0
- package/samples/playwright-test-script.spec.ts +18 -0
- package/samples/r.json +16 -0
- package/src/actions.ts +42 -0
- package/src/cli/index.ts +74 -0
- package/src/lib/helper.ts +81 -0
- package/src/lib/parser.ts +341 -0
- package/src/model/runnerConfig.ts +41 -0
- package/src/model/traceModel.ts +150 -0
- package/src/runner.ts +272 -0
- package/tsconfig.json +23 -0
package/src/runner.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import {Browser, BrowserContext, chromium, firefox, Page, webkit} from "playwright";
|
|
4
|
+
import {actionRegistry} from "./actions";
|
|
5
|
+
import {BrowserType, ResultDir, RunnerConfig, ScriptType} from "./model/runnerConfig";
|
|
6
|
+
import {prepareTraceModel, zipResources} from "./lib/parser";
|
|
7
|
+
import {getLogger, Logger} from "log4js";
|
|
8
|
+
import {spawn} from "node:child_process";
|
|
9
|
+
import {collectTraceFiles, loadFlow} from "./lib/helper";
|
|
10
|
+
|
|
11
|
+
const RESULT_FILE: string = "trace-model.json"
|
|
12
|
+
|
|
13
|
+
export class PlaywrightRunner {
|
|
14
|
+
private logger: Logger;
|
|
15
|
+
private browser: Browser | null = null;
|
|
16
|
+
private context: BrowserContext | null = null;
|
|
17
|
+
private page: Page | null = null;
|
|
18
|
+
private options: RunnerConfig | null = null;
|
|
19
|
+
|
|
20
|
+
public constructor() {
|
|
21
|
+
this.logger = getLogger(PlaywrightRunner.name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async init(collectionPath: string, options: RunnerConfig): Promise<void> {
|
|
25
|
+
this.logger.info("Initializing PlaywrightRunner...");
|
|
26
|
+
|
|
27
|
+
// Normalize resultDir
|
|
28
|
+
if (!options.resultDir || options.resultDir.trim() === "") {
|
|
29
|
+
options.resultDir = ResultDir.BASE_DIR;
|
|
30
|
+
this.logger.debug("No resultDir specified. Defaulting to 'results'.");
|
|
31
|
+
}
|
|
32
|
+
this.options = options;
|
|
33
|
+
|
|
34
|
+
// Detect script type
|
|
35
|
+
this.options.scriptType = await this.detectScriptType(collectionPath);
|
|
36
|
+
this.options.isValidScript = this.options.scriptType !== undefined;
|
|
37
|
+
|
|
38
|
+
if (!this.options.isValidScript) {
|
|
39
|
+
this.logger.warn("Script is not valid.");
|
|
40
|
+
process.exit(9);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Skip browser setup for Playwright Test files
|
|
44
|
+
if (this.options.scriptType === ScriptType.PLAYWRIGHT_TEST) {
|
|
45
|
+
this.logger.info("Playwright Test detected. Skipping manual browser setup.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Browser setup
|
|
50
|
+
const browserType = options.browser || BrowserType.CHROMIUM;
|
|
51
|
+
const headless = options.headless;
|
|
52
|
+
this.logger.debug(`Browser type: ${browserType}, headless: ${headless}`);
|
|
53
|
+
|
|
54
|
+
this.browser = await (
|
|
55
|
+
browserType === BrowserType.FIREFOX ? firefox :
|
|
56
|
+
browserType === BrowserType.WEBKIT ? webkit :
|
|
57
|
+
chromium
|
|
58
|
+
).launch({
|
|
59
|
+
headless,
|
|
60
|
+
...(options.chromiumPath && {executablePath: options.chromiumPath})
|
|
61
|
+
});
|
|
62
|
+
this.logger.info("Browser launched successfully.");
|
|
63
|
+
|
|
64
|
+
let headers: Record<string, string> = {};
|
|
65
|
+
if (options.extraHTTPHeaders) {
|
|
66
|
+
try {
|
|
67
|
+
headers = JSON.parse(options.extraHTTPHeaders);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this.logger.warn("Invalid JSON for extraHTTPHeaders, ignoring:", options.extraHTTPHeaders);
|
|
70
|
+
headers = {}; // fallback to empty headers
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.context = await this.browser.newContext({
|
|
75
|
+
ignoreHTTPSErrors: true,
|
|
76
|
+
...(options.sslClientCert && {
|
|
77
|
+
clientCert: {
|
|
78
|
+
cert: options.sslClientCert,
|
|
79
|
+
key: options.sslClientKey,
|
|
80
|
+
passphrase: options.sslClientPassphrase
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
extraHTTPHeaders: headers,
|
|
84
|
+
});
|
|
85
|
+
this.logger.info("Browser context created.");
|
|
86
|
+
|
|
87
|
+
await this.context.tracing.start({snapshots: true, screenshots: true, sources: true});
|
|
88
|
+
this.logger.info("Tracing started (snapshots, screenshots, sources enabled).");
|
|
89
|
+
|
|
90
|
+
this.page = await this.context.newPage();
|
|
91
|
+
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
if (this.options?.scriptType != ScriptType.PLAYWRIGHT_TEST && this.options.timeout != null) {
|
|
94
|
+
this.page.setDefaultTimeout(Number(this.options.timeout));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.logger.info("New page created.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public async run(collectionPath: string, options: RunnerConfig): Promise<void> {
|
|
101
|
+
this.logger.info(`Running collection: ${collectionPath}`);
|
|
102
|
+
|
|
103
|
+
// Default trace path(s)
|
|
104
|
+
let tracePaths: string[] = [
|
|
105
|
+
options.resultDir
|
|
106
|
+
? path.join(options.resultDir, "trace.zip")
|
|
107
|
+
: "trace.zip"
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
switch (this.options?.scriptType) {
|
|
111
|
+
case ScriptType.JSON: {
|
|
112
|
+
const flow = loadFlow(collectionPath);
|
|
113
|
+
this.logger.info(`Loaded flow: ${flow.name} with ${flow.steps.length} steps`);
|
|
114
|
+
|
|
115
|
+
for (const [index, step] of flow.steps.entries()) {
|
|
116
|
+
const handler = actionRegistry[step.action];
|
|
117
|
+
this.logger.debug(`Executing step ${index}: action=${step.action}`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (!handler) this.logger.error(`Unknown action: ${step.action}`);
|
|
121
|
+
if (!this.page) {
|
|
122
|
+
this.logger.error("Page not initialized. Did you call init()?");
|
|
123
|
+
process.exit(9);
|
|
124
|
+
}
|
|
125
|
+
await handler(this.page, step);
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
this.logger.error(`Step ${index} failed: ${err.message}`, err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case ScriptType.PLAYWRIGHT: {
|
|
134
|
+
this.logger.info("Detected plain Playwright script with default export.");
|
|
135
|
+
const mod = await import(path.resolve(collectionPath));
|
|
136
|
+
const fn = mod.default;
|
|
137
|
+
if (typeof fn !== "function") {
|
|
138
|
+
throw new Error("Script does not export a default function");
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await fn(this.page!);
|
|
142
|
+
this.logger.info("Script execution finished.");
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
this.logger.error(`Script execution failed: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case ScriptType.PLAYWRIGHT_TEST: {
|
|
150
|
+
this.logger.info("Detected Playwright Test file. Running via CLI...");
|
|
151
|
+
|
|
152
|
+
const args: any[] = [
|
|
153
|
+
"playwright",
|
|
154
|
+
"test",
|
|
155
|
+
collectionPath,
|
|
156
|
+
"--browser", options.browser || BrowserType.CHROMIUM,
|
|
157
|
+
"--output", options.resultDir || "results",
|
|
158
|
+
"--trace", "on"
|
|
159
|
+
];
|
|
160
|
+
if (options.timeout != null) args.push("--timeout", options.timeout);
|
|
161
|
+
if (!options.headless) args.push("--headed");
|
|
162
|
+
// if (options.verbose) args.push("--verbose");
|
|
163
|
+
|
|
164
|
+
this.logger.debug(`Spawning Playwright CLI: npx ${args.join(" ")}`);
|
|
165
|
+
|
|
166
|
+
await new Promise<void>((resolve) => {
|
|
167
|
+
const proc = spawn("npx", args, {stdio: "inherit", shell: true});
|
|
168
|
+
proc.on("close", (code) => {
|
|
169
|
+
if (code === 0) {
|
|
170
|
+
this.logger.info("Playwright Test finished successfully.");
|
|
171
|
+
} else {
|
|
172
|
+
this.logger.error(`Playwright Test exited with code ${code}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tracePaths = collectTraceFiles(options.resultDir || "results");
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
this.logger.error("Unsupported or invalid script type.");
|
|
184
|
+
process.exit(9);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.logger.info("Stopping tracing...");
|
|
188
|
+
await this.traceProcess(tracePaths);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async traceProcess(tracePaths: string[]): Promise<void> {
|
|
192
|
+
if (!tracePaths?.length) {
|
|
193
|
+
this.logger.warn("No trace paths provided.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Only stop tracing if this.context is active and we’re handling a live run
|
|
198
|
+
if (this.context && this.options?.scriptType != ScriptType.PLAYWRIGHT_TEST) {
|
|
199
|
+
await this.context.tracing.stop({
|
|
200
|
+
path: tracePaths[0]
|
|
201
|
+
});
|
|
202
|
+
this.logger.info(`Trace saved to ${tracePaths}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const models: Record<string, any> = {};
|
|
206
|
+
|
|
207
|
+
for (const tracePath of tracePaths) {
|
|
208
|
+
try {
|
|
209
|
+
this.logger.info(`Parsing trace: ${tracePath}`);
|
|
210
|
+
const traceModel = await prepareTraceModel(tracePath, this.options?.logLevel);
|
|
211
|
+
const runName = path.basename(path.dirname(tracePath));
|
|
212
|
+
models[runName] = traceModel;
|
|
213
|
+
|
|
214
|
+
const individualPath = path.join(path.dirname(tracePath), RESULT_FILE);
|
|
215
|
+
|
|
216
|
+
fs.writeFileSync(individualPath, JSON.stringify({"__self": traceModel}, null, 2), "utf-8");
|
|
217
|
+
this.logger.debug(`Individual trace model written to ${individualPath}`);
|
|
218
|
+
} catch (err: any) {
|
|
219
|
+
this.logger.error(`Failed to process trace ${tracePath}: ${err.message}`, err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Write combined model if more than one trace
|
|
224
|
+
if (this.options?.scriptType == ScriptType.PLAYWRIGHT_TEST) {
|
|
225
|
+
const combinedPath = this.options?.resultDir
|
|
226
|
+
? path.join(this.options.resultDir, RESULT_FILE)
|
|
227
|
+
: RESULT_FILE;
|
|
228
|
+
|
|
229
|
+
fs.writeFileSync(combinedPath, JSON.stringify(models, null, 2), "utf-8");
|
|
230
|
+
this.logger.info(`Combined trace model written to ${combinedPath}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const resourceDir = this.options?.resultDir ?? ResultDir.BASE_DIR;
|
|
234
|
+
const finalZipPath = path.join(resourceDir, `artifacts.zip`);
|
|
235
|
+
await zipResources(finalZipPath, path.join(resourceDir, ResultDir.SCREENSHOT), path.join(resourceDir, ResultDir.SOURCE));
|
|
236
|
+
|
|
237
|
+
if(this.options?.returnResult){
|
|
238
|
+
console.log(JSON.stringify(models));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async detectScriptType(collectionPath: string): Promise<ScriptType | undefined> {
|
|
243
|
+
try {
|
|
244
|
+
const mod = await import(path.resolve(collectionPath));
|
|
245
|
+
if (typeof mod.default === "function") {
|
|
246
|
+
this.logger.debug("Detected plain Playwright script (default export).");
|
|
247
|
+
return ScriptType.PLAYWRIGHT;
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
this.logger.debug("Dynamic import failed, falling back to file content check.");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (collectionPath.endsWith(".json")) {
|
|
254
|
+
this.logger.debug("Detected JSON flow.");
|
|
255
|
+
return ScriptType.JSON;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const fileContent = fs.readFileSync(collectionPath, "utf-8");
|
|
259
|
+
if (fileContent.includes("test(")) {
|
|
260
|
+
this.logger.debug("Detected Playwright Test file.");
|
|
261
|
+
return ScriptType.PLAYWRIGHT_TEST;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public async close(): Promise<void> {
|
|
268
|
+
this.logger.info("Closing browser...");
|
|
269
|
+
await this.browser?.close();
|
|
270
|
+
this.logger.info("Browser closed.");
|
|
271
|
+
}
|
|
272
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es6",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": false,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"allowJs": false,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
// "noEmit": true,
|
|
15
|
+
"noImplicitOverride": true,
|
|
16
|
+
"useUnknownInCatchVariables": false,
|
|
17
|
+
"useDefineForClassFields": false,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src"],
|
|
22
|
+
"exclude": ["node_modules", "dist", "results"]
|
|
23
|
+
}
|