@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.
@@ -0,0 +1,341 @@
1
+ import unzipper from 'unzipper';
2
+ import archiver from "archiver";
3
+ import {Action, HookAction, StackFrame, TraceModel} from "../model/traceModel";
4
+ import path from "path";
5
+ import fs from "fs";
6
+ import {LogLevel, ResultDir} from "../model/runnerConfig";
7
+
8
+ export async function getJsonFileData(tracePath: string, fileName: string): Promise<Record<string, any>> {
9
+ const directory = await unzipper.Open.file(tracePath);
10
+
11
+ const file = directory.files.find(
12
+ f => f.path.endsWith(fileName)
13
+ );
14
+
15
+ if (!file) {
16
+ return {}
17
+ }
18
+
19
+ const content = await file.buffer();
20
+ return JSON.parse(content.toString("utf-8"));
21
+ }
22
+
23
+ export async function getFileData(tracePath: string, fileName: string): Promise<string[]> {
24
+ const directory = await unzipper.Open.file(tracePath);
25
+
26
+ const traceFile = directory.files.find(f => f.path.endsWith(fileName));
27
+ if (!traceFile) {
28
+ return [];
29
+ }
30
+
31
+ const content = await traceFile.buffer();
32
+ return content.toString('utf-8').split('\n').filter(Boolean);
33
+ }
34
+
35
+ async function extractResources(
36
+ zipPath: string,
37
+ targetImages: string[] = []
38
+ ) {
39
+ const baseDir = path.dirname(zipPath);
40
+ const runName = path.basename(baseDir);
41
+ const resultDir = path.dirname(baseDir);
42
+
43
+ const parentDir = resultDir === "." ? runName : resultDir;
44
+ const appendRunName = resultDir === "." ? [] : [runName];
45
+
46
+ const screenshotsDir = path.join(
47
+ parentDir, ResultDir.SCREENSHOT, ...appendRunName
48
+ );
49
+
50
+ const sourceDir = path.join(
51
+ parentDir, ResultDir.SOURCE, ...appendRunName
52
+ );
53
+
54
+ // Create folders if not exists
55
+ [screenshotsDir, sourceDir].forEach(dir => {
56
+ if (!fs.existsSync(dir)) {
57
+ fs.mkdirSync(dir, {recursive: true});
58
+ }
59
+ });
60
+
61
+ const directory = await unzipper.Open.file(zipPath);
62
+
63
+ // Copy target images
64
+ const imagePromises = targetImages.map(targetImage => {
65
+ const fileEntry = directory.files.find(
66
+ (file: { path: string; }) => file.path === `resources/${targetImage}`
67
+ );
68
+
69
+ if (!fileEntry) {
70
+ return Promise.resolve();
71
+ }
72
+
73
+ const outputPath = path.join(
74
+ screenshotsDir, targetImage
75
+ );
76
+
77
+ return new Promise((resolve, reject) => {
78
+ fileEntry
79
+ .stream()
80
+ .pipe(fs.createWriteStream(outputPath))
81
+ .on('finish', () => {
82
+ resolve(true);
83
+ })
84
+ .on('error', reject);
85
+ });
86
+ });
87
+
88
+ // Copy all src@*.txt files
89
+ const sourcePromises = directory.files
90
+ .filter(
91
+ file =>
92
+ file.path.startsWith('resources/') &&
93
+ /^src@.*\.txt$/.test(path.basename(file.path))
94
+ )
95
+ .map(fileEntry => {
96
+ const fileName = path.basename(fileEntry.path);
97
+ const outputPath = path.join(sourceDir, fileName);
98
+
99
+ return new Promise((resolve, reject) => {
100
+ fileEntry
101
+ .stream()
102
+ .pipe(fs.createWriteStream(outputPath))
103
+ .on('finish', () => {
104
+ resolve(true);
105
+ })
106
+ .on('error', reject);
107
+ });
108
+ });
109
+
110
+ await Promise.all([...imagePromises, ...sourcePromises]);
111
+ }
112
+
113
+ export async function zipResources(outputZipPath: string, screenshotsDir: string, sourceDir: string) {
114
+ return new Promise<void>((resolve, reject) => {
115
+ const output = fs.createWriteStream(outputZipPath);
116
+ const archive = archiver("zip", { zlib: { level: 9 } });
117
+
118
+ output.on("close", resolve);
119
+ archive.on("error", reject);
120
+
121
+ archive.pipe(output);
122
+
123
+ if (fs.existsSync(screenshotsDir)) {
124
+ archive.directory(screenshotsDir, ResultDir.SCREENSHOT);
125
+ }
126
+
127
+ if (fs.existsSync(sourceDir)) {
128
+ archive.directory(sourceDir, ResultDir.SOURCE);
129
+ }
130
+
131
+ archive.finalize();
132
+ });
133
+ }
134
+
135
+ export async function setHookActions(traceRawResult: string[]): Promise<TraceModel> {
136
+ const hookModel: TraceModel = {actions: [], stdio: []};
137
+ const hookActions: Record<string, HookAction> = {};
138
+
139
+ for (const [index, row] of traceRawResult.entries()) {
140
+ const entry = JSON.parse(row);
141
+
142
+ if (entry.type === "context-options") {
143
+ Object.assign(hookModel, {
144
+ origin: entry.origin,
145
+ browserName: entry.browserName,
146
+ playwrightVersion: entry.playwrightVersion,
147
+ options: entry.options,
148
+ platform: entry.platform,
149
+ wallTime: entry.wallTime,
150
+ startTime: entry.monotonicTime,
151
+ sdkLanguage: entry.sdkLanguage,
152
+ testIdAttributeName: entry.testIdAttributeName,
153
+ contextId: entry.contextId,
154
+ testTimeout: entry.testTimeout,
155
+ });
156
+ continue;
157
+ } else if (entry.type === "stdout") {
158
+ hookModel?.stdio?.push(entry)
159
+ } else if (entry.type === "before") {
160
+ const action: HookAction = {
161
+ type: "action",
162
+ callId: entry.callId,
163
+ stepId: entry.stepId,
164
+ parentId: entry.parentId,
165
+ startTime: entry.startTime,
166
+ class: entry.class,
167
+ method: entry.method,
168
+ title: entry.title,
169
+ params: entry.params,
170
+ stack: entry.stack,
171
+ log: [],
172
+ };
173
+ hookActions[entry.callId] = action;
174
+ hookModel.actions!.push(action);
175
+ } else if (entry.type === "after" && hookActions[entry.callId]) {
176
+ Object.assign(hookActions[entry.callId], {
177
+ endTime: entry.endTime,
178
+ annotations: entry.annotations,
179
+ });
180
+ }
181
+
182
+ if (index === (traceRawResult.length - 1) && entry.type === "after") {
183
+ hookModel.endTime = entry.endTime;
184
+ }
185
+ }
186
+
187
+ return hookModel;
188
+ }
189
+
190
+
191
+ export async function setActions(traceRawResult: string[], traceModel: TraceModel, tracePath: string, stackData: Record<string, any>) {
192
+ let beforeFlag = false
193
+ let action: Action | null = null;
194
+ let targetImages: string[] = [];
195
+
196
+ for (const row of traceRawResult) {
197
+ const entry = JSON.parse(row);
198
+ if (entry.type === 'context-options') {
199
+ traceModel.origin = entry.origin;
200
+ traceModel.browserName = entry.browserName;
201
+ traceModel.playwrightVersion = entry.playwrightVersion;
202
+ traceModel.options = entry.options;
203
+ traceModel.platform = entry.platform;
204
+ traceModel.wallTime = entry.wallTime;
205
+ traceModel.startTime = entry.monotonicTime;
206
+ traceModel.sdkLanguage = entry.sdkLanguage;
207
+ traceModel.testIdAttributeName = entry.testIdAttributeName;
208
+ traceModel.contextId = entry.contextId;
209
+ } else if (entry.type == "event" || entry.type == "console") {
210
+ traceModel.events?.push(entry);
211
+ } else if (entry.type == "screencast-frame") {
212
+ let page = traceModel.pages?.find(
213
+ p => p.pageId === entry.pageId
214
+ );
215
+
216
+ if (!page) {
217
+ page = {
218
+ pageId: entry.pageId,
219
+ screencastFrames: [],
220
+ };
221
+
222
+ traceModel.pages?.push(page);
223
+ }
224
+ targetImages.push(entry.sha1);
225
+ page.screencastFrames.push(entry);
226
+ } else {
227
+ if (entry.type == "before") {
228
+ action = {
229
+ callId: entry.callId,
230
+ class: entry.class,
231
+ method: entry.method,
232
+ startTime: entry.startTime,
233
+ type: "action",
234
+ params: entry.params,
235
+ pageId: entry.pageId,
236
+ log: [],
237
+ stack: stackData[entry.callId]
238
+ }
239
+
240
+ if (entry.class == "Frame") {
241
+ action.beforeSnapshot = entry.beforeSnapshot
242
+ }
243
+
244
+ beforeFlag = true
245
+ }
246
+
247
+ if (beforeFlag && action != null && action.callId == entry.callId) {
248
+ if (entry.type == "log") {
249
+ action.log?.push({
250
+ time: entry.time,
251
+ message: entry.message
252
+ })
253
+ }
254
+
255
+ if (entry.type == "after") {
256
+ beforeFlag = false
257
+ action.endTime = entry.endTime;
258
+ action.result = entry.result;
259
+ action.error = entry.error;
260
+ action.afterSnapshot = entry.afterSnapshot
261
+ traceModel.actions?.push(action);
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ traceModel.endTime = action?.endTime
268
+ await extractResources(tracePath, targetImages)
269
+ }
270
+
271
+ export async function setResources(resourceRawResult: string[], traceModel: TraceModel) {
272
+ for (const row of resourceRawResult) {
273
+ const entry = JSON.parse(row);
274
+ traceModel.resources?.push(entry.snapshot);
275
+ }
276
+ }
277
+
278
+ interface PreparedStackData {
279
+ [key: string]: StackFrame[];
280
+ }
281
+
282
+ export async function prepareStackData(stacksRawResult: Record<string, any>): Promise<PreparedStackData> {
283
+ const preparedData: PreparedStackData = {};
284
+ const files: string[] = stacksRawResult.files || [];
285
+ const stacks = stacksRawResult.stacks || [];
286
+
287
+ for (const stack of stacks) {
288
+ const [callId, frames] = stack;
289
+ preparedData[`call@${callId}`] = frames.map(
290
+ ([fileIndex, line, column, fn]: [number, number, number, string]) => ({
291
+ file: files[fileIndex] || `source-file-${fileIndex}`,
292
+ fileIndex: `source-file-${fileIndex}`,
293
+ line,
294
+ column,
295
+ functionName: fn || ""
296
+ })
297
+ );
298
+ }
299
+
300
+ return preparedData;
301
+ }
302
+
303
+ export async function prepareTraceModel(tracePath: string, logLevel: LogLevel | undefined): Promise<TraceModel[]> {
304
+ const traceRawResult = await getFileData(tracePath, "trace.trace");
305
+ const resourceRawResult = await getFileData(tracePath, "trace.network");
306
+ const stacksRawResult = await getJsonFileData(tracePath, "trace.stacks")
307
+ const traceModel: TraceModel = {
308
+ actions: [],
309
+ events: [],
310
+ resources: [],
311
+ errors: [],
312
+ pages: []
313
+ };
314
+
315
+ if (logLevel == LogLevel.DEBUG) {
316
+ fs.writeFileSync(
317
+ path.join(path.dirname(tracePath), "trace-raw-data.json"),
318
+ JSON.stringify(traceRawResult, null, 2), "utf-8"
319
+ );
320
+ }
321
+
322
+ const stackData = await prepareStackData(stacksRawResult);
323
+ await setActions(traceRawResult, traceModel, tracePath, stackData);
324
+ await setResources(resourceRawResult, traceModel);
325
+
326
+ const testHookResult = await getFileData(tracePath, "test.trace");
327
+ if (testHookResult && testHookResult.length > 0) {
328
+ if (logLevel == LogLevel.DEBUG) {
329
+ //raw data to validate data from trace
330
+ fs.writeFileSync(
331
+ path.join(path.dirname(tracePath), "test-trace-raw-data.json"),
332
+ JSON.stringify(testHookResult, null, 2), "utf-8"
333
+ );
334
+ }
335
+
336
+ const hookModel = await setHookActions(testHookResult)
337
+ return [hookModel, traceModel]
338
+ }
339
+
340
+ return [traceModel];
341
+ }
@@ -0,0 +1,41 @@
1
+ export interface RunnerConfig {
2
+ browser: BrowserType;
3
+ chromiumPath?: string;
4
+ headless: boolean;
5
+ verbose: boolean;
6
+ resultDir: string;
7
+ logLevel: LogLevel;
8
+ trace?: string;
9
+ returnResult: boolean
10
+ sslClientCert?: string;
11
+ sslClientKey?: string;
12
+ sslClientPassphrase?: string;
13
+ isValidScript?: boolean;
14
+ scriptType?: ScriptType;
15
+ extraHTTPHeaders?: string;
16
+ timeout?: number;
17
+ }
18
+
19
+ export enum BrowserType {
20
+ CHROMIUM = "chromium",
21
+ FIREFOX = "firefox",
22
+ WEBKIT = "webkit"
23
+ }
24
+
25
+ export enum LogLevel {
26
+ INFO = "info",
27
+ DEBUG = "debug",
28
+ ERROR = "error"
29
+ }
30
+
31
+ export enum ScriptType {
32
+ JSON = "json",
33
+ PLAYWRIGHT = "playwright",
34
+ PLAYWRIGHT_TEST = "playwright/test"
35
+ }
36
+
37
+ export enum ResultDir {
38
+ BASE_DIR = "result",
39
+ SCREENSHOT = "screenshots",
40
+ SOURCE = "source"
41
+ }
@@ -0,0 +1,150 @@
1
+ export interface TraceModel {
2
+ actions?: Action[];
3
+ browserName?: string;
4
+ contextId?: string;
5
+ endTime?: number;
6
+ events?: Event[];
7
+ hasSource?: boolean;
8
+ options?: BrowserContextOptions;
9
+ origin?: string;
10
+ pages?: Page[];
11
+ platform?: string;
12
+ playwrightVersion?: string;
13
+ resources?: Resource[];
14
+ sdkLanguage?: string;
15
+ startTime?: number;
16
+ testIdAttributeName?: string;
17
+ wallTime?: number;
18
+ stdio?: Record<string, any>[];
19
+
20
+ // this is for hook test model
21
+ testTimeout?: number
22
+
23
+ errors?: [];
24
+ }
25
+
26
+ export interface Resource {
27
+ cache: Record<string, any>;
28
+ pageref: string;
29
+ request: Request;
30
+ response: Response;
31
+ serverIPAddress: string;
32
+ startedDateTime: string;
33
+ time: number;
34
+ timings: Timing;
35
+ _frameref: string;
36
+ _monotonicTime: number;
37
+ _securityDetails: Record<string, any>;
38
+ _serverPort: number
39
+ }
40
+
41
+ export interface Timing {
42
+ connect: number;
43
+ dns: number;
44
+ receive: number;
45
+ send: number;
46
+ ssl: number;
47
+ wait: number;
48
+ }
49
+
50
+ export interface Response {
51
+ bodySize: number;
52
+ content: Record<string, any>;
53
+ cookies: string[];
54
+ headers: Record<string, any>;
55
+ headersSize: number;
56
+ httpVersion: string;
57
+ redirectURL: string;
58
+ status: number;
59
+ statusText: string;
60
+ _transferSize: number;
61
+ }
62
+
63
+ export interface Request {
64
+ bodySize: number;
65
+ cookies: string[];
66
+ headers: Record<string, any>;
67
+ headersSize: number;
68
+ httpVersion: string;
69
+ method: string;
70
+ queryString: string[];
71
+ url: string;
72
+ }
73
+
74
+ export interface Page {
75
+ pageId: string;
76
+ screencastFrames: ScreencastFrame[]
77
+ }
78
+
79
+ export interface ScreencastFrame {
80
+ frameSwapWallTime: number;
81
+ height: number;
82
+ pageId: string;
83
+ sha1: string;
84
+ timestamp: number;
85
+ type: string;
86
+ width: number;
87
+ }
88
+
89
+ export interface Event {
90
+ class: string;
91
+ method: string;
92
+ params: Record<string, any>;
93
+ time: number;
94
+ type: 'event';
95
+ }
96
+
97
+ export type TraceEventType =
98
+ | 'context-options'
99
+ | 'before'
100
+ | 'after'
101
+ | 'event';
102
+
103
+ export interface BrowserContextOptions {
104
+ noDefaultViewport?: boolean;
105
+ ignoreHTTPSErrors?: boolean;
106
+ selectorEngines?: any[];
107
+ acceptDownloads?: string;
108
+ viewport?: Viewport;
109
+ }
110
+
111
+ export interface HookAction extends Action{
112
+ stepId?: string;
113
+ annotations?: [];
114
+ title?: string;
115
+ parentId?: string;
116
+ }
117
+
118
+ export interface Action {
119
+ type: 'action';
120
+ callId: string;
121
+ startTime: number;
122
+ endTime?: number;
123
+ class: string;
124
+ method: string;
125
+ params?: Record<string, any>;
126
+ pageId?: string;
127
+ beforeSnapshot?: string;
128
+ afterSnapshot?: string;
129
+ log?: ActionLog[];
130
+ result?: Record<string, any>;
131
+ stack?: StackFrame[];
132
+ error?: Record<string, any>;
133
+ }
134
+
135
+ export interface ActionLog {
136
+ time: number;
137
+ message: string;
138
+ }
139
+
140
+ export interface StackFrame {
141
+ file: string;
142
+ line: number;
143
+ column: number;
144
+ function: string;
145
+ }
146
+
147
+ export interface Viewport {
148
+ width: number;
149
+ height: number;
150
+ }