Skip to content

Commit e9d34a3

Browse files
committed
many improvements
1 parent fb2b613 commit e9d34a3

File tree

7 files changed

+110
-43
lines changed

7 files changed

+110
-43
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ You can easily install the CLI via `npm` or `yarn`:
2121
$ npm install -g testinbot-cypress-cli
2222
```
2323

24+
To use the CLI in combination with [TestingBot Tunnel](https://testingbot.com/support/other/tunnel), you will need to have JDK8 (or higher) installed.
25+
TestingBot Tunnel is used to connect TestingBot's browser grid with your local computer or network.
26+
2427
### Configure
2528

2629
Once the CLI is installed, you'll need to point it to a configuration file.
@@ -50,6 +53,14 @@ Once you've started this command, the tests will start to appear in the [Testing
5053

5154
## Documentation
5255

56+
### Environment variables
57+
58+
If you prefer to keep your TestingBot credentials in environment variables, you can use `TESTINGBOT_KEY` and `TESTINGBOT_SECRET` environment variables.
59+
60+
If you are running this CLI in a CI/CD like Jenkins or TeamCity, you can set the
61+
environment variable `TESTINGBOT_CI=1`. The CLI will output the `TestingBotSessionID`, in combination
62+
with a TestingBot CI plugin you will be able to view the test's details from inside your CI.
63+
5364
## Getting Help
5465

5566
If you need help, please reach out to us via [email protected] or our public Slack: https://testingbot.com/support

src/commands/run.ts

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import log from '../log';
2+
import request from 'request';
23
import Archiver from '../utils/archiver';
34
import Uploader from '../utils/uploader';
45
import Poller, { IRun, ITest } from '../utils/poller';
@@ -7,20 +8,25 @@ import Tunnel from '../utils/tunnel';
78
import ora from 'ora';
89
import chalk from 'chalk';
910
import io from 'socket.io-client';
10-
import { runInThisContext } from 'vm';
1111

1212
interface Arguments {
1313
[x: string]: unknown;
1414
cf: string | boolean;
1515
}
1616

17+
interface ISocketData {
18+
id: number
19+
payload: string
20+
}
21+
1722
export default class RunProject {
1823
private archiver: Archiver | undefined = undefined;
1924
private uploader: Uploader | undefined = undefined;
2025
private poller: Poller | undefined = undefined;
2126
private tunnel: Tunnel | undefined = undefined;
2227
private configFilePath: string | undefined = undefined;
2328
private config: IConfig | undefined = undefined;
29+
private projectId: number | undefined = undefined;
2430

2531
constructor(argv: Arguments) {
2632
if (typeof(argv.cf) === 'string') {
@@ -29,20 +35,52 @@ export default class RunProject {
2935
}
3036

3137
public exitHandler(): void {
38+
this.stopJob();
3239
if (this.config && this.config.run_settings.start_tunnel) {
3340
if (this.tunnel) {
3441
this.tunnel.stop().then(() => {
3542
process.exit();
36-
}).catch(console.error);
43+
}).catch(log.error);
3744
this.tunnel = undefined;
3845
}
3946
} else {
4047
process.exit()
4148
}
4249
}
4350

44-
public errorHandler(): void {
45-
console.error(chalk.white.bgRed.bold(`A fatal error occurred, please report this to testingbot.com`));
51+
private async stopJob(): Promise<boolean> {
52+
if (!this.config) {
53+
return false;
54+
}
55+
56+
if (!this.projectId) {
57+
return false;
58+
}
59+
60+
return new Promise((resolve, reject) => {
61+
const requestOptions = {
62+
method: 'POST',
63+
uri: `https://api.testingbot.com/v1/cypress/${this.projectId}/stop_project`,
64+
auth: {
65+
user: this.config ? this.config.auth.key : '',
66+
pass: this.config ? this.config.auth.secret : '',
67+
sendImmediately: true,
68+
}
69+
};
70+
71+
request(requestOptions, (error) => {
72+
if (error) {
73+
return reject(error);
74+
}
75+
76+
resolve(true);
77+
});
78+
});
79+
}
80+
81+
public errorHandler(err: Error): void {
82+
log.error(chalk.white.bgRed.bold(`A fatal error occurred, please report this to testingbot.com`));
83+
log.error(err);
4684
this.exitHandler();
4785
}
4886

