Skip to content

Commit

Permalink
Add support to upload app to Browserstack AppAutomate service. (night…
Browse files Browse the repository at this point in the history
  • Loading branch information
garg3133 authored and harshit-bs committed Mar 16, 2023
1 parent 0147a3a commit 5ef19c6
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 24 deletions.
3 changes: 2 additions & 1 deletion lib/http/http.js
@@ -1,6 +1,7 @@
const ContentTypes = {
JSON: 'application/json',
JSON_WITH_CHARSET: 'application/json; charset=utf-8'
JSON_WITH_CHARSET: 'application/json; charset=utf-8',
MULTIPART_FORM_DATA: 'multipart/form-data'
};

const Headers = {
Expand Down
43 changes: 41 additions & 2 deletions lib/http/request.js
Expand Up @@ -2,6 +2,8 @@ const EventEmitter = require('events');
const http = require('http');
const https = require('https');
const dns = require('dns');
const path = require('path');

const HttpUtil = require('./http.js');
const HttpOptions = require('./options.js');
const Auth = require('./auth.js');
Expand Down Expand Up @@ -88,8 +90,15 @@ class HttpRequest extends EventEmitter {
options.data = '';
}

if (options.multiPartFormData) {
this.multiPartFormData = options.multiPartFormData;
this.formBoundary = `----NightwatchFormBoundary${Math.random().toString(16).slice(2)}`;

this.setFormData(this.formBoundary);
} else {
this.setData(options);
}
this.params = options.data;
this.setData(options);
this.contentLength = this.data.length;
this.use_ssl = this.httpOpts.use_ssl || options.use_ssl;
this.reqOptions = this.createHttpOptions(options);
Expand Down Expand Up @@ -364,7 +373,9 @@ class HttpRequest extends EventEmitter {
this.reqOptions.headers[HttpUtil.Headers.ACCEPT] = HttpUtil.ContentTypes.JSON;
}

if (this.contentLength > 0) {
if (this.multiPartFormData) {
this.reqOptions.headers[HttpUtil.Headers.CONTENT_TYPE] = `${HttpUtil.ContentTypes.MULTIPART_FORM_DATA}; boundary=${this.formBoundary}`;
} else if (this.contentLength > 0) {
this.reqOptions.headers[HttpUtil.Headers.CONTENT_TYPE] = HttpUtil.ContentTypes.JSON_WITH_CHARSET;
}

Expand All @@ -391,6 +402,34 @@ class HttpRequest extends EventEmitter {
return this;
}

setFormData(boundary) {
const crlf = '\r\n';
const delimiter = `${crlf}--${boundary}`;
const closeDelimiter = `${delimiter}--`;

const bufferArray = [];
for (const [fieldName, fieldValue] of Object.entries(this.multiPartFormData)) {
// set header
let header = `Content-Disposition: form-data; name="${fieldName}"`;
if (fieldValue.filePath) {
const fileName = path.basename(fieldValue.filePath);
header += `; filename="${fileName}"`;
}
bufferArray.push(Buffer.from(delimiter + crlf + header + crlf + crlf));

// set data
const {readFileSync} = require('fs');
if (fieldValue.filePath) {
bufferArray.push(readFileSync(fieldValue.filePath));
} else {
bufferArray.push(Buffer.from(fieldValue.data));
}
}
bufferArray.push(Buffer.from(closeDelimiter));

this.data = Buffer.concat(bufferArray);
}

hasCredentials() {
return Utils.isObject(this.httpOpts.credentials) && this.httpOpts.credentials.username;
}
Expand Down
95 changes: 93 additions & 2 deletions lib/transport/selenium-webdriver/browserstack/appAutomate.js
@@ -1,5 +1,9 @@
const path = require('path');
const Utils = require('../../../utils');
const BrowserStack = require('./browserstack.js');

const {Logger} = Utils;

class AppAutomate extends BrowserStack {
get ApiUrl() {
return `https://api.browserstack.com/${this.productNamespace}`;
Expand All @@ -9,10 +13,97 @@ class AppAutomate extends BrowserStack {
return 'app-automate';
}

createSessionOptions() {
async createSessionOptions() {
this.extractAppiumOptions();

return this.desiredCapabilities;
const options = this.desiredCapabilities;
if (options && (options.appUploadUrl || options.appUploadPath)) {
await this.uploadAppToBrowserStack(options);
}

return options;
}

async uploadAppToBrowserStack(options) {
const {appUploadPath, appUploadUrl} = options;

const multiPartFormData = {};
if (appUploadPath) {
multiPartFormData['file'] = {
filePath: path.resolve(appUploadPath)
};
} else if (appUploadUrl) {
multiPartFormData['url'] = {
data: appUploadUrl
};
}

if (options['appium:app'] && !options['appium:app'].startsWith('bs://')) {
multiPartFormData['custom_id'] = {
data: options['appium:app']
};
}

// eslint-disable-next-line no-console
console.log(Logger.colors.stack_trace(`Uploading app to BrowserStack from '${appUploadPath || appUploadUrl}'...`));

try {
const response = await this.sendHttpRequest({
url: 'https://api-cloud.browserstack.com/app-automate/upload',
method: 'POST',
use_ssl: true,
port: 443,
auth: {
user: this.username,
pass: this.accessKey
},
multiPartFormData
});

if (response.error) {
const errMessage = 'App upload to BrowserStack failed. Original error: ' + response.error;

throw new Error(errMessage);
}

if (!response.app_url) {
const errMessage = 'App upload was unsuccessful. Got response: ' + response;

throw new Error(errMessage);
}

// eslint-disable-next-line no-console
console.log(Logger.colors.green(Utils.symbols.ok), Logger.colors.stack_trace('App upload successful!'), '\n');

if (!response.custom_id) {
// custom_id not being used
options['appium:app'] = response.app_url;

// to display url when test suite is finished
this.uploadedAppUrl = response.app_url;
}
} catch (err) {
err.help = [];

if (appUploadPath) {
err.help.push('Check if you have entered correct file path in \'appUploadPath\' desired capability.');
} else if (appUploadUrl) {
err.help.push('Check if you have entered correct publicly available file URL in \'appUploadUrl\' desired capability.');
}

if (err.message.includes('BROWSERSTACK_INVALID_CUSTOM_ID')) {
err.help.push('Check if \'appium:app\' or \'appium:options\' > app desired capability is correctly set to BrowserStack app url or required custom ID.');
}

err.help.push(
'See BrowserStack app-upload docs for more details: https://www.browserstack.com/docs/app-automate/api-reference/appium/apps#upload-an-app',
'More details on setting custom ID for app: https://www.browserstack.com/docs/app-automate/appium/upload-app-define-custom-id'
);

Logger.error(err);

throw err;
}
}

createDriver({options}) {
Expand Down
20 changes: 15 additions & 5 deletions lib/transport/selenium-webdriver/browserstack/browserstack.js
Expand Up @@ -138,11 +138,21 @@ class Browserstack extends AppiumBaseServer {

async testSuiteFinished(failures) {
try {
const reason = failures instanceof Error ? `${failures.name}: ${failures.message}` : '';
await this.sendReasonToBrowserstack(!!failures, reason);
// eslint-disable-next-line no-console
console.log('\n ' + 'See more info, video, & screenshots on Browserstack:\n' +
' ' + Logger.colors.light_cyan(`https://${this.productNamespace}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
if (this.sessionId) {
const reason = failures instanceof Error ? `${failures.name}: ${failures.message}` : '';
await this.sendReasonToBrowserstack(!!failures, reason);
// eslint-disable-next-line no-console
console.log('\n ' + 'See more info, video, & screenshots on Browserstack:\n' +
' ' + Logger.colors.light_cyan(`https://${this.productNamespace}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
}

if (this.uploadedAppUrl) {
// App was uploaded to BrowserStack and custom_id not being used
// eslint-disable-next-line no-console
console.log('\n ' + Logger.colors.light_cyan(
`Please set 'appium:app' capability to '${this.uploadedAppUrl}' to avoid uploading the app again in future runs.`
) + '\n');
}

this.sessionId = null;

Expand Down
2 changes: 1 addition & 1 deletion lib/transport/selenium-webdriver/index.js
Expand Up @@ -270,7 +270,7 @@ class Transport extends BaseTransport {
const startTime = new Date();
const {host, port, start_process} = this.settings.webdriver;
const portStr = port ? `port ${port}` : 'auto-generated port';
const options = this.createSessionOptions(argv);
const options = await this.createSessionOptions(argv);

if (start_process) {
if (this.usingSeleniumServer) {
Expand Down

0 comments on commit 5ef19c6

Please sign in to comment.