@testrevolution/bugbug-cli 12.34.1 → 13.5.3

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/.env CHANGED
@@ -1 +1 @@
1
- SENTRY_DSN=https://33fb182a5d1a45f783ee5362a20f219a@o346063.ingest.sentry.io/6360873
1
+ SENTRY_DSN=https://c850ce209b19d8558600d0ecbb5a160f@sentry.bugbug.io/52
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testrevolution/bugbug-cli",
3
3
  "description": "BugBug CLI",
4
- "version": "12.34.1",
4
+ "version": "13.5.3",
5
5
  "keywords": [
6
6
  "automation",
7
7
  "cli",
@@ -19,16 +19,17 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@sentry/node": "^10.5.0",
22
- "axios": "^1.10.0",
23
- "console-table-printer": "^2.12.1",
24
- "dotenv": "^16.4.7",
22
+ "axios": "^1.13.4",
23
+ "chalk": "^4.1.2",
24
+ "console-table-printer": "^2.15.0",
25
+ "dotenv": "^17.2.3",
25
26
  "jest-junit": "^16.0.0",
26
27
  "junit-xml": "^1.2.0",
27
28
  "minimist": "^1.2.8",
28
- "nanoid": "^3.3.4",
29
+ "nanoid": "^3.3.11",
29
30
  "ora": "^5.4.1",
30
31
  "path": "^0.12.7",
31
- "validator": "^13.15.0"
32
+ "validator": "^13.15.26"
32
33
  },
33
34
  "scripts": {
34
35
  "start": "cross-env NODE_ENV=development node ./bin/bugbug",
@@ -49,9 +50,9 @@
49
50
  "eslint": "^8.57.1",
50
51
  "eslint-config-airbnb-base": "^15.0.0",
51
52
  "eslint-plugin-import": "^2.32.0",
52
- "eslint-plugin-jest": "^29.0.1",
53
+ "eslint-plugin-jest": "^29.12.2",
53
54
  "eslint-plugin-node": "^11.1.0",
54
- "jest": "^30.0.3"
55
+ "jest": "^30.2.0"
55
56
  },
56
57
  "author": "TestRevolution sp. z o.o."
57
58
  }
@@ -1,5 +1,10 @@
1
1
  const axios = require('axios');
2
+ const fs = require('fs');
3
+ const chalk = require('chalk');
2
4
 
5
+ chalk.level = 0;
6
+
7
+ const { join } = require('path');
3
8
  const {
4
9
  config: configCommand,
5
10
  help: helpCommand,
@@ -36,7 +41,7 @@ describe('commands module', () => {
36
41
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
37
42
  await helpCommand(args);
38
43
  expect(consoleLogSpy).toHaveBeenCalled();
39
- expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('bugbug [command] <options>'));
44
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('bugbug <command> [options]'));
40
45
  });
41
46
 
42
47
  it('should console log config help menu', async () => {
@@ -46,7 +51,7 @@ describe('commands module', () => {
46
51
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
47
52
  await helpCommand(args);
48
53
  expect(consoleLogSpy).toHaveBeenCalled();
49
- expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('bugbug config <options>'));
54
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('config <command>'));
50
55
  });
51
56
 
52
57
  it('should console log remote help menu', async () => {
@@ -56,7 +61,7 @@ describe('commands module', () => {
56
61
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
57
62
  await helpCommand(args);
58
63
  expect(consoleLogSpy).toHaveBeenCalled();
59
- expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('bugbug remote <options>'));
64
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('remote <command> [options]'));
60
65
  });
61
66
  });
62
67
 
