dev-utils/webdriver.js (294 lines of code) (raw):
/**
* MIT License
*
* Copyright (c) 2017-present, Elasticsearch BV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
const Promise = require('promise-polyfill')
const { join } = require('path')
const glob = require('glob')
const {
getSauceConnectOptions,
getTestEnvironmentVariables,
getBrowserList,
getAppiumBrowsersForWebdriver
} = require('./test-config')
const logLevels = {
ALL: { value: Number.MIN_VALUE },
DEBUG: { value: 700 },
INFO: { value: 800 },
WARNING: { value: 900 },
SEVERE: { value: 1000 },
OFF: { value: Number.MAX_VALUE }
}
const debugMode = false
const debugLevel = logLevels.INFO.value
function isLogEntryATestFailure(entry, whitelist = []) {
var result = false
if (logLevels[entry.level].value > logLevels.WARNING.value) {
result = true
// Chrome's versions lower than 81 had a bug where a preflight request with keepalive specified was not supported
// Bug info: https://bugs.chromium.org/p/chromium/issues/detail?id=835821
whitelist.push(
'Preflight request for request with keepalive specified is currently not supported'
)
for (var i = 0, l = whitelist.length; i < l; i++) {
if (entry.message.indexOf(whitelist[i]) !== -1) {
result = false
break
}
}
}
return result
}
function assertNoBrowserErrors(whitelist) {
return new Promise((resolve, reject) => {
/**
* browser.log API is available only in chrome driver
* from 76 using webdriver protocol
*
* https://bugs.chromium.org/p/chromedriver/issues/detail?id=2947
*/
if (!isChromeLatest()) {
return resolve()
}
const failureEntries = []
const debugLogs = []
const browserLog = browser.getLogs('browser')
for (var i = 0; i < browserLog.length; i++) {
var logEntry = browserLog[i]
if (isLogEntryATestFailure(logEntry, whitelist)) {
failureEntries.push(logEntry)
}
if (logLevels[logEntry.level].value >= debugLevel) {
debugLogs.push(logEntry)
}
}
if (failureEntries.length > 0 || debugMode) {
console.log('------------> FailuresLogs')
console.log(JSON.stringify(failureEntries, undefined, 2))
}
if (debugMode) {
console.log('------------> debugLogs')
console.log(JSON.stringify(debugLogs, undefined, 2))
}
if (failureEntries.length > 0) {
return reject(
new Error(
'Expected no errors in the browserLog but got ' +
failureEntries.length +
' error(s)'
)
)
}
resolve()
})
}
function allowSomeBrowserErrors(whitelist, done) {
if (typeof done === 'function') {
assertNoBrowserErrors(whitelist).then(() => {
done()
})
} else {
return assertNoBrowserErrors(whitelist)
}
}
function verifyNoBrowserErrors(done) {
if (typeof done === 'function') {
assertNoBrowserErrors([]).then(() => {
done()
})
} else {
return assertNoBrowserErrors([])
}
}
function handleError(done) {
return function (error) {
console.log(error, error.stack)
if (error.message.indexOf('received Inspector.detached event') === -1) {
done.fail(error)
}
done()
}
}
function getWebdriveBaseConfig(
path,
specs = './test/e2e/**/*.e2e-spec.js',
capabilities
) {
const { tunnelIdentifier, username, accessKey } = getSauceConnectOptions()
/**
* Skip the ios platform on E2E tests because of script
* timeout issue in Appium
*/
const browsers = [...getBrowserList(), ...getAppiumBrowsersForWebdriver()]
capabilities = (capabilities || browsers)
.map(c => {
const sauceOptions = c['sauce:options'] || {}
sauceOptions.tunnelIdentifier = tunnelIdentifier
sauceOptions.seleniumVersion = '3.141.59'
return {
...c,
'sauce:options': sauceOptions
}
})
.filter(({ platformName }) => platformName !== 'iOS')
const baseConfig = {
runner: 'local',
specs: glob.sync(join(path, specs)),
maxInstancesPerCapability: 3,
services: ['sauce'],
user: username,
key: accessKey,
sauceConnect: false,
capabilities,
logLevel: 'error',
bail: 1,
screenshotPath: join(path, 'error-screenshot'),
baseUrl: 'http://localhost:8000',
waitforTimeout: 30000,
framework: 'jasmine',
reporters: ['dot', 'spec'],
jasmineOpts: {
defaultTimeoutInterval: 90000
},
async before() {
/**
* Increase script timeout so that executeAsyncScript does not
* throw async script failure in 0 ms error
*
* Skip setting timeouts on firefox and microsoftedge since they
* result in NullPointerException issue from selenium driver
*/
const { name } = getBrowserInfo()
if (name === 'firefox' || name === 'microsoftedge') {
return
}
await browser.setTimeout({ script: 30000 })
},
afterTest(test) {
/**
* Log only on failures
* Log api is only available in chrome driver
* */
if (!test.passed && isChromeLatest()) {
const response = browser.getLogs('browser')
console.log(
'[Chrome Browser Logs]:',
JSON.stringify(response, undefined, 2)
)
}
}
}
const { sauceLabs } = getTestEnvironmentVariables()
if (!sauceLabs) {
Object.assign(baseConfig, {
automationProtocol: 'devtools',
capabilities: [
{
browserName: 'chrome',
'goog:chromeOptions': {
headless: true
}
},
{
browserName: 'firefox',
'moz:firefoxOptions': {
headless: true
}
}
]
})
}
return baseConfig
}
function isPageLoaded() {
return browser.execute(() => {
return document.readyState === 'complete'
})
}
async function getLastServerCall(errorCount = 0, transactionCount = 0) {
const { name = '', version = '' } = getBrowserInfo()
console.log(
`Waiting for minimum ${errorCount} Errors and ${transactionCount} Transactions in`,
name,
version
)
const serverCalls = await browser.executeAsync(
function (errorCount, transactionCount, done) {
var apmServerMock = window.elasticApm.serviceFactory.getService(
'ApmServer'
)
function checkCalls() {
var serverCalls = apmServerMock.calls
/**
* If there is more than one event we consider that as valid call
*/
var validCall =
serverCalls.sendEvents && serverCalls.sendEvents.length > 0
if (validCall) {
var promises = serverCalls.sendEvents.map(function (s) {
return s.returnValue
})
Promise.all(promises)
.then(function () {
var transactions = []
var errors = []
var spyCalls = serverCalls.sendEvents
for (var i = 0; i < spyCalls.length; i++) {
var args = spyCalls[i].args
for (var j = 0; j < args.length; j++) {
var arg = args[j]
arg.forEach(function (event) {
if (event['transactions']) {
transactions.push(event['transactions'])
} else if (event['errors']) {
errors.push(event['errors'])
}
})
}
}
if (
errors.length >= errorCount &&
transactions.length >= transactionCount
) {
var calls = {
sendEvents: {
// eslint-disable-next-line object-shorthand
transactions: transactions,
// eslint-disable-next-line object-shorthand
errors: errors
}
}
apmServerMock.resetMock()
done(calls)
}
})
.catch(function (reason) {
console.log('reason', JSON.stringify(reason))
apmServerMock.resetMock()
try {
done({ error: reason.message || JSON.stringify(reason) })
} catch (e) {
done({
error: 'Failed serializing rejection reason: ' + e.message
})
}
})
}
}
checkCalls()
apmServerMock.events.observe('sendEvents', checkCalls)
},
errorCount,
transactionCount
)
if (!serverCalls) {
throw new Error('serverCalls is undefined!')
}
console.log(
`Payload in ${name} ${version}`,
JSON.stringify(serverCalls, null, 2)
)
if (serverCalls.error) {
fail(serverCalls.error)
}
return serverCalls
}
function getBrowserInfo() {
const { browserVersion, browserName } = browser.capabilities
return {
name: browserName.toLowerCase(),
version: browserVersion
}
}
async function getBrowserFeatures() {
return await browser.executeAsync(function (done) {
done({
EventTarget: !!window.EventTarget
})
})
}
function isChromeLatest() {
const { name, version } = getBrowserInfo()
const isChrome = name.indexOf('chrome') !== -1
const isLatest = isChrome && version && Number(version.split('.')[0]) >= 76
return isLatest
}
module.exports = {
allowSomeBrowserErrors,
verifyNoBrowserErrors,
handleError,
isChromeLatest,
getWebdriveBaseConfig,
getBrowserInfo,
getLastServerCall,
getBrowserFeatures,
isPageLoaded
}