Skip to content

Commit

Permalink
Vendor specific protocol commands for Sauce Labs (#3299)
Browse files Browse the repository at this point in the history
**Is your feature request related to a problem? Please describe.**

Sauce Labs supports vendor specific WebDriver extension commands (https://wiki.saucelabs.com/display/DOCS/Custom+Sauce+Labs+WebDriver+Extensions+for+Network+and+Log+Commands).

**Describe the solution you'd like**
It would be create if we could have a protocol that gets applied when running on SauceLabs.
  • Loading branch information
christian-bromann committed Jan 10, 2019
1 parent bbf7df6 commit 3a7beb9
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 341 deletions.
157 changes: 157 additions & 0 deletions packages/webdriver/protocol/saucelabs.json
@@ -0,0 +1,157 @@
{
"/session/:sessionId/sauce/ondemand/log": {
"POST": {
"command": "getPageLogs",
"description": "Get webpage specific log information based on the last page load.",
"ref": "https://wiki.saucelabs.com/display/DOCS/Custom+Sauce+Labs+WebDriver+Extensions+for+Network+and+Log+Commands#CustomSauceLabsWebDriverExtensionsforNetworkandLogCommands-ExtendedDebuggingTools",
"examples": [
[
"// Get Application Metrics Logs",
"console.log(browser.getPageLogs('sauce:metrics'));",
"/**",
" * outputs:",
" * {",
" * \"firstMeaningfulPaint\": 35036.03356,",
" * \"domContentLoaded\": 35036.122972,",
" * \"navigationStart\": 35035.833805,",
" * ...",
" * }",
" */"
],
[
"// Get Network Timing Logs",
"console.log(browser.getPageLogs('sauce:timing'));",
"/**",
" * outputs:",
" * {",
" * \"loadEventStart\": 622,",
" * \"loadEventEnd\": 622,",
" * \"domInteractive\": 359,",
" * ...",
" * }",
" */"
],
[
"// Get Network Logs",
"console.log(browser.getPageLogs('sauce:network'));",
"/**",
" * outputs:",
" * [{",
" * \"url\": \"https://app.saucelabs.com/dashboard\",",
" * \"statusCode\": 200,",
" * \"method\": \"GET\",",
" * \"requestHeaders\": {",
" * ...",
" * },",
" * \"responseHeaders\": {",
" * ...",
" * },",
" * \"timing\": {",
" * ...",
" * }",
" * }, {,",
" * ...",
" * }]",
" */"
],
[
"// Get Performance Logs (coming soon)",
"console.log(browser.getPageLogs('sauce:performance'));",
"/**",
" * outputs:",
" * {",
" * \"speedIndex\": 1472.023,",
" * \"timeToFirstInteractive\": 1243.214,",
" * \"firstMeaningfulPaint\": 892.643,",
" * ...",
" * }",
" */"
]
],
"parameters": [{
"name": "type",
"type": "string",
"description": "log type (e.g. 'sauce:timing', 'sauce:metrics', 'sauce:network', 'sauce:performance')",
"required": true
}],
"returns": {
"type": "Object",
"name": "log",
"description": "log output of desired type (see example)"
}
}
},
"/session/:sessionId/sauce/ondemand/throttle": {
"POST": {
"command": "throttleNetwork",
"description": "With network conditioning you can test your site on a variety of network connections, including Edge, 3G, and even offline. You can throttle the data throughput, including the maximum download and upload throughput, and use latency manipulation to enforce a minimum delay in connection round-trip time (RTT).",
"ref": "https://wiki.saucelabs.com/display/DOCS/Custom+Sauce+Labs+WebDriver+Extensions+for+Network+and+Log+Commands#CustomSauceLabsWebDriverExtensionsforNetworkandLogCommands-ThrottleNetworkCapabilities",
"examples": [
[
"// predefined network condition",
"browser.throttleNetwork('offline')"
],
[
"// custom network condition",
"browser.throttleNetwork({",
" download: 1000,",
" upload: 500,",
" latency: 40'",
"})"
]
],
"parameters": [{
"name": "condition",
"type": "(string|object)",
"description": "network condition to set (e.g. 'online', 'offline', 'GPRS', 'Regular 2G', 'Good 2G', 'Regular 3G', 'Good 3G', 'Regular 4G', 'DSL', 'Wifi')",
"required": true
}]
}
},
"/session/:sessionId/sauce/ondemand/intercept": {
"POST": {
"command": "interceptRequest",
"description": "Allows modifying any request made by the browser. You can blacklist, modify, or redirect these as required for your tests.",
"ref": "https://wiki.saucelabs.com/display/DOCS/Custom+Sauce+Labs+WebDriver+Extensions+for+Network+and+Log+Commands#CustomSauceLabsWebDriverExtensionsforNetworkandLogCommands-InterceptNetworkRequests",
"examples": [
[
"// redirect a request",
"browser.interceptRequest({",
" url: 'https://saucelabs.com',",
" redirect: 'https://google.com'",
"})"
],
[
"// Blacklist requests to 3rd party vendors",
"browser.interceptRequest({",
" url: 'https://api.segment.io/v1/p',",
" error: 'Failed'",
"})"
],
[
"// Modify requests to REST API (Mock REST API response)",
"browser.interceptRequest({",
" url: 'http://sampleapp.appspot.com/api/todos',",
" response: {",
" headers: {",
" 'x-custom-headers': 'foobar'",
" },",
" body: [{",
" title: 'My custom todo',",
" order: 1,",
" completed: false,",
" url: 'http://todo-backend-express.herokuapp.com/15727'",
" }]",
" }",
"})"
]
],
"parameters": [{
"name": "rule",
"type": "object",
"description": "A rule describing the request interception.",
"required": true
}]
}
}
}
4 changes: 2 additions & 2 deletions packages/webdriver/src/index.js
Expand Up @@ -59,7 +59,7 @@ export default class WebDriver {
/**
* apply mobile flags to driver scope
*/
const { isW3C, isMobile, isIOS, isAndroid, isChrome } = environmentDetector(params.capabilities)
const { isW3C, isMobile, isIOS, isAndroid, isChrome, isSauce } = environmentDetector(params)
const environmentFlags = {
isW3C: { value: isW3C },
isMobile: { value: isMobile },
Expand All @@ -68,7 +68,7 @@ export default class WebDriver {
isChrome: { value: isChrome }
}

const protocolCommands = getPrototype({ isW3C, isMobile, isIOS, isAndroid, isChrome })
const protocolCommands = getPrototype({ isW3C, isMobile, isIOS, isAndroid, isChrome, isSauce })
const prototype = merge(protocolCommands, environmentFlags, userPrototype)
const monad = webdriverMonad(params, modifier, prototype)
return monad(response.value.sessionId || response.sessionId, commandWrapper)
Expand Down
49 changes: 36 additions & 13 deletions packages/webdriver/src/utils.js
Expand Up @@ -7,6 +7,7 @@ import MJsonWProtocol from '../protocol/mjsonwp.json'
import JsonWProtocol from '../protocol/jsonwp.json'
import AppiumProtocol from '../protocol/appium.json'
import ChromiumProtocol from '../protocol/chromium.json'
import SauceLabsProtocol from '../protocol/saucelabs.json'

const log = logger('webdriver')

Expand Down Expand Up @@ -117,7 +118,7 @@ export function getArgumentType (arg) {
/**
* creates the base prototype for the webdriver monad
*/
export function getPrototype ({ isW3C, isChrome, isMobile }) {
export function getPrototype ({ isW3C, isChrome, isMobile, isSauce }) {
const prototype = {}
const ProtocolCommands = merge(
/**
Expand All @@ -131,15 +132,15 @@ export function getPrototype ({ isW3C, isChrome, isMobile }) {
/**
* only apply mobile protocol if session is actually for mobile
*/
isMobile
? merge(MJsonWProtocol, AppiumProtocol)
: {},
isMobile ? merge(MJsonWProtocol, AppiumProtocol) : {},
/**
* only apply special Chrome commands if session is using Chrome
*/
isChrome
? ChromiumProtocol
: {}
isChrome ? ChromiumProtocol : {},
/**
* only Sauce Labs specific vendor commands
*/
isSauce ? SauceLabsProtocol : {}
)

for (const [endpoint, methods] of Object.entries(ProtocolCommands)) {
Expand Down Expand Up @@ -253,17 +254,39 @@ export function isAndroid (caps) {
)
}

/**
* detects if session is run on Sauce with extended debugging enabled
* @param {string} hostname hostname of session request
* @param {object} capabilities session capabilities
* @return {Boolean} true if session is running on Sauce with extended debugging enabled
*/
export function isSauce (hostname, caps) {
return Boolean(
hostname &&
hostname.includes('saucelabs') &&
(
caps.extendedDebugging ||
(
caps['sauce:options'] &&
caps['sauce:options'].extendedDebugging
)
)
)
}

/**
* returns information about the environment
* @param {Object} hostname name of the host to run the session against
* @param {Object} capabilities caps of session response
* @return {Object} object with environment flags
*/
export function environmentDetector (caps) {
export function environmentDetector ({ hostname, capabilities, requestedCapabilities }) {
return {
isW3C: isW3C(caps),
isChrome: isChrome(caps),
isMobile: isMobile(caps),
isIOS: isIOS(caps),
isAndroid: isAndroid(caps)
isW3C: isW3C(capabilities),
isChrome: isChrome(capabilities),
isMobile: isMobile(capabilities),
isIOS: isIOS(capabilities),
isAndroid: isAndroid(capabilities),
isSauce: isSauce(hostname, requestedCapabilities.w3cCaps.alwaysMatch)
}
}
68 changes: 54 additions & 14 deletions packages/webdriver/tests/utils.test.js
Expand Up @@ -98,6 +98,10 @@ describe('utils', () => {
expect(typeof mobileChromePrototype.sendKeys.value).toBe('function')
expect(typeof mobileChromePrototype.lock.value).toBe('function')
expect(typeof mobileChromePrototype.getNetworkConnection.value).toBe('function')

const saucePrototype = getPrototype({ isW3C: true, isSauce: true })
expect(saucePrototype instanceof Object).toBe(true)
expect(typeof saucePrototype.getPageLogs.value).toBe('function')
})

it('commandCallStructure', () => {
Expand All @@ -106,68 +110,104 @@ describe('utils', () => {
})

describe('environmentDetector', () => {
const chromeCaps = chromedriverResponse.value
const appiumCaps = appiumResponse.value.capabilities
const geckoCaps = geckodriverResponse.value.capabilities

it('isW3C', () => {
expect(environmentDetector(appiumResponse.value.capabilities).isW3C).toBe(true)
expect(environmentDetector(chromedriverResponse.value).isW3C).toBe(false)
expect(environmentDetector(geckodriverResponse.value.capabilities).isW3C).toBe(true)
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
expect(environmentDetector({ capabilities: appiumCaps, requestedCapabilities }).isW3C).toBe(true)
expect(environmentDetector({ capabilities: chromeCaps, requestedCapabilities }).isW3C).toBe(false)
expect(environmentDetector({ capabilities: geckoCaps, requestedCapabilities }).isW3C).toBe(true)
})

it('isChrome', () => {
expect(environmentDetector(appiumResponse.value.capabilities).isChrome).toBe(false)
expect(environmentDetector(chromedriverResponse.value).isChrome).toBe(true)
expect(environmentDetector(geckodriverResponse.value.capabilities).isChrome).toBe(false)
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
expect(environmentDetector({ capabilities: appiumCaps, requestedCapabilities }).isChrome).toBe(false)
expect(environmentDetector({ capabilities: chromeCaps, requestedCapabilities }).isChrome).toBe(true)
expect(environmentDetector({ capabilities: geckoCaps, requestedCapabilities }).isChrome).toBe(false)
})

it('isSauce', () => {
const capabilities = { browserName: 'chrome' }
let requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
let hostname = 'ondemand.saucelabs.com'

expect(environmentDetector({ capabilities, requestedCapabilities }).isSauce).toBe(false)
expect(environmentDetector({ capabilities, hostname, requestedCapabilities }).isSauce).toBe(false)

requestedCapabilities.w3cCaps.alwaysMatch.extendedDebugging = true
expect(environmentDetector({ capabilities, hostname, requestedCapabilities }).isSauce).toBe(true)
requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
expect(environmentDetector({ capabilities, hostname, requestedCapabilities }).isSauce).toBe(false)

requestedCapabilities.w3cCaps.alwaysMatch['sauce:options'] = { extendedDebugging: true }
expect(environmentDetector({ capabilities, hostname, requestedCapabilities }).isSauce).toBe(true)
expect(environmentDetector({ capabilities, requestedCapabilities }).isSauce).toBe(false)
})

it('should not detect mobile app for browserName===undefined', function () {
const {isMobile, isIOS, isAndroid} = environmentDetector({})
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
const capabilities = {}
const {isMobile, isIOS, isAndroid} = environmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(false)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(false)
})

it('should not detect mobile app for browserName==="firefox"', function () {
const {isMobile, isIOS, isAndroid} = environmentDetector({browserName: 'firefox'})
const capabilities = { browserName: 'firefox' }
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
const {isMobile, isIOS, isAndroid} = environmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(false)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(false)
})

it('should not detect mobile app for browserName==="chrome"', function () {
const {isMobile, isIOS, isAndroid} = environmentDetector({browserName: 'chrome'})
const capabilities = { browserName: 'chrome' }
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
const {isMobile, isIOS, isAndroid} = environmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(false)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(false)
})

it('should detect mobile app for browserName===""', function () {
const {isMobile, isIOS, isAndroid} = environmentDetector({browserName: ''})
const capabilities = { browserName: '' }
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
const {isMobile, isIOS, isAndroid} = environmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(true)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(false)
})

it('should detect Android mobile app', function () {
const {isMobile, isIOS, isAndroid} = environmentDetector({
const capabilities = {
platformName: 'Android',
platformVersion: '4.4',
deviceName: 'LGVS450PP2a16334',
app: 'foo.apk'
})
}
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
const {isMobile, isIOS, isAndroid} = environmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(true)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(true)
})

it('should detect Android mobile app without upload', function () {
const {isMobile, isIOS, isAndroid} = environmentDetector({
const capabilities = {
platformName: 'Android',
platformVersion: '4.4',
deviceName: 'LGVS450PP2a16334',
appPackage: 'com.example',
appActivity: 'com.example.gui.LauncherActivity',
noReset: true,
appWaitActivity: 'com.example.gui.LauncherActivity'
})
}
const requestedCapabilities = { w3cCaps: { alwaysMatch: {} } }
const {isMobile, isIOS, isAndroid} = environmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(true)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(true)
Expand Down

0 comments on commit 3a7beb9

Please sign in to comment.