@@ -297,7 +302,10 @@ describe('commands module', () => {
297
302
  variable: ['var=val', 'var2=val2'],
298
303
  };
299
304
  await remoteCommand(args);
300
- expect(runFunMock).toHaveBeenCalledWith(id, undefined, [{ key: 'var', value: 'val' }, { key: 'var2', value: 'val2' }], {
305
+ expect(runFunMock).toHaveBeenCalledWith(id, undefined, [{ key: 'var', value: 'val' }, {
306
+ key: 'var2',
307
+ value: 'val2',
308
+ }], {
301
309
  noprogress: true,
302
310
  nowait: false,
303
311
  withDetails: false,
@@ -421,7 +429,10 @@ describe('commands module', () => {
421
429
  variable: ['var=val', 'var2=val2'],
422
430
  };
423
431
  await remoteCommand(args);
424
- expect(runFunMock).toHaveBeenCalledWith(id, undefined, [{ key: 'var', value: 'val' }, { key: 'var2', value: 'val2' }], {
432
+ expect(runFunMock).toHaveBeenCalledWith(id, undefined, [{ key: 'var', value: 'val' }, {
433
+ key: 'var2',
434
+ value: 'val2',
435
+ }], {
425
436
  noprogress: true,
426
437
  nowait: false,
427
438
  withDetails: false,
@@ -581,6 +592,31 @@ describe('commands module', () => {
581
592
  });
582
593
  });
583
594
 
595
+ it.each(['test', 'suite'])('result %s command should call API and generate JUnit report', async (type) => {
596
+ // Arrange
597
+ const reportFile = 'report data';
598
+ jest.spyOn(fs, 'writeFileSync').mockImplementation();
599
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
600
+ axios
601
+ .mockResolvedValueOnce(apiMocks.getResultTestRunSuccess)
602
+ .mockResolvedValueOnce({ data: reportFile });
603
+ const id = '95a37d6a-a7a5-42ae-8fe3-336a120b807b';
604
+ const args = {
605
+ _: ['remote', 'result', type, id],
606
+ noprogress: true,
607
+ 'with-details': true,
608
+ reporter: settings.REPORTER_TYPE.junit,
609
+ 'output-path': 'test-report.xml',
610
+ };
611
+
612
+ // Act
613
+ await remoteCommand(args);
614
+
615
+ // Assert
616
+ expect(fs.writeFileSync).toHaveBeenCalledWith(join(process.cwd(), 'test-report.xml'), reportFile, expect.any(Object));
617
+ expect(consoleLogSpy).toHaveBeenCalledWith(`JUnit report generated at ${join(process.cwd(), 'test-report.xml')}`);
618
+ });
619
+
584
620
  it('result test command should call API and print errorCode when failed', async () => {
585
621
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
586
622
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
@@ -789,7 +825,7 @@ describe('commands module', () => {
789
825
  test_id: id, profile_name: undefined, variables: [], triggeredBy: 'cli',
790
826
 
791
827
  },
792
- params: { },
828
+ params: {},
793
829
  headers: { Authorization: `Token ${configValue.token}`, 'User-Agent': settings.USER_AGENT },
794
830
  });
795
831
  expect(axios).toHaveBeenNthCalledWith(2, {
@@ -27,7 +27,7 @@ const validateArgs = async (args) => {
27
27
  help({ _: ['config'] });
28
28
  return false;
29
29
  }
30
- const knownKeys = ['_'];
30
+ const knownKeys = ['_', 'color', 'no-color'];
31
31
  const unknownKeys = await getUnknownOptions(args, knownKeys);
32
32
  if (unknownKeys.length > 0) {
33
33
  console.error(`Unknown options: ${unknownKeys.join(', ')}`);
@@ -43,6 +43,10 @@ module.exports = async (args) => {
43
43
  }
44
44
  const { subCmd, token } = await parseArgs(args);
45
45
  switch (subCmd) {
46
+ case settings.ACTION_HELP:
47
+ help({ _: ['config'] });
48
+ exitCode = await getExitCode(true);
49
+ break;
46
50
  case settings.ACTION_SET_TOKEN:
47
51
  exitCode = await setToken(token);
48
52
  break;
@@ -1,40 +1,58 @@
1
+ const chalk = require('chalk');
1
2
  const settings = require('../settings');
2
3
 
3
4
  const help = {
4
5
  main: `
5
- bugbug [command] <options>
6
+ ${chalk.bold('Usage:')}
7
+ ${chalk.green('bugbug')} <command> [options]
6
8
 
7
- config ................
8
- remote ................
9
- version ...............
10
- help <config|remote>... show help menu for a command`,
9
+ ${chalk.bold('Commands:')}
10
+ ${chalk.cyan('config')} Configure CLI settings
11
+ ${chalk.cyan('remote')} Run and manage tests on remote
12
+ ${chalk.cyan('version')} Show version information
13
+ ${chalk.cyan('help <command>')} Show help menu for a command
14
+
15
+ ${chalk.bold('Global flags:')}
16
+ ${chalk.yellow('--debug')} Show raw API responses
17
+ ${chalk.yellow('--no-color')} Disable colored output`,
11
18
 
12
19
  config: `
13
- bugbug config <options>
20
+ ${chalk.bold('Usage:')}
21
+ ${chalk.green('bugbug config')} <command>
14
22
 
15
- options:
16
- * set-token <token>`,
23
+ ${chalk.bold('Commands:')}
24
+ ${chalk.cyan('set-token <token>')} Set BugBug API token`,
17
25
 
18
26
  remote: `
19
- bugbug remote <options>
20
-
21
- options:
22
- * list [test|suite|profile] [--no-wait] [--no-progress] [--debug]
23
- * run [test|suite] <string:testId|suiteId> [--no-wait] [--no-progress] [--debug] [--with-details] [--profile] [--variable] [--result-timeout <int>] [--reporter] [--output-path]
24
- * status [test|suite] <string:testRunId|suiteRunId> [--no-progress] [--debug]
25
- * result [test|suite] <string:testRunId|suiteRunId> [--no-progress] [--debug] [--with-details] [--result-timeout <int>]
26
- * stop [test|suite] <string:testRunId|suiteRunId> [--no-progress] [--debug] [--result-timeout <int>]
27
-
28
- optional flags:
29
- * --debug - show more data (like raw API response)
30
- * --no-progress - don't show progress spinner
31
- * --no-wait - exit immediately, don't wait for result
32
- * --profile <string:"profile name"> - run with specific profile
33
- * --variable <string:"varName=varValue"> - override variable during single run
34
- * --with-details - show result with details
35
- * --reporter <"inline"|"junit"> - the name of the reporter to use (default: "inline")
36
- * --output-path - the path to save the test report; relative to the current working directory
37
- * --result-timeout - modify the default result waiting time (minutes, default: 60)
27
+ ${chalk.bold('Usage:')}
28
+ ${chalk.green('bugbug remote')} <command> [options]
29
+
30
+ ${chalk.bold('Commands:')}
31
+ ${chalk.cyan('list')} <test|suite|profile> List tests, suites or profiles
32
+ ${chalk.cyan('run')} <test|suite> <id> Run a test or suite
33
+ ${chalk.cyan('status')} <test|suite> <id> Check run status
34
+ ${chalk.cyan('result')} <test|suite> <id> Get run result
35
+ ${chalk.cyan('stop')} <test|suite> <id> Stop a running test or suite
36
+
37
+
38
+ ${chalk.bold('Global flags:')}
39
+ ${chalk.yellow('--debug')} Show raw API responses
40
+ ${chalk.yellow('--no-progress')} Hide progress spinner
41
+ ${chalk.yellow('--no-color')} Disable colored output
42
+
43
+ ${chalk.bold('Command-specific flags:')}
44
+ ${chalk.white.underline('run, status')}
45
+ ${chalk.yellow('--no-wait')} Exit immediately, don't wait for result
46
+ ${chalk.yellow('--result-timeout <min>')} Max waiting time (default: 60)
47
+
48
+ ${chalk.white.underline('run')}
49
+ ${chalk.yellow('--profile <name>')} Run with specific profile
50
+ ${chalk.yellow('--variable <key=val>')} Override variable (can be used multiple times)
51
+
52
+ ${chalk.white.underline('run, status, result')}
53
+ ${chalk.yellow('--with-details')} Include detailed step information
54
+ ${chalk.yellow('--reporter <type>')} "inline" or "junit" (default: "inline")
55
+ ${chalk.yellow('--output-path <path>')} Path for JUnit report (default: test-report.xml)
38
56
  `,
39
57
  };
40
58
 
@@ -1,3 +1,5 @@
1
+ const fs = require('node:fs');
2
+ const { join } = require('node:path');
1
3
  const { format } = require('util');
2
4
  const validator = require('validator');
3
5
 
@@ -14,9 +16,6 @@ const {
14
16
  printTestRunInfo,
15
17
  printSuiteRunInfo,
16
18
  } = require('../utils/print');
17
- const {
18
- generateJunitReport,
19
- } = require('../utils/testReports');
20
19
  const help = require('./help');
21
20
  const { sentry } = require('../utils/sentry');
22
21
 
@@ -54,8 +53,16 @@ const getResult = async (type, id, extraParams) => {
54
53
  if (result) {
55
54
  switch (reporter) {
56
55
  case settings.REPORTER_TYPE.junit: {
57
- console.log('Generating JUnit report...');
58
- await generateJunitReport(type, result.data, outputPath);
56
+ spinner.info('Generating JUnit report...');
57
+ const report = await apiCall(
58
+ format(settings.API_ROUTING[`${type}RunReport`].path, id),
59
+ settings.API_ROUTING[`${type}RunReport`].method,
60
+ {},
61
+ {},
62
+ extraParams.triggeredBy,
63
+ );
64
+ fs.writeFileSync(join(process.cwd(), outputPath), report.data, { flag: 'w+' });
65
+ spinner.info(`JUnit report generated at ${join(process.cwd(), outputPath)}`);
59
66
  break;
60
67
  }
61
68
  default: {
@@ -185,8 +192,8 @@ const checkTokenConfig = async () => {
185
192
  };
186
193
 
187
194
  const parseArgs = async (args) => {
188
- const subCmd = args._[1].toLowerCase();
189
- const objectType = args._[2].toLowerCase();
195
+ const subCmd = (args._[1] || '').toLowerCase();
196
+ const objectType = (args._[2] || '').toLowerCase();
190
197
  const id = args._[3];
191
198
  const variables = await parseVariables(args.variable);
192
199
 
@@ -208,19 +215,24 @@ const parseArgs = async (args) => {
208
215
  };
209
216
 
210
217
  const validateArgs = async (args) => {
218
+ const subCmd = (args._[1] || '').toLowerCase();
219
+ if (subCmd === settings.ACTION_HELP) {
220
+ return true;
221
+ }
222
+
211
223
  if (args._.length < 3) {
212
224
  console.error('Missing parameters!');
213
225
  help({ _: ['remote'] });
214
226
  return false;
215
227
  }
216
228
 
217
- const knownKeys = ['_', 'wait', 'nowait', 'noprogress', 'progress', 'with-details', 'debug', 'profile', 'variable', 'result-timeout', 'reporter', 'output-path', 'triggered-by'];
229
+ const knownKeys = ['_', 'wait', 'nowait', 'noprogress', 'progress', 'with-details', 'debug', 'profile', 'variable', 'result-timeout', 'reporter', 'output-path', 'triggered-by', 'color', 'no-color'];
218
230
  const unknownKeys = await getUnknownOptions(args, knownKeys);
219
231
  if (unknownKeys.length > 0) {
220
232
  console.error(`Unknown options: ${unknownKeys.join(', ')}`);
221
233
  return false;
222
234
  }
223
- const { subCmd, objectType, id } = await parseArgs(args);
235
+ const { objectType, id } = await parseArgs(args);
224
236
 
225
237
  if (!OBJECT_TYPES.includes(objectType)) {
226
238
  console.error(`Incorrect object type ${objectType}. Available types: ${OBJECT_TYPES}.`);
@@ -260,6 +272,10 @@ module.exports = async (args) => {
260
272
  let exitCode;
261
273
  await checkTokenConfig();
262
274
  switch (subCmd) {
275
+ case settings.ACTION_HELP:
276
+ help({ _: ['remote'] });
277
+ exitCode = await getExitCode(true);
278
+ break;
263
279
  case settings.ACTION_LIST:
264
280
  exitCode = await getList(objectType, id, extraParams);
265
281
  break;
package/src/index.js CHANGED
@@ -3,9 +3,10 @@ const minimist = require('minimist');
3
3
  const { nanoid } = require('nanoid');
4
4
  const { sentry, init: initSentry } = require('./utils/sentry');
5
5
  const settings = require('./settings');
6
- require('dotenv').config();
6
+ require('dotenv').config({ quiet: true });
7
7
  require('dotenv').config({
8
8
  path: path.resolve(process.cwd(), `.env.${settings.NODE_ENV}`),
9
+ quiet: true,
9
10
  });
10
11
  // Initialize Sentry first
11
12
  initSentry();
package/src/settings.js CHANGED
@@ -9,7 +9,7 @@ util.inspect.styles.string = 'white';
9
9
  const NODE_ENV = process.env.NODE_ENV || 'production';
10
10
  const ENV_FILE_PATH = path.resolve(__dirname, '..', `.env.${NODE_ENV}`);
11
11
  const USER_AGENT = `BugBug CLI ${VERSION}`;
12
- dotenv.config({ path: ENV_FILE_PATH });
12
+ dotenv.config({ path: ENV_FILE_PATH, quiet: true });
13
13
 
14
14
  // TODO: Fix this path due to Mac OS restrictions
15
15
  const CONFIG_DIR_PATH = path.join(os.homedir(), '.bugbug');
@@ -42,6 +42,8 @@ const API_ROUTING = {
42
42
  testRun: { method: 'POST', path: `${API_VERSION}/testruns/` },
43
43
  testStatus: { method: 'GET', path: `${API_VERSION}/testruns/%s/status/` },
44
44
  testStop: { method: 'POST', path: `${API_VERSION}/testruns/%s/stop/` },
45
+ testRunReport: { method: 'GET', path: `${API_VERSION}/testruns/%s/report/junit/` },
46
+ suiteRunReport: { method: 'GET', path: `${API_VERSION}/suiteruns/%s/report/junit/` },
45
47
  };
46
48
 
47
49
  const API_POLLING_MODIFIED_INTERVAL = 120000; // 120 seconds
@@ -0,0 +1,37 @@
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <testsuites>
3
+ <testsuite errors="0" failures="0" id="08deed80-07d1-4987-aaa3-e14a1f906a29" name="Test_Tic Tac Toe simple [OK]" skipped="0" tests="1" time="7.00" sequence="#3" timestamp="2025-10-28T13:08:49.533959+00:00">
4
+ <properties>
5
+ <property name="report_id" value="ca971f23-d2e1-4401-bf12-5cd2e968ce3a" />
6
+ <property name="generated_at" value="2025-10-28T13:08:57.329443+00:00" />
7
+ <property name="generated_by" value="admin@testrevolution.io" />
8
+ <property name="auto_retry_attempt" value="0" />
9
+ <property name="browser_height" value="786" />
10
+ <property name="browser_name" value="Chrome" />
11
+ <property name="browser_version" value="136.0.0.0" />
12
+ <property name="browser_width" value="1366" />
13
+ <property name="extension_version" value="12.33.2" />
14
+ <property name="finished" value="2025-10-28T13:08:56.534381+00:00" />
15
+ <property name="os_name" value="Linux" />
16
+ <property name="profile_name" value="Default" />
17
+ <property name="run_mode" value="cloud" />
18
+ <property name="sequence" value="#3" />
19
+ <property name="screen_size_type" value="desktop" />
20
+ <property name="test_id" value="d998ade4-93bc-4d94-bab8-a73a290f8490" />
21
+ <property name="triggered_by" value="cli" />
22
+ <property name="report_format_version" value="1.0" />
23
+ </properties>
24
+ <testcase name="Tic Tac Toe simple [OK]" classname="TestSuite_Single" time="7.00" test_run_id="08deed80-07d1-4987-aaa3-e14a1f906a29" sequence="#3">
25
+ <system-out>=== Test Steps Execution (summary) ===
26
+ Total steps: 9
27
+ Passed: 9
28
+ Failed: 0
29
+
30
+ First 3 steps:
31
+ 1. OK goto (2.36s)
32
+ 2. OK click (1.22s)
33
+ 3. OK click (0.27s)
34
+ </system-out>
35
+ </testcase>
36
+ </testsuite>
37
+ </testsuites>
@@ -1,65 +0,0 @@
1
- const { getJunitXml } = require('junit-xml');
2
- const fs = require('node:fs');
3
- const path = require('node:path');
4
-
5
- const settings = require('../settings');
6
- const { getSecondsFromDuration } = require('./helper');
7
-
8
- const getTestInJunitFormat = (testRun) => {
9
- const errorCode = testRun.errorCode ?? 'MISSING_ERROR_CODE';
10
- const stepInfo = testRun.details?.[0]?.step;
11
- const errorDetails = stepInfo
12
- ? {
13
- message: `Error ${errorCode} occurred in step: ${stepInfo.type} (${stepInfo.id})`,
14
- stack: errorCode,
15
- }
16
- : {
17
- message: `Error ${errorCode} occurred`,
18
- stack: errorCode,
19
- };
20
- const errorsList = errorDetails ? [errorDetails] : [];
21
-
22
- return {
23
- id: testRun.id,
24
- name: testRun.name,
25
- time: getSecondsFromDuration(testRun.duration),
26
- classname: null,
27
- errors: testRun.status === settings.STATUS_ERROR ? errorsList : [],
28
- failures: testRun.status === settings.STATUS_FAILED ? errorsList : [],
29
- };
30
- };
31
-
32
- const getSuiteInJunitFormat = (suiteRun) => ({
33
- id: suiteRun.id,
34
- name: suiteRun.name,
35
- time: getSecondsFromDuration(suiteRun.duration),
36
- testCases: suiteRun.details.map((testRun) => getTestInJunitFormat(testRun)),
37
- });
38
-
39
- const generateJunitReport = async (type, result, outputPath) => {
40
- const testReport = {};
41
- testReport.name = 'BugBug Report';
42
- testReport.time = getSecondsFromDuration(result.duration);
43
- testReport.suites = [];
44
-
45
- if (type === settings.TYPE_TEST) {
46
- testReport.suites.push(
47
- getSuiteInJunitFormat({
48
- name: 'Single Test Run',
49
- duration: result.duration,
50
- details: [result],
51
- }),
52
- );
53
- }
54
-
55
- if (type === settings.TYPE_SUITE) {
56
- testReport.suites.push(getSuiteInJunitFormat(result));
57
- }
58
-
59
- const junitReport = getJunitXml(testReport);
60
- fs.writeFileSync(path.join(process.cwd(), outputPath), junitReport, { flag: 'w+' });
61
- };
62
-
63
- module.exports = {
64
- generateJunitReport,
65
- };