Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to upload app to app-automate. #3573

Merged
merged 9 commits into from Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
42 changes: 40 additions & 2 deletions lib/http/request.js
Expand Up @@ -4,6 +4,8 @@ const https = require('https');
const dns = require('dns');
const HttpUtil = require('./http.js');
const HttpOptions = require('./options.js');
const fs = require('fs');
const path = require('path');
const Auth = require('./auth.js');
const Formatter = require('./formatter.js');
const HttpResponse = require('./response.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,33 @@ 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
if (fieldValue.filePath) {
bufferArray.push(fs.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
66 changes: 65 additions & 1 deletion 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 @@ -15,7 +19,67 @@ class AppAutomate extends BrowserStack {
return this.desiredCapabilities;
}

createDriver({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(`Uploading app to BrowserStack from '${appUploadPath || appUploadUrl}'...`);
garg3133 marked this conversation as resolved.
Show resolved Hide resolved

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

if (response.error) {
Logger.error(response.error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a more descriptive error message here, like "Uploading the app the BrowSerstack has failed because of: ...". And we can also add some help text and a link for more info (if applicable), using the actionable error messages framework. Same goes for the error logging in the catch() block

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the url in the screenshot contains undefined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0a4ba71. We should only show the build URL if a session with BrowserStack was successfully created.


return;
}

// eslint-disable-next-line no-console
console.log(Logger.colors.green(Utils.symbols.ok), 'App uploaded successfully:', response, '\n');

if (response.app_url && !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) {
Logger.error(err);
}
}

async createDriver({options}) {
if (options && (options.appUploadUrl || options.appUploadPath)) {
await this.uploadAppToBrowserStack(options);
}

return this.createAppiumDriver({options});
}
}
Expand Down
8 changes: 8 additions & 0 deletions lib/transport/selenium-webdriver/browserstack/browserstack.js
Expand Up @@ -144,6 +144,14 @@ class Browserstack extends AppiumBaseServer {
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;

return true;
Expand Down