Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Karma BrowserStack reporting enhancements #170

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
656f6bc
Updated reporting for browserstack launcher to show more details on t…
samirans89 Feb 3, 2020
8f8a202
modified logging
samirans89 Feb 3, 2020
a9b71ff
updated readme
samirans89 Feb 4, 2020
cdc0335
removed session retries on timeout, removed update build name when in…
samirans89 Feb 4, 2020
29fab20
updated index file to remove unwanted logs
samirans89 Feb 4, 2020
ca020e9
Merge pull request #1 from samirans89/generic_repo_updates
samirans89 Feb 5, 2020
f9e49a8
reverted readme for raising PR with base browserstack karma launcher
samirans89 Feb 5, 2020
863118a
Merge branch 'master' of https://github.com/samirans89/karma-browsers…
samirans89 Feb 5, 2020
668d4bb
updated readme
samirans89 Feb 5, 2020
f346e8b
travis build #313 updates
samirans89 Feb 5, 2020
f960b5f
travis build #314 updates
samirans89 Feb 5, 2020
5cf3eec
addressed linter comments
samirans89 Feb 5, 2020
79ce978
Merge branch 'master' into correct_linter_checks
samirans89 Feb 5, 2020
ee41448
Merge pull request #2 from samirans89/correct_linter_checks
samirans89 Feb 5, 2020
0e989bd
linter updater
samirans89 Feb 5, 2020
6d35034
Merge pull request #3 from samirans89/correct_linter_checks
samirans89 Feb 5, 2020
a9d5e9a
boundary cases handling post testing karma-browserstack-example
samirans89 Feb 6, 2020
aa828c8
implemented grunt code quality checks for all js files
samirans89 Feb 7, 2020
4b44907
including base64 lib and adding console log
samirans89 Feb 10, 2020
df5cbf2
reporter console logs commented code removal
samirans89 Feb 10, 2020
ced1b8c
resolved grunt syntax checks
samirans89 Feb 10, 2020
48c0527
removed dependency for base64-js and used an existing node capability
samirans89 Feb 14, 2020
9e5f78c
temp log to check env variable
samirans89 Feb 17, 2020
8fed409
handled creds for jenkins
samirans89 Feb 17, 2020
57a6592
removed logs
samirans89 Feb 17, 2020
399db6e
Merge pull request #4 from samirans89/sample_test
samirans89 Feb 17, 2020
aab2340
common js buffer logic correction
samirans89 Feb 18, 2020
e4a949a
removed handling for - in username when run from jenkins, as it was n…
samirans89 Feb 19, 2020
efedb1a
karma runner base repo master merge
samirans89 Mar 31, 2020
18930f2
Merge pull request #6 from samirans89/karma-runner-master
samirans89 Mar 31, 2020
56fd028
deleting package-lock.json based on travis build failure suggestion: …
samirans89 Mar 31, 2020
5110653
Merge branch 'master' of https://github.com/samirans89/karma-browsers…
samirans89 Mar 31, 2020
9870253
updating yarn.lock and package-lock.json based on travis build failur…
samirans89 Mar 31, 2020
8da9e6e
test revert old package-lock json - same as master
samirans89 Mar 31, 2020
0ab37a2
corrected yarn errors with mkdirp locally
samirans89 Mar 31, 2020
e112aad
corrected yarn errors with mkdirp locally. forgot to add yarn.lock in…
samirans89 Mar 31, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,17 @@ module.exports = function(config) {

### Global options

- `username` your BS username, you can also use `BROWSERSTACK_USERNAME` env variable.
- `accessKey` your BS access key, you can also use `BROWSERSTACK_ACCESS_KEY` env variable.
- `username` your BS username, you can also use `BROWSER_STACK_USERNAME` env variable.
- `accessKey` your BS access key, you can also use `BROWSER_STACK_ACCESS_KEY` env variable.
- `startTunnel` do you wanna establish the BrowserStack tunnel ? (defaults to `true`)
- `tunnelIdentifier` in case you want to start the BrowserStack tunnel outside `karma` by setting `startTunnel` to `false`, set the identifier passed to the `-localIdentifier` option here (optional)
- `tunnelIdentifier`/`localIdentifier` in case you want to start the BrowserStack tunnel outside `karma` by setting `startTunnel` to `false`, set the identifier passed to the `-localIdentifier` option here (optional)
- `retryLimit` how many times do you want to retry to capture the browser ? (defaults to `3`)
- `captureTimeout` the browser capture timeout (defaults to `120`)
- `timeout` the BS worker timeout (defaults to `300`
- `build` the BS worker build name (optional)
- `name` the BS worker name (optional)
- `project` the BS worker project name (optional)
- `binaryBasePath` the BS binary base bath, you can also use `BROWSER_STACK_BINARY_BASE_PATH` env variable. This will override the default and set the base path to the BS local binary (optional)
- `proxyHost` the host of your proxy for communicating with BrowserStack REST API and BrowserStackLocal (optional)
- `proxyPort` the port of your proxy (optional)
- `proxyUser` the username used for authentication with your proxy (optional)
Expand All @@ -85,7 +86,7 @@ module.exports = function(config) {
- `name` the BS worker name (optional, defaults to global)
- `project` the BS worker project name (optional, defaults to global)

> **Note:** you can also pass through any additional options supported by browserstack. (EG. `url`, `resolution`, etc.)
> **Note:** you can also pass through any additional options supported by browserstack. (EG. `timezone`, `resolution`, etc.)
See https://www.browserstack.com/automate/capabilities for a full list of supported options.

### BrowserStack reporter
Expand Down
222 changes: 188 additions & 34 deletions browserstack-reporter.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,207 @@
var Browserstack = require('browserstack')
var common = require('./common')

var BrowserStackReporter = function (logger, /* BrowserStack:sessionMapping */ sessionMapping) {
var log = logger.create('reporter.browserlabs')
var BrowserStackReporter = function (baseReporterDecorator, logger, /* BrowserStack:sessionMapping */ sessionMapping, config) {
try {
var log = logger.create('reporter.browserstack')
var suiteResults = []
var buildId
var allMessages = []
baseReporterDecorator(this)
var callWhenFinished = function () {}

var pendingUpdates = 0
var callWhenFinished = function () {}

var exitIfAllFinished = function () {
if (pendingUpdates === 0) {
var exitIfAllFinished = function () {
callWhenFinished()
}
}

// We're only interested in the final results per browser
this.onBrowserComplete = function (browser) {
var result = browser.lastResult
this.onRunStart = function (browsers) {
// implement this function if you want to perform custom actions post the overall test batch run has started

}

this.onBrowserStart = function (browser) {
// implement this function if you want to perform custom actions post browser start
log.debug('Browser: ' + browser.name + ' has been started')
}

this.onBrowserError = function (browser, error) {
var browserId = browser.launchId || browser.id
if (browserId in sessionMapping) {
var existingObj = suiteResults.find(o => o.browserId === browserId)
if (typeof existingObj === 'undefined') {
suiteResults.push({
browserId: browserId,
records: [{
apiStatus: 'Error',
suite: 'N/A [Karma <-> Browser error]',
log: error
}]
})
} else {
existingObj.records.push({
apiStatus: 'Error',
suite: 'N/A [Karma <-> Browser error]',
log: error
})
}
}
}

if (result.disconnected) {
log.error('✖ Test Disconnected')
this.specSuccess = function (browser, result) {
try {
var browserId = browser.launchId || browser.id
if (browserId in sessionMapping) {
var existingObj = suiteResults.find(o => o.browserId === browserId)
if (typeof existingObj === 'undefined') {
suiteResults.push({
browserId: browserId,
records: [{
apiStatus: 'Passed',
suite: result.suite,
specs: [result.description]
}]
})
} else {
existingObj['records'][0]['specs'].push(result.description)
}
}
} catch (e) {
log.debug('Browserstack reporter module specSuccess function encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
}

if (result.error) {
log.error('✖ Test Errored')
this.specFailure = this.specSkipped = function (browser, result) {
try {
var browserId = browser.launchId || browser.id
log.debug('Spec Failure:' + JSON.stringify(result))
if (browserId in sessionMapping) {
var existingObj = suiteResults.find(o => o.browserId === browserId)
if (typeof existingObj === 'undefined') {
suiteResults.push({
browserId: browserId,
records: [{
apiStatus: 'Failed',
suite: result.suite,
log: result.log
}]
})
} else {
existingObj.records.push({
apiStatus: 'Failed',
suite: result.suite,
log: result.log
})
}
}
} catch (e) {
log.debug('Browserstack reporter module specFailure function encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
}

var browserId = browser.launchId || browser.id
if (browserId in sessionMapping) {
pendingUpdates++
var browserstackClient = Browserstack.createAutomateClient(sessionMapping.credentials)
var apiStatus = !(result.failed || result.error || result.disconnected) ? 'completed' : 'error'
this.onSpecComplete = function (browser, result) {
if (result.skipped) {
this.specSkipped(browser, result)
} else if (result.success) {
this.specSuccess(browser, result)
} else {
this.specFailure(browser, result)
}
log.debug('Suite: ' + result.suite + ', Spec completed: ' + result.description)
}

this.onRunComplete = function (browsersCollection, results) {
// implement this function if you want to perform custom actions post test run completion
try {
log.debug('Test run completed\n' + JSON.stringify(results))
var browserId = browsersCollection.browsers[0].id

browserstackClient.updateSession(sessionMapping[browserId], {
status: apiStatus
}, function (error) {
if (error) {
log.error('✖ Could not update BrowserStack status')
log.debug(error)
if (typeof buildId === 'undefined') {
var browserstackClient = Browserstack.createAutomateClient(sessionMapping.credentials)
common.getSessionDetails(log, browserstackClient, sessionMapping, browserId, config, function (sessionObj) {
var browserUrl = sessionObj['automation_session']['browser_url']
if (typeof browserUrl !== 'undefined') {
buildId = browserUrl.split('/').slice(-3)[0]
var originalBuildName = config.browserStack.build
if (typeof buildId !== 'undefined' || buildId !== undefined) {
common.updateBuildName(log, buildId, originalBuildName, results, config)
}
}
})
}
pendingUpdates--
exitIfAllFinished()
allMessages = []
} catch (e) {
log.debug('onRunComplete: Browserstack reporter module encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
}

var findSpecObjectRecords = function (browserId, specStatusSearchCondition) {
var suiteObj = suiteResults.find(o => o.browserId === browserId)
var specRecord
if (typeof suiteObj !== 'undefined') {
if (specStatusSearchCondition !== '') {
specRecord = suiteObj.records.find(o => o.apiStatus === specStatusSearchCondition)
}
}
return [suiteObj, specRecord]
}

var removeFromSuiteResults = function (objectListToRemove) {
suiteResults = suiteResults.filter(function (item) {
return item !== objectListToRemove
})
}
}

// Wait until all updates have been pushed to Browserstack
this.onExit = function (done) {
callWhenFinished = done
exitIfAllFinished()
this.adapters = [function (msg) {
allMessages.push(msg)
}]

this.onBrowserComplete = function (browser) {
try {
var browserId = browser.launchId || browser.id
var failedObjRecord = findSpecObjectRecords(browserId, 'Failed')
var errorObjRecord = findSpecObjectRecords(browserId, 'Error')
var browserstackClient = Browserstack.createAutomateClient(sessionMapping.credentials)
if (browserId in sessionMapping) {
if (typeof failedObjRecord[1] !== 'undefined') {
var reason = 'Suite: ' + failedObjRecord[1]['suite'] + ', Error log: ' + failedObjRecord[1]['log']
common.updateStatusSession(log, browserstackClient, sessionMapping, browserId, failedObjRecord[1].apiStatus, reason, failedObjRecord[1]['suite'])
removeFromSuiteResults(failedObjRecord[0])
} else if (typeof errorObjRecord[1] === 'undefined') {
var passedObjRecord = findSpecObjectRecords(browserId, 'Passed')
if (typeof passedObjRecord[1] !== 'undefined') {
reason = 'Suite: ' + passedObjRecord[1]['suite'] + ', Specs run: ' + JSON.stringify(passedObjRecord[1]['specs'])
common.updateStatusSession(log, browserstackClient, sessionMapping, browserId, passedObjRecord[1].apiStatus, reason, passedObjRecord[1]['suite'])
removeFromSuiteResults(passedObjRecord[0])
allMessages = []
}
}
}
var result = browser.lastResult
log.debug('Browser Last Result: ' + JSON.stringify(result, null, 2))
if (result.disconnected) {
log.error('✖ Test Disconnected')
} else if (result.error) {
log.error('✖ Test Errored')
}
errorObjRecord = findSpecObjectRecords(browserId, 'Error')
if (typeof errorObjRecord[1] !== 'undefined') {
reason = 'Suite: ' + errorObjRecord[1]['suite'] + ', Error log: ' + errorObjRecord[1]['log']
common.updateStatusSession(log, browserstackClient, sessionMapping, browserId, errorObjRecord[1].apiStatus, reason, errorObjRecord[1]['suite'])
removeFromSuiteResults(errorObjRecord[0])
}
} catch (e) {
log.debug('Browserstack reporter module encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
}

// Wait until all updates have been pushed to Browserstack
this.onExit = function (done) {
callWhenFinished = done
exitIfAllFinished()
}
} catch (e) {
log.debug('Browserstack reporter full module encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
}

// PUBLISH DI MODULE
module.exports = BrowserStackReporter
93 changes: 93 additions & 0 deletions common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
var RestClient = require('node-rest-client').Client

module.exports = {
updateStatusSession: function (log, browserstackClient, sessionMapping, browserId, status, reason, sessionName) {
browserstackClient.updateSession(sessionMapping[browserId], {
status: status,
reason: reason,
name: sessionName.length !== 0 ? sessionName.toString() : 'Karma Test Suite'
}, function (error) {
if (error) {
log.error('✖ Could not update BrowserStack status' + error)
}
})
},
getSessionDetails: function (log, browserstackClient, sessionMapping, browserId, config, callback) {
try {
var apiClientEndpoint = config.browserStack.apiClientEndpoint
var creds = sessionMapping.credentials.username + ':' + sessionMapping.credentials.password

let buff = Buffer.from(creds)
let base64data = buff.toString('base64')
var encodedCreds = base64data
var client = new RestClient()
var args = {
data: {},
headers: {
'Authorization': 'Basic ' + encodedCreds
}
}
client.get(apiClientEndpoint + '/automate/sessions/' + sessionMapping[browserId] + '.json', args, function (data, response) {
callback(data)
})
} catch (e) {
log.debug('Browserstack reporter module encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
},
updateBuildName: function (log, buildId, originalBuildName, results, config) {
try {
log.debug('Attempting build name update')
if (typeof buildId === 'undefined') {
log.debug('BrowserStack build id is not defined. Not attempting update for build id!')
return
}

var apiClientEndpoint = config.browserStack.apiClientEndpoint
var client = new RestClient()
var newBuildName
var t = new Date().toISOString()

if (originalBuildName === null || typeof originalBuildName === 'undefined') {
originalBuildName = 'Karma build'
}

if (typeof results !== 'undefined') {
if (results.exitCode === 1) {
if (results.disconnected === true) {
newBuildName = originalBuildName + ' completed at ' + t + ' with 1 or more session disconnects: [Specs passed: ' + results.success + ' | Specs failed: ' + results.failed + ']'
} else if (results.error === true) {
newBuildName = originalBuildName + ' completed at ' + t + ' with 1 or more session errors: [Specs passed: ' + results.success + ' | Specs failed: ' + results.failed + ']'
} else if (results.failed !== 0) {
newBuildName = originalBuildName + ' completed at ' + t + ' with spec failures: [Specs passed: ' + results.success + ' | Specs failed: ' + results.failed + ']'
}
} else if (results.failed === 0) {
newBuildName = originalBuildName + ' completed successfully at ' + t + ': [Specs passed: ' + results.success + ']'
}
} else {
newBuildName = originalBuildName + ' started at ' + t + ' | In Progress | '
}

var creds = config.browserStack.username + ':' + config.browserStack.accessKey

let buff = Buffer.from(creds)
let base64data = buff.toString('base64')
var encodedCreds = base64data
var args = {
data: {
name: newBuildName
},
headers: {
'Authorization': 'Basic ' + encodedCreds,
'Content-Type': 'application/json'
}
}
var completeEndpoint = apiClientEndpoint + '/automate/builds/' + buildId + '.json'
log.debug('Complete endpoint for build name update: ' + completeEndpoint)
client.put(completeEndpoint, args, function (data, response) {
})
log.debug('Exiting updateBuildName function')
} catch (e) {
log.debug('Browserstack reporter module update build name function encountered issues.\nError message: ' + e.message + '\nStacktrace: ' + e.stack)
}
}
}
2 changes: 1 addition & 1 deletion gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = function (grunt) {
},
eslint: {
target: [
'index.js',
'*.js',
'worker.js',
'worker-manager.js',
'gruntfile.js'
Expand Down
Loading