@@ -52,30 +90,30 @@ export default class RunProject {
5290
process.on('SIGINT', this.exitHandler.bind(this, {exit:true}));
5391
process.on('SIGUSR1', this.exitHandler.bind(this, {exit:true}));
5492
process.on('SIGUSR2', this.exitHandler.bind(this, {exit:true}));
55-
process.on('uncaughtException', this.errorHandler.bind(this, {exit:true}));
93+
process.on('uncaughtException', this.errorHandler.bind(this));
5694
}
5795

5896
private realTimeMessage(message: string): void {
59-
console.log(message);
97+
const data: ISocketData = JSON.parse(message);
98+
log.info(data.payload);
6099
}
61100

62101
private realTimeError(message: string): void {
63-
console.error(message);
102+
const data: ISocketData = JSON.parse(message);
103+
log.error(data.payload);
64104
}
65105

66-
private parseErrors(runs: IRun[]): string[] {
67-
let errors: string[] = []
106+
private parseSuccess(runs: IRun[]): boolean {
107+
let success = true;
68108
for (let i = 0; i < runs.length; i++) {
69-
if (runs[i].errors.length > 0) {
70-
errors = errors.concat(runs[i].errors);
71-
}
109+
success = success && runs[i].success;
72110
}
73111

74-
return errors;
112+
return success;
75113
}
76114

77115
private parseTestCases(runs: IRun[]): ITest[] {
78-
let testCases: ITest[] = []
116+
const testCases: ITest[] = []
79117
for (let i = 0; i < runs.length; i++) {
80118
const testCase = runs[i].test;
81119
if (testCase) {
@@ -91,14 +129,14 @@ export default class RunProject {
91129
try {
92130
config = await Config.getConfig(this.configFilePath || `testingbot.json`);
93131
} catch (e) {
94-
console.error(chalk.white.bgRed.bold(`Configuration file problem: ${e.message} for Config File: ${this.configFilePath || `testingbot.json`}`));
132+
log.error(chalk.white.bgRed.bold(`Configuration file problem: ${e.message} for Config File: ${this.configFilePath || `testingbot.json`}`));
95133
return;
96134
}
97135

98136
const configValidationErrors = Config.validate(config);
99137

100138
if (configValidationErrors.length > 0) {
101-
console.error(chalk.white.bgRed.bold(`Configuration errors: ${configValidationErrors.join('\n')}`));
139+
log.error(chalk.white.bgRed.bold(`Configuration errors: ${configValidationErrors.join('\n')}`));
102140
return;
103141
}
104142

@@ -112,7 +150,7 @@ export default class RunProject {
112150
let zipFile: string;
113151

114152
if (!this.archiver || !this.uploader) {
115-
console.error(chalk.white.bgRed.bold(`Invalid state, please try again`));
153+
log.error(chalk.white.bgRed.bold(`Invalid state, please try again`));
116154
return;
117155
}
118156

@@ -122,31 +160,31 @@ export default class RunProject {
122160
tunnelSpinner.succeed('TestingBot Tunnel Ready');
123161
}
124162

125-
const uploadSpinner = ora('Starting Project on TestingBot').start();
163+
const uploadSpinner = ora('Starting Cypress Project on TestingBot').start();
126164
try {
127165
zipFile = await this.archiver.start();
128166
} catch (err) {
129-
console.error(err);
167+
log.error(err);
130168
return;
131169
}
132170

133171
this.registerCloseHandlers();
134172

135173
try {
136174
const response = await this.uploader.start(zipFile);
137-
uploadSpinner.succeed('Cypress is now running on TestingBot')
138-
const realTime = io.connect('https://hub.testingbot.com:3031');
139-
realTime.emit('join', `cypress_${response.id}`)
140-
realTime.on('cypress_data', (msg: string) => this.realTimeMessage.bind(this, msg));
141-
realTime.on('cypress_error', (msg: string) => this.realTimeError.bind(this, msg));
175+
this.projectId = response.id;
176+
uploadSpinner.succeed('Cypress Project is now running on TestingBot');
177+
178+
if (config.run_settings.realTimeLogs) {
179+
const realTime = io.connect('https://hub.testingbot.com:3031');
180+
realTime.emit('join', `cypress_${response.id}`)
181+
realTime.on('cypress_data', this.realTimeMessage.bind(this));
182+
realTime.on('cypress_error', this.realTimeError.bind(this));
183+
}
142184

143185
const poller = await this.poller.check(response.id, uploadSpinner);
144186
const testCases = this.parseTestCases(poller.runs);
145-
const errors = this.parseErrors(poller.runs);
146-
147-
if (errors.length > 0) {
148-
console.error(chalk.white.bgRed.bold(`Errors occurred:` + errors.join(`\n`)));
149-
}
187+
const success = this.parseSuccess(poller.runs);
150188

151189
if (process.env.TESTINGBOT_CI && testCases.length > 0) {
152190
for (let i = 0; i < testCases.length; i++) {
@@ -155,10 +193,10 @@ export default class RunProject {
155193
}
156194
}
157195

158-
process.exit(errors.length === 0 ? 0 : 1);
196+
process.exit(success === true ? 0 : 1);
159197

160198
} catch (err) {
161-
console.error(chalk.white.bgRed.bold(err));
199+
log.error(chalk.white.bgRed.bold(err));
162200
if (config.run_settings.start_tunnel) {
163201
await this.tunnel.stop();
164202
}

src/log.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import tracer from 'tracer';
22

3-
const logger = tracer.colorConsole({
3+
const logger = tracer.console({
44
level: 'info',
5-
format: '{{timestamp}} {{file}}:{{line}} {{title}}: {{message}}',
6-
dateformat: 'HH:MM:ss.L',
5+
format: '{{message}}',
76
});
87

98
export default logger;

src/templates/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export default (): string => {
1818
npm_dependencies: {},
1919
package_config_options: {},
2020
start_tunnel: true,
21-
local_ports: []
21+
local_ports: [],
22+
realTimeLogs: true
2223
},
2324
tunnel_settings: {
2425
verbose: false

src/utils/archiver.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,23 @@ export default class Archiver {
5656
'png',
5757
'zip'
5858
];
59+
60+
let ignoredPaths = [
61+
'**/node_modules/**',
62+
'./node_modules/**',
63+
'package-lock.json',
64+
'package.json',
65+
'testingbot-package.json',
66+
];
67+
68+
if (this.config.run_settings.exclude && this.config.run_settings.exclude.length > 0) {
69+
ignoredPaths = ignoredPaths.concat(this.config.run_settings.exclude)
70+
}
5971
allowedFileTypes.forEach((fileType) => {
6072
archive.glob(`**/*.${fileType}`, {
6173
cwd: this.config.run_settings.cypress_project_dir,
6274
matchBase: true,
63-
ignore: [
64-
'**/node_modules/**',
65-
'./node_modules/**',
66-
'package-lock.json',
67-
'package.json',
68-
'testingbot-package.json',
69-
],
75+
ignore: ignoredPaths,
7076
});
7177
});
7278

src/utils/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ interface IRunSettings {
2222
package_config_options: any
2323
start_tunnel: boolean
2424
local_ports: number[]
25+
exclude: string[]
26+
realTimeLogs: boolean
2527
}
2628

2729
export interface IConfig {

src/utils/poller.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import request from 'request';
2-
import log from './../log';
32
import { IConfig } from './config';
3+
import log from '../log';
44
import ora from 'ora';
55

66
export interface ITest {
@@ -19,6 +19,7 @@ export interface IRun {
1919
capabilities: any
2020
errors: string[]
2121
test?: ITest
22+
success: boolean
2223
}
2324

2425
interface IPollResponse {
@@ -33,6 +34,7 @@ export default class Poller {
3334
private intervalId: NodeJS.Timeout | undefined;
3435
private static readonly MAX_RETRIES_WAITING = 60;
3536
private static readonly MAX_RETRIES_READY = 600;
37+
private initSuccess = false;
3638

3739
constructor(config: IConfig) {
3840
this.config = config;
@@ -75,6 +77,14 @@ export default class Poller {
7577
this.intervalId = undefined;
7678
}
7779
return reject(new Error(`This project has been running for over 20 minutes, stopping now.`));
80+
} else if (status === IStatus.READY && !this.initSuccess) {
81+
this.initSuccess = true;
82+
for (let i = 0; i < response.runs.length; i++) {
83+
const testCase = response.runs[i].test;
84+
if (testCase) {
85+
log.info(`Testcase started, view live stream https://testingbot.com/members/tests/${testCase.sessionId}`);
86+
}
87+
}
7888
}
7989

8090
this.retryNumber += 1;

0 commit comments

Comments
 (0)