@mablhq/mabl-cli 2.29.7 → 2.31.2

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,439 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.processTracesPath = exports.processSingleTraceFile = void 0;
30
+ const env_1 = require("../../../../env/env");
31
+ const pluralize_1 = __importDefault(require("pluralize"));
32
+ const inquirer = __importStar(require("inquirer"));
33
+ const mablApiClientFactory_1 = require("../../../../api/mablApiClientFactory");
34
+ const util_1 = require("../../../commandUtil/util");
35
+ const mablApi_1 = require("../../../../mablApi");
36
+ const pureUtil_1 = require("../../../../util/pureUtil");
37
+ const constants_1 = require("../../../constants");
38
+ const loggingProvider_1 = require("../../../../providers/logging/loggingProvider");
39
+ const chromiumBrowserEngine_1 = require("../../../../browserEngines/chromiumBrowserEngine");
40
+ const RichPromise_1 = __importDefault(require("../../../../util/RichPromise"));
41
+ const { spawn } = require('child_process');
42
+ const fs_1 = __importDefault(require("fs"));
43
+ const path_1 = __importDefault(require("path"));
44
+ const adm_zip_1 = __importDefault(require("adm-zip"));
45
+ const execution_1 = require("../../../../execution");
46
+ const projectInfoTestFile = `
47
+ import { test } from "@playwright/test";
48
+ test("mablImportInfo", async ({baseURL, trace}) => {
49
+ console.log('@mabl project info',
50
+ JSON.stringify({
51
+ baseURL: baseURL,
52
+ })
53
+ );
54
+ });
55
+ `;
56
+ var PostImportActions;
57
+ (function (PostImportActions) {
58
+ PostImportActions["Discard"] = "Discard the test";
59
+ PostImportActions["Run"] = "Run the test locally";
60
+ PostImportActions["Save"] = "Save the test";
61
+ PostImportActions["View"] = "View the test description";
62
+ })(PostImportActions || (PostImportActions = {}));
63
+ exports.command = 'playwright';
64
+ exports.describe =
65
+ '[LABS]: Import an existing Playwright test.\nThis functionality is in an experimental stage.';
66
+ exports.builder = (yargs) => {
67
+ yargs
68
+ .option(constants_1.CommandArgAuto, {
69
+ default: false,
70
+ describe: 'Automatically save imported test when complete',
71
+ requiresArg: false,
72
+ type: 'boolean',
73
+ })
74
+ .option(constants_1.CommandArgWorkspaceId, {
75
+ alias: constants_1.CommandArgAliases.WorkspaceId,
76
+ describe: 'Workspace into which tests should be imported',
77
+ nargs: 1,
78
+ requiresArg: true,
79
+ type: 'string',
80
+ })
81
+ .option(constants_1.CommandArgPath, {
82
+ describe: 'Project location. Current directory is default',
83
+ nargs: 1,
84
+ type: 'string',
85
+ })
86
+ .option(constants_1.CommandArgProject, {
87
+ describe: 'Playwright project to run. This project must have tracing enabled.',
88
+ nargs: 1,
89
+ type: 'string',
90
+ })
91
+ .option(constants_1.CommandArgGrep, {
92
+ describe: 'Grep argument to pass to Playwright to filter tests',
93
+ nargs: 1,
94
+ type: 'string',
95
+ })
96
+ .option(constants_1.CommandArgExtraArguments, {
97
+ describe: 'Extra arguments to pass to playwright',
98
+ nargs: 1,
99
+ type: 'string',
100
+ })
101
+ .option(constants_1.CommandArgTestPath, {
102
+ describe: 'Path where the tests are located',
103
+ type: 'string',
104
+ default: 'tests',
105
+ })
106
+ .option(constants_1.CommandArgTraceFile, {
107
+ describe: 'Path of a single playwright trace file to import',
108
+ type: 'string',
109
+ })
110
+ .option(constants_1.CommandArgTracesPath, {
111
+ describe: 'Path of the directory containing playwright trace files to import',
112
+ type: 'string',
113
+ })
114
+ .check((argv) => {
115
+ if (!argv[constants_1.CommandArgTraceFile] &&
116
+ !argv[constants_1.CommandArgTracesPath] &&
117
+ !argv[constants_1.CommandArgPath]) {
118
+ throw new Error(`Please pass in one of --${constants_1.CommandArgTraceFile}, --${constants_1.CommandArgTracesPath}, or ${constants_1.CommandArgPath}`);
119
+ }
120
+ if (argv[constants_1.CommandArgTraceFile] && argv[constants_1.CommandArgTracesPath]) {
121
+ throw new Error(`Only one of --${constants_1.CommandArgTraceFile} or --${constants_1.CommandArgTracesPath} can be provided`);
122
+ }
123
+ return true;
124
+ });
125
+ };
126
+ exports.handler = (0, util_1.failWrapper)(processCommand);
127
+ async function processCommand(parsed) {
128
+ const workspaceId = await (0, util_1.getWorkspaceId)(parsed);
129
+ const apiClient = await mablApiClientFactory_1.MablApiClientFactory.createApiClient();
130
+ const importedTests = await importTests(parsed);
131
+ if (!importedTests.length) {
132
+ return Promise.reject(new Error(`No tests were imported.`));
133
+ }
134
+ if (importedTests.length > 1) {
135
+ loggingProvider_1.logger.info(`Imported ${importedTests.length} tests.\n`);
136
+ }
137
+ else {
138
+ loggingProvider_1.logger.info(`Imported ${(0, pluralize_1.default)('step', importedTests[0].steps.length, true)}.\n`);
139
+ }
140
+ return handlePostImportActions(workspaceId, apiClient, importedTests, parsed[constants_1.CommandArgAuto]);
141
+ }
142
+ async function importTests(args) {
143
+ var _a, _b;
144
+ const cwd = (_a = args[constants_1.CommandArgPath]) !== null && _a !== void 0 ? _a : process.cwd();
145
+ const tracesPath = (_b = args[constants_1.CommandArgTracesPath]) !== null && _b !== void 0 ? _b : path_1.default.join(cwd, 'test-results');
146
+ const playwrightProject = args[constants_1.CommandArgProject]
147
+ ? await getProjectInfo(args)
148
+ : { baseUrl: '' };
149
+ if (!args[constants_1.CommandArgTraceFile] && !args[constants_1.CommandArgTracesPath]) {
150
+ await runPlaywrightProject(args, tracesPath, cwd);
151
+ }
152
+ let importedTests = [];
153
+ const converter = new execution_1.PlaywrightToMablStepConverter();
154
+ if (args[constants_1.CommandArgTraceFile]) {
155
+ importedTests = processSingleTraceFile(args[constants_1.CommandArgTraceFile], playwrightProject, converter);
156
+ }
157
+ else {
158
+ importedTests = processTracesPath(tracesPath, playwrightProject, converter);
159
+ }
160
+ converter.printMissingApis(loggingProvider_1.logger);
161
+ return importedTests;
162
+ }
163
+ async function handlePostImportActions(workspaceId, apiClient, importedTests, autoSave) {
164
+ let nextAction;
165
+ do {
166
+ nextAction = await getNextAction(autoSave);
167
+ loggingProvider_1.logger.logNewLine();
168
+ switch (nextAction) {
169
+ case PostImportActions.Save:
170
+ return saveTests(workspaceId, apiClient, importedTests).then(() => {
171
+ loggingProvider_1.logger.info('Import complete.');
172
+ });
173
+ case PostImportActions.View:
174
+ displayTestDescriptions(importedTests);
175
+ break;
176
+ case PostImportActions.Run:
177
+ await runTests(workspaceId, apiClient, importedTests);
178
+ break;
179
+ case PostImportActions.Discard:
180
+ loggingProvider_1.logger.info('Exiting without saving.');
181
+ return;
182
+ }
183
+ loggingProvider_1.logger.logNewLine();
184
+ } while ([PostImportActions.View, PostImportActions.Run].includes(nextAction));
185
+ }
186
+ async function getNextAction(autoSave) {
187
+ if (autoSave) {
188
+ return PostImportActions.Save;
189
+ }
190
+ const answer = await inquirer.prompt([
191
+ {
192
+ type: 'list',
193
+ name: 'action',
194
+ hint: false,
195
+ message: 'What do you want to do next?',
196
+ choices: Object.values(PostImportActions),
197
+ },
198
+ ]);
199
+ return answer.action;
200
+ }
201
+ function saveTests(workspaceId, apiClient, importedTests) {
202
+ loggingProvider_1.logger.info(`Saving ${(0, pluralize_1.default)('test', importedTests.length, true)}...`);
203
+ const savePromises = importedTests.map((test) => apiClient
204
+ .createFlow(stepsToFlow(workspaceId, test.steps))
205
+ .then((flow) => apiClient.createJourney(flowToJourney(test.testName, flow)))
206
+ .then((journey) => {
207
+ loggingProvider_1.logger.info(`Saved test ${journey.invariant_id} : ${env_1.BASE_APP_URL}/workspaces/${journey.organization_id}/train/tests/${journey.invariant_id}/current`);
208
+ return journey;
209
+ }));
210
+ return Promise.all(savePromises);
211
+ }
212
+ function displayTestDescriptions(importedTests) {
213
+ importedTests.forEach((test, index) => {
214
+ if (importedTests.length > 1) {
215
+ loggingProvider_1.logger.info(`Test ${index + 1} of ${importedTests.length}: '${test.testName}'`);
216
+ }
217
+ loggingProvider_1.logger.info(test.steps.map((step) => step.description).join('\n'));
218
+ loggingProvider_1.logger.logNewLine();
219
+ });
220
+ }
221
+ async function runTests(workspaceId, apiClient, importedTests) {
222
+ var _a, _b, _c;
223
+ const tests = importedTests.map((test) => ({
224
+ test,
225
+ flow: stepsToFlow(workspaceId, test.steps),
226
+ }));
227
+ const firstUrl = (_b = (_a = tests.find((test) => test.flow.url)) === null || _a === void 0 ? void 0 : _a.flow) === null || _b === void 0 ? void 0 : _b.url;
228
+ const browserPath = await new chromiumBrowserEngine_1.ChromiumBrowserEngine().findBrowserExecutable();
229
+ for (let testIndex = 0; testIndex < tests.length; testIndex++) {
230
+ const testToRun = tests[testIndex];
231
+ loggingProvider_1.logger.info(`Running test '${testToRun.test.testName}' ${testIndex + 1} of ${tests.length}.`);
232
+ const journey = {
233
+ organization_id: workspaceId,
234
+ name: testToRun.test.testName,
235
+ flows: [],
236
+ url: (_c = testToRun.flow.url) !== null && _c !== void 0 ? _c : firstUrl,
237
+ };
238
+ const testRunner = await createTestRunner(apiClient, journey, testToRun.flow, journey.url, workspaceId, browserPath);
239
+ const results = await testRunner.run();
240
+ loggingProvider_1.logger.info(`Test result: ${results.status}`);
241
+ }
242
+ }
243
+ function stepsToFlow(workspaceId, steps) {
244
+ const prototype = {
245
+ import_source_id: mablApi_1.Flow.ImportSourceIdEnum.SeleniumNodeProxy,
246
+ import_source_version: (0, pureUtil_1.getCliVersion)(),
247
+ flow_type: mablApi_1.Flow.FlowTypeEnum.Mablscript,
248
+ organization_id: workspaceId,
249
+ reusable: false,
250
+ selectors: steps
251
+ .map((step) => step.selector)
252
+ .map((selector) => selector === null || selector === void 0 ? void 0 : selector.toMablscriptSelector())
253
+ .filter((selector) => selector)
254
+ .map((selector) => selector),
255
+ script: steps.map((step) => step.mablscript).join('\n'),
256
+ script_description: steps.map((step) => step.description).join('\n'),
257
+ url: steps.map((step) => step.url).find((url) => url),
258
+ };
259
+ return prototype;
260
+ }
261
+ function flowToJourney(name, flow) {
262
+ const prototype = {
263
+ organization_id: flow.organization_id,
264
+ name,
265
+ flows: [flow.id],
266
+ url: flow.url,
267
+ };
268
+ return prototype;
269
+ }
270
+ async function createTestRunner(apiClient, test, flow, url, workspaceId, browserPath) {
271
+ const runner = new execution_1.BrowserTestRunner({
272
+ testRunConfig: {
273
+ _cliCreated: true,
274
+ imported: true,
275
+ filterHttpRequests: false,
276
+ url,
277
+ workspaceId,
278
+ },
279
+ mablApiClient: apiClient,
280
+ });
281
+ await runner.initializeTestRunner(test, [flow], 'master', apiClient, false, browserPath, { url });
282
+ return runner;
283
+ }
284
+ function processSingleTraceFile(traceFile, playwrightProject, converter) {
285
+ const importedTests = [];
286
+ if (!fs_1.default.existsSync(traceFile)) {
287
+ throw new Error(`Trace file not found: ${traceFile}`);
288
+ }
289
+ const importedTest = processTestResult(traceFile, playwrightProject, converter);
290
+ if (importedTest) {
291
+ importedTests.push(importedTest);
292
+ }
293
+ return importedTests;
294
+ }
295
+ exports.processSingleTraceFile = processSingleTraceFile;
296
+ function processTracesPath(tracesPath, playwrightProject, converter) {
297
+ const importedTests = [];
298
+ const testResults = fs_1.default.readdirSync(tracesPath);
299
+ console.log(`Found ${testResults.length} test results`);
300
+ for (const testResult of testResults) {
301
+ const testResultPath = path_1.default.join(tracesPath, testResult);
302
+ const tracePath = path_1.default.join(testResultPath, 'trace.zip');
303
+ const importedTest = processTestResult(tracePath, playwrightProject, converter);
304
+ if (importedTest) {
305
+ importedTests.push(importedTest);
306
+ }
307
+ }
308
+ return importedTests;
309
+ }
310
+ exports.processTracesPath = processTracesPath;
311
+ function processTestResult(traceZip, playwrightProject, converter) {
312
+ const testResultPath = path_1.default.dirname(traceZip);
313
+ if (!fs_1.default.existsSync(traceZip)) {
314
+ return;
315
+ }
316
+ const zip = new adm_zip_1.default(traceZip);
317
+ const zipEntries = zip.getEntries();
318
+ const lines = [];
319
+ const traceInfoFiles = zipEntries.filter((entry) => entry.entryName.endsWith('-trace.trace'));
320
+ traceInfoFiles.sort((a, b) => {
321
+ const aNum = parseInt(a.entryName.split('-')[0]);
322
+ const bNum = parseInt(b.entryName.split('-')[0]);
323
+ return aNum - bNum;
324
+ });
325
+ for (const entry of traceInfoFiles) {
326
+ const entryContent = entry.getData().toString('utf8');
327
+ fs_1.default.writeFileSync(path_1.default.join(testResultPath, entry.entryName), entryContent);
328
+ lines.push(...entryContent.split('\n'));
329
+ }
330
+ const traceLines = lines
331
+ .filter((line) => line.trim() !== '')
332
+ .map((line) => {
333
+ try {
334
+ return JSON.parse(line);
335
+ }
336
+ catch (e) {
337
+ return;
338
+ }
339
+ })
340
+ .filter((line) => line !== undefined);
341
+ const testName = getTestNameFromTraceInfo(traceLines);
342
+ const steps = converter.getStepsFromTraceInfo(traceLines, playwrightProject.baseUrl);
343
+ return { testName, steps };
344
+ }
345
+ function getTestNameFromTraceInfo(traceLines) {
346
+ var _a;
347
+ const contextOptionsLine = traceLines.find((trace) => trace.type === 'context-options');
348
+ return (_a = contextOptionsLine === null || contextOptionsLine === void 0 ? void 0 : contextOptionsLine.title) !== null && _a !== void 0 ? _a : '';
349
+ }
350
+ async function getProjectInfo(args) {
351
+ var _a, _b, _c;
352
+ const consoleLines = [];
353
+ const commandArgs = [
354
+ 'playwright',
355
+ 'test',
356
+ '--project',
357
+ args[constants_1.CommandArgProject],
358
+ '--grep',
359
+ 'mablImportInfo',
360
+ ];
361
+ const testFile = path_1.default.join((_a = args[constants_1.CommandArgPath]) !== null && _a !== void 0 ? _a : process.cwd(), (_b = args[constants_1.CommandArgTestPath]) !== null && _b !== void 0 ? _b : 'tests', 'mablImportInfo.spec.ts');
362
+ fs_1.default.writeFileSync(testFile, projectInfoTestFile);
363
+ const options = {
364
+ cwd: (_c = args[constants_1.CommandArgPath]) !== null && _c !== void 0 ? _c : process.cwd(),
365
+ env: {
366
+ ...process.env,
367
+ },
368
+ };
369
+ const processClosedPromise = new RichPromise_1.default();
370
+ const playwrightProcess = spawn('npx', commandArgs, options);
371
+ playwrightProcess.stdout.on('data', (data) => {
372
+ consoleLines.push(data.toString());
373
+ if (data.toString().includes('@mabl project info') &&
374
+ processClosedPromise.isPending()) {
375
+ try {
376
+ const info = JSON.parse(data.toString().split('@mabl project info')[1]);
377
+ processClosedPromise.resolve(info);
378
+ }
379
+ catch {
380
+ }
381
+ }
382
+ });
383
+ playwrightProcess.on('close', (code) => {
384
+ if (code !== 0) {
385
+ processClosedPromise.reject(new Error(`Playwright process exited with code ${code}`));
386
+ console.log(consoleLines.join('\n'));
387
+ }
388
+ else {
389
+ processClosedPromise.resolve();
390
+ }
391
+ });
392
+ const projectInfo = await processClosedPromise;
393
+ fs_1.default.unlinkSync(testFile);
394
+ return projectInfo !== null && projectInfo !== void 0 ? projectInfo : { baseUrl: '' };
395
+ }
396
+ async function runPlaywrightProject(args, tracesPath, cwd) {
397
+ const processClosedPromise = new RichPromise_1.default();
398
+ const commandArgs = ['playwright', 'test', '--trace', 'on'];
399
+ if (args[constants_1.CommandArgProject]) {
400
+ commandArgs.push('--project', args[constants_1.CommandArgProject]);
401
+ }
402
+ if (args.grep) {
403
+ commandArgs.push('--grep', args.grep);
404
+ }
405
+ if (args[constants_1.CommandArgExtraArguments]) {
406
+ commandArgs.push(args[constants_1.CommandArgExtraArguments]);
407
+ }
408
+ const options = {
409
+ cwd,
410
+ env: {
411
+ ...process.env,
412
+ },
413
+ };
414
+ loggingProvider_1.logger.info('Checking playwright project...');
415
+ const playwrightProjectFile = path_1.default.join(options.cwd, 'playwright.config.ts');
416
+ if (!fs_1.default.existsSync(playwrightProjectFile)) {
417
+ throw new Error('No playwright project found. Please ensure you are in the root directory of a playwright project.');
418
+ }
419
+ loggingProvider_1.logger.info('Deleting existing traces...');
420
+ if (fs_1.default.existsSync(tracesPath)) {
421
+ fs_1.default.rmSync(tracesPath, { recursive: true });
422
+ }
423
+ loggingProvider_1.logger.info('Importing tests from Playwright...');
424
+ loggingProvider_1.logger.info('This may take a few minutes.');
425
+ const playwrightProcess = spawn('npx', commandArgs, options);
426
+ playwrightProcess.stdout.on('data', (data) => {
427
+ loggingProvider_1.logger.info(data.toString());
428
+ });
429
+ playwrightProcess.on('close', (code) => {
430
+ if (code !== 0) {
431
+ processClosedPromise.reject(new Error(`Playwright process exited with code ${code}`));
432
+ }
433
+ else {
434
+ processClosedPromise.resolve();
435
+ }
436
+ });
437
+ await processClosedPromise;
438
+ loggingProvider_1.logger.info('Test import complete. Processing output');
439
+ }