diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts
index c87c557811..24990c06d8 100644
--- a/cli/types/index.d.ts
+++ b/cli/types/index.d.ts
@@ -60,7 +60,7 @@ declare namespace Cypress {
name: "electron" | "chrome" | "canary" | "chromium" | "firefox"
displayName: "Electron" | "Chrome" | "Canary" | "Chromium" | "FireFox"
version: string
- majorVersion: string
+ majorVersion: number
path: string
isHeaded: boolean
isHeadless: boolean
diff --git a/packages/desktop-gui/cypress/fixtures/config.json b/packages/desktop-gui/cypress/fixtures/config.json
index ae30abd14b..9eef4b608c 100644
--- a/packages/desktop-gui/cypress/fixtures/config.json
+++ b/packages/desktop-gui/cypress/fixtures/config.json
@@ -34,10 +34,11 @@
"commandTimeout": 4000,
"cypressHostUrl": "http://localhost:2020",
"cypressEnv": "development",
- "env": {
-
- },
- "blacklistHosts": ["www.google-analytics.com", "hotjar.com"],
+ "env": {},
+ "blacklistHosts": [
+ "www.google-analytics.com",
+ "hotjar.com"
+ ],
"execTimeout": 60000,
"fileServerFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink",
"fixturesFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/fixtures",
@@ -46,9 +47,7 @@
"integrationFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration",
"isHeadless": false,
"isNewProject": false,
- "javascripts": [
-
- ],
+ "javascripts": [],
"morgan": true,
"namespace": "__cypress",
"numTestsKeptInMemory": 50,
@@ -180,6 +179,43 @@
"from": "config",
"value": "http://localhost:8080"
},
+ "browsers": {
+ "from": "plugins",
+ "value": [
+ {
+ "name": "chrome",
+ "displayName": "Chrome",
+ "family": "chrome",
+ "version": "50.0.2661.86",
+ "path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "majorVersion": "50"
+ },
+ {
+ "name": "chromium",
+ "displayName": "Chromium",
+ "family": "chrome",
+ "version": "49.0.2609.0",
+ "path": "/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium",
+ "majorVersion": "49"
+ },
+ {
+ "name": "canary",
+ "displayName": "Canary",
+ "family": "chrome",
+ "version": "48.0",
+ "path": "/Users/bmann/Downloads/chrome-mac/Canary.app/Contents/MacOS/Canary",
+ "majorVersion": "48"
+ },
+ {
+ "name": "electron",
+ "family": "electron",
+ "displayName": "Electron",
+ "path": "",
+ "version": "99.101.1234",
+ "majorVersion": "99"
+ }
+ ]
+ },
"commandTimeout": {
"from": "default",
"value": 4000
@@ -269,7 +305,8 @@
"blacklistHosts": {
"from": "config",
"value": [
- "www.google-analytics.com", "hotjar.com"
+ "www.google-analytics.com",
+ "hotjar.com"
]
},
"hosts": {
diff --git a/packages/desktop-gui/cypress/integration/settings_spec.js b/packages/desktop-gui/cypress/integration/settings_spec.js
index 86df12f18f..f3755707aa 100644
--- a/packages/desktop-gui/cypress/integration/settings_spec.js
+++ b/packages/desktop-gui/cypress/integration/settings_spec.js
@@ -77,17 +77,85 @@ describe('Settings', () => {
cy.contains('Your project\'s configuration is displayed')
})
- it('displays legend in table', () => {
- cy.get('table>tbody>tr').should('have.length', 6)
+ it('displays browser information which is collapsed by default', () => {
+ cy.contains('.config-vars', 'browsers')
+ cy.get('.config-vars').invoke('text')
+ .should('not.contain', '0:Chrome')
+
+ cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
+ cy.get('.config-vars').invoke('text')
+ .should('contain', '0:Chrome')
})
- it('wraps config line in proper classes', () => {
- cy.get('.line').first().within(() => {
- cy.contains('animationDistanceThreshold').should('have.class', 'key')
- cy.contains(':').should('have.class', 'colon')
- cy.contains('5').should('have.class', 'default')
- cy.contains(',').should('have.class', 'comma')
- })
+ it('removes the summary list of values once a key is expanded', () => {
+ cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
+ cy.get('.config-vars').invoke('text')
+ .should('not.contain', 'Chrome, Chromium')
+
+ cy.get('.config-vars').invoke('text')
+ .should('contain', '0:Chrome')
+ })
+
+ it('distinguishes between Arrays and Objects when expanded', () => {
+ cy.get('.config-vars').invoke('text')
+ .should('not.contain', 'browsers: Array (4)')
+
+ cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
+ cy.get('.config-vars').invoke('text')
+ .should('contain', 'browsers: Array (4)')
+ })
+
+ it('applies the same color treatment to expanded key values as the root key', () => {
+ cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
+ cy.get('.config-vars').as('config-vars')
+ .contains('span', 'Chrome').parent('span').should('have.class', 'plugins')
+
+ cy.get('@config-vars')
+ .contains('span', 'Chromium').parent('span').should('have.class', 'plugins')
+
+ cy.get('@config-vars')
+ .contains('span', 'Canary').parent('span').should('have.class', 'plugins')
+
+ cy.get('@config-vars')
+ .contains('span', 'Electron').parent('span').should('have.class', 'plugins')
+
+ cy.contains('span', 'blacklistHosts').parents('div').first().find('span').first().click()
+ cy.get('@config-vars')
+ .contains('span', 'www.google-analytics.com').parent('span').should('have.class', 'config')
+
+ cy.get('@config-vars')
+ .contains('span', 'hotjar.com').parent('span').should('have.class', 'config')
+
+ cy.contains('span', 'hosts').parents('div').first().find('span').first().click()
+ cy.get('@config-vars')
+ .contains('span', '127.0.0.1').parent('span').should('have.class', 'config')
+
+ cy.get('@config-vars')
+ .contains('span', '127.0.0.2').parent('span').should('have.class', 'config')
+
+ cy.get('@config-vars')
+ .contains('span', 'Electron').parents('div').first().find('span').first().click()
+
+ cy.get('@config-vars').contains('span', 'electron').parents('li').eq(1).find('.line .plugins').should('have.length', 6)
+ })
+
+ it('displays string values as quoted strings', () => {
+ cy.get('.config-vars').invoke('text')
+ .should('contain', 'baseUrl:"http://localhost:8080"')
+ })
+
+ it('displays undefined and null without quotations', () => {
+ cy.get('.config-vars').invoke('text')
+ .should('not.contain', '"undefined"')
+ .should('not.contain', '"null"')
+ })
+
+ it('does not show the root config label', () => {
+ cy.get('.config-vars').find('> ol > li > div').should('have.css', 'display', 'none')
+ })
+
+ it('displays legend in table', () => {
+ cy.get('table>tbody>tr').should('have.length', 6)
})
it('displays "true" values', () => {
@@ -99,26 +167,13 @@ describe('Settings', () => {
})
it('displays "object" values for env and hosts', () => {
- cy.get('.nested-obj').eq(0)
- .contains('fixturesFolder')
+ cy.get('.line').contains('www.google-analytics.com, hotjar.com')
- cy.get('.nested-obj').eq(1)
- .contains('*.foobar.com')
+ cy.get('.line').contains('*.foobar.com, *.bazqux.com')
})
it('displays "array" values for blacklistHosts', () => {
- cy.get('.nested-arr')
- .parent()
- .should('contain', '[')
- .and('contain', ']')
- .and('not.contain', '0')
- .and('not.contain', '1')
- .find('.line .config').should(($lines) => {
- expect($lines).to.have.length(2)
- expect($lines).to.contain('www.google-analytics.com')
-
- expect($lines).to.contain('hotjar.com')
- })
+ cy.contains('.line', 'blacklistHosts').contains('www.google-analytics.com, hotjar.com')
})
it('opens help link on click', () => {
diff --git a/packages/desktop-gui/package.json b/packages/desktop-gui/package.json
index eda71ccd9e..76dba09600 100644
--- a/packages/desktop-gui/package.json
+++ b/packages/desktop-gui/package.json
@@ -17,6 +17,7 @@
"watch": "npm run build -- --watch --progress"
},
"devDependencies": {
+ "@babel/polyfill": "^7.7.0",
"@cypress/icons": "0.7.0",
"@cypress/json-schemas": "5.33.0",
"@cypress/react-tooltip": "0.5.3",
@@ -40,6 +41,7 @@
"react": "16.8.6",
"react-bootstrap-modal": "4.2.0",
"react-dom": "16.8.6",
+ "react-inspector": "^4.0.0",
"react-loader": "2.4.5",
"webpack": "4.35.3",
"webpack-cli": "3.3.2"
diff --git a/packages/desktop-gui/src/settings/configuration.jsx b/packages/desktop-gui/src/settings/configuration.jsx
index 259d261e78..f804441548 100644
--- a/packages/desktop-gui/src/settings/configuration.jsx
+++ b/packages/desktop-gui/src/settings/configuration.jsx
@@ -1,115 +1,105 @@
import _ from 'lodash'
+import cn from 'classnames'
import { observer } from 'mobx-react'
import React from 'react'
import Tooltip from '@cypress/react-tooltip'
+import { ObjectInspector, ObjectName } from 'react-inspector'
import { configFileFormatted } from '../lib/config-file-formatted'
import ipc from '../lib/ipc'
-const display = (obj) => {
- const keys = _.keys(obj)
- const lastKey = _.last(keys)
-
- return _.map(obj, (value, key) => {
- const hasComma = lastKey !== key
-
- if (value.from == null) {
- return displayNestedObj(key, value, hasComma)
- }
-
- if (value.isArray) {
- return getSpan(key, value, hasComma, true)
- }
-
- if (_.isObject(value.value)) {
- const realValue = value.value.toJS ? value.value.toJS() : value.value
-
- if (_.isArray(realValue)) {
- return displayArray(key, value, hasComma)
+const formatData = (data) => {
+ if (Array.isArray(data)) {
+ return _.map(data, (v) => {
+ if (_.isObject(v) && (v.name || v.displayName)) {
+ return _.defaultTo(v.displayName, v.name)
}
- return displayObject(key, value, hasComma)
- }
+ return String(v)
+ }).join(', ')
+ }
- return getSpan(key, value, hasComma)
- })
+ if (_.isObject(data)) {
+ return _.defaultTo(_.defaultTo(data.displayName, data.name), String(Object.keys(data).join(', ')))
+ }
+
+ const excludedFromQuotations = ['null', 'undefined']
+
+ if (_.isString(data) && !excludedFromQuotations.includes(data)) {
+ return `"${data}"`
+ }
+
+ return String(data)
}
+const ObjectLabel = ({ name, data, expanded, from, isNonenumerable }) => {
+ const formattedData = formatData(data)
-const displayNestedObj = (key, value, hasComma) => (
-
-
- {key}
- :{' '}
- {'{'}
- {display(value)}
-
- {'}'}{getComma(hasComma)}
-
-
-)
-
-const displayNestedArr = (key, value, hasComma) => (
-
-
- {key}
- :{' '}
- {'['}
- {display(value)}
-
- {']'}{getComma(hasComma)}
-
-
-)
-
-const displayArray = (key, nestedArr, hasComma) => {
- const arr = _.map(nestedArr.value, (value) => {
- return { value, from: nestedArr.from, isArray: true }
- })
-
- return displayNestedArr(key, arr, hasComma)
-}
-
-const displayObject = (key, nestedObj, hasComma) => {
- const obj = _.reduce(nestedObj.value, (obj, value, key) => {
- return _.extend(obj, {
- [key]: { value, from: nestedObj.from },
- })
- }, {})
-
- return displayNestedObj(key, obj, hasComma)
-}
-
-const getSpan = (key, obj, hasComma, isArray) => {
return (
-
- {getKey(key, isArray)}
- {getColon(isArray)}
-
-
- {getString(obj.value)}
- {`${obj.value}`}
- {getString(obj.value)}
-
-
- {getComma(hasComma)}
-
+
+
+ :
+ {!expanded && (
+ <>
+
+
+ {formattedData}
+
+
+ >
+ )}
+ {expanded && Array.isArray(data) && (
+ Array ({data.length})
+ )}
+
)
}
-const getKey = (key, isArray) => {
- return isArray ? '' : {key}
+ObjectLabel.defaultProps = {
+ data: 'undefined',
}
-const getColon = (isArray) => {
- return isArray ? '' : :{' '}
+const createComputeFromValue = (obj) => {
+ return (name, path) => {
+ const pathParts = path.split('.')
+ const pathDepth = pathParts.length
+
+ const rootKey = pathDepth <= 2 ? name : pathParts[1]
+
+ return obj[rootKey] ? obj[rootKey].from : undefined
+ }
}
-const getString = (val) => {
- return _.isString(val) ? '\'' : ''
-}
+const ConfigDisplay = ({ data: obj }) => {
+ const computeFromValue = createComputeFromValue(obj)
+ const renderNode = ({ depth, name, data, isNonenumerable, expanded, path }) => {
+ if (depth === 0) {
+ return null
+ }
-const getComma = (hasComma) => {
- return hasComma ? , : ''
+ const from = computeFromValue(name, path)
+
+ return (
+
+ )
+ }
+
+ const data = _.reduce(obj, (acc, value, key) => Object.assign(acc, {
+ [key]: value.value,
+ }), {})
+
+ return (
+
+ {'{'}
+
+ {'}'}
+
+ )
}
const openHelp = (e) => {
@@ -146,16 +136,12 @@ const Configuration = observer(({ project }) => (
set from CLI arguments |
- | plugin |
+ plugin |
set from plugin file |
-
- {'{'}
- {display(project.resolvedConfig)}
- {'}'}
-
+
))
diff --git a/packages/desktop-gui/src/settings/settings.scss b/packages/desktop-gui/src/settings/settings.scss
index baf7c5b8ba..cb553116d5 100644
--- a/packages/desktop-gui/src/settings/settings.scss
+++ b/packages/desktop-gui/src/settings/settings.scss
@@ -68,45 +68,40 @@
font-size: 13px;
border: 1px solid #ddd;
border-radius: 3px;
- background-color: #252831;
+ background: #fff;
color: #eee;
margin: 0;
- padding: 10px;
+ padding: 8px 12px;
font-family: $font-mono;
+ > span {
+ color: #000;
+ }
- .envFile:hover, .env:hover, .config:hover, .cli:hover, .plugin:hover, .default:hover {
+ ol[role="tree"] {
+ > li > div{
+ display: none;
+ }
+ }
+
+ .key-value-pair-value {
+ margin-left: 2px;
+ display: inline-block;
+ line-height: 1;
+ padding: 2px;
+ border-bottom: 1px solid transparent
+ }
+
+ .key-value-pair-value:hover {
border-bottom: 1px dotted #777;
}
p {
margin-bottom: 0;
}
-
- .key {
- color: #ccc;
- }
-
- .comma, .colon, {
- color: #ccc;
- }
-
- .line {
- margin-left: 15px;
- color: #eee;
- }
-
- .nested {
- margin-left: 15px;
-
- .line {
- margin-left: 30px;
- }
- }
}
- .envFile, .env, .config, .cli, .plugin, .default {
+ .envFile, .env, .config, .cli, .plugins, .default {
font-family: $font-mono;
-
padding: 2px;
}
@@ -130,7 +125,7 @@
color: #A21313;
}
- .plugin {
+ .plugins {
background-color: #f0e7fc;
color: #134aa2;
}
diff --git a/packages/desktop-gui/webpack.config.ts b/packages/desktop-gui/webpack.config.ts
index b99ac09363..a4c821d2ec 100644
--- a/packages/desktop-gui/webpack.config.ts
+++ b/packages/desktop-gui/webpack.config.ts
@@ -4,7 +4,7 @@ import path from 'path'
const config: typeof commonConfig = {
...commonConfig,
entry: {
- app: [path.resolve(__dirname, 'src/main')],
+ app: [require.resolve('@babel/polyfill'), path.resolve(__dirname, 'src/main')],
},
output: {
path: path.resolve(__dirname, 'dist'),
diff --git a/packages/launcher/lib/detect.ts b/packages/launcher/lib/detect.ts
index fd7ba9890b..12caecf7fe 100644
--- a/packages/launcher/lib/detect.ts
+++ b/packages/launcher/lib/detect.ts
@@ -15,7 +15,14 @@ import {
} from './types'
import * as windowsHelper from './windows'
-const setMajorVersion = (browser: FoundBrowser) => {
+type HasVersion = {
+ version?: string
+ majorVersion?: string | number
+ name: string
+}
+
+// TODO: make this function NOT change its argument
+export const setMajorVersion = (browser: T): T => {
if (browser.version) {
browser.majorVersion = browser.version.split('.')[0]
log(
@@ -24,6 +31,10 @@ const setMajorVersion = (browser: FoundBrowser) => {
browser.version,
browser.majorVersion
)
+
+ if (browser.majorVersion) {
+ browser.majorVersion = parseInt(browser.majorVersion)
+ }
}
return browser
@@ -153,14 +164,18 @@ export const detectByPath = (
const regexExec = browser.versionRegex.exec(stdout) as Array
- return extend({}, browser, {
+ const parsedBrowser = {
+ name: browser.name,
displayName: `Custom ${browser.displayName}`,
info: `Loaded from ${path}`,
custom: true,
path,
version: regexExec[1],
- majorVersion: regexExec[1].split('.', 2)[0],
- })
+ }
+
+ setMajorVersion(parsedBrowser)
+
+ return extend({}, browser, parsedBrowser)
}
return helper
diff --git a/packages/launcher/test/unit/detect_spec.ts b/packages/launcher/test/unit/detect_spec.ts
index dc3fc0d397..694b71da0d 100644
--- a/packages/launcher/test/unit/detect_spec.ts
+++ b/packages/launcher/test/unit/detect_spec.ts
@@ -1,4 +1,5 @@
-import { detect } from '../../lib/detect'
+require('../spec_helper')
+import { detect, setMajorVersion } from '../../lib/detect'
const os = require('os')
import { log } from '../log'
import { project } from 'ramda'
@@ -32,4 +33,14 @@ describe('browser detection', () => {
it('detects available browsers', () => {
return detect().then(checkBrowsers)
})
+
+ context('setMajorVersion', () => {
+ const foundBrowser = {
+ name: 'test browser',
+ version: '11.22.33',
+ }
+
+ setMajorVersion(foundBrowser)
+ expect(foundBrowser.majorVersion, 'major version was converted to number').to.equal(11)
+ })
})
diff --git a/packages/launcher/test/unit/linux/spec.ts b/packages/launcher/test/unit/linux/spec.ts
index 4489aadca7..9ff336218f 100644
--- a/packages/launcher/test/unit/linux/spec.ts
+++ b/packages/launcher/test/unit/linux/spec.ts
@@ -74,14 +74,14 @@ describe('linux browser detection', () => {
name: 'test-browser-name',
version: '100.1.2.3',
path: 'test-browser',
- majorVersion: '100',
+ majorVersion: 100,
},
{
displayName: 'Foo Browser',
name: 'foo-browser',
version: '100.1.2.3',
path: 'foo-browser',
- majorVersion: '100',
+ majorVersion: 100,
},
]
@@ -107,7 +107,7 @@ describe('linux browser detection', () => {
name: 'foo-browser',
version: '100.1.2.3',
path: 'foo-browser',
- majorVersion: '100',
+ majorVersion: 100,
},
]
@@ -130,7 +130,7 @@ describe('linux browser detection', () => {
info: 'Loaded from /foo/bar/browser',
custom: true,
version: '9001.1.2.3',
- majorVersion: '9001',
+ majorVersion: 9001,
path: '/foo/bar/browser',
})
)
@@ -168,7 +168,7 @@ describe('linux browser detection', () => {
info: 'Loaded from /Applications/My Shiny New Browser.app',
custom: true,
version: '100.1.2.3',
- majorVersion: '100',
+ majorVersion: 100,
path: '/Applications/My Shiny New Browser.app',
})
)
diff --git a/packages/server/README.md b/packages/server/README.md
index e40d36c06e..110defc15b 100644
--- a/packages/server/README.md
+++ b/packages/server/README.md
@@ -73,4 +73,6 @@ To run an individual e2e test:
npm run test-e2e -- --spec base_url
```
-To update snapshots, see `snap-shot-it` instructions: https://github.com/bahmutov/snap-shot-it#advanced-use
\ No newline at end of file
+When running e2e tests, some test projects output verbose logs. To see them run the test with `DEBUG=cypress:e2e` environment variable.
+
+To update snapshots, see `snap-shot-it` instructions: https://github.com/bahmutov/snap-shot-it#advanced-use
diff --git a/packages/server/__snapshots__/3_config_spec.coffee.js b/packages/server/__snapshots__/3_config_spec.coffee.js
index 42b9e5563a..7fad62eb5b 100644
--- a/packages/server/__snapshots__/3_config_spec.coffee.js
+++ b/packages/server/__snapshots__/3_config_spec.coffee.js
@@ -138,3 +138,17 @@ exports['e2e config fails 1'] = `
`
+
+exports['e2e config catches invalid viewportWidth in the configuration file 1'] = `
+We found an invalid value in the file: \`cypress.json\`
+
+Expected \`viewportWidth\` to be a number. Instead the value was: \`"foo"\`
+
+`
+
+exports['e2e config catches invalid browser in the configuration file 1'] = `
+We found an invalid value in the file: \`cypress.json\`
+
+Found an error while validating the \`browsers\` list. Expected \`family\` to be either electron, chrome or firefox. Instead the value was: \`{"name":"bad browser","family":"unknown family","displayName":"Bad browser","version":"no version","path":"/path/to","majorVersion":123}\`
+
+`
diff --git a/packages/server/__snapshots__/3_plugins_spec.coffee.js b/packages/server/__snapshots__/3_plugins_spec.coffee.js
index 3d6be86e83..49a7aa32b6 100644
--- a/packages/server/__snapshots__/3_plugins_spec.coffee.js
+++ b/packages/server/__snapshots__/3_plugins_spec.coffee.js
@@ -372,3 +372,33 @@ exports['e2e plugins calls after:screenshot for cy.screenshot() and failure scre
`
+
+exports['e2e plugins can filter browsers from config 1'] = `
+Can't run because you've entered an invalid browser name.
+
+Browser: 'chrome' was not found on your system.
+
+Available browsers found are: electron
+
+`
+
+exports['e2e plugins catches invalid viewportWidth returned from plugins 1'] = `
+An invalid configuration value returned from the plugins file: \`cypress/plugins/index.coffee\`
+
+Expected \`viewportWidth\` to be a number. Instead the value was: \`"foo"\`
+
+`
+
+exports['e2e plugins catches invalid browsers list returned from plugins 1'] = `
+An invalid configuration value returned from the plugins file: \`cypress/plugins/index.coffee\`
+
+Expected at list one browser
+
+`
+
+exports['e2e plugins catches invalid browser returned from plugins 1'] = `
+An invalid configuration value returned from the plugins file: \`cypress/plugins/index.coffee\`
+
+Found an error while validating the \`browsers\` list. Expected \`displayName\` to be a non-empty string. Instead the value was: \`{"name":"browser name","family":"chrome"}\`
+
+`
diff --git a/packages/server/__snapshots__/validation_spec.coffee.js b/packages/server/__snapshots__/validation_spec.coffee.js
index b6acfcb9fd..10ba68ea83 100644
--- a/packages/server/__snapshots__/validation_spec.coffee.js
+++ b/packages/server/__snapshots__/validation_spec.coffee.js
@@ -21,3 +21,69 @@ Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`"
exports['null instead of a number'] = `
Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`null\`
`
+
+exports['lib/util/validation #isValidBrowser passes valid browsers and forms error messages for invalid ones isValidBrowser 1'] = {
+ "name": "isValidBrowser",
+ "behavior": [
+ {
+ "given": {
+ "name": "Chrome",
+ "displayName": "Chrome Browser",
+ "family": "chrome",
+ "path": "/path/to/chrome",
+ "version": "1.2.3",
+ "majorVersion": 1
+ },
+ "expect": true
+ },
+ {
+ "given": {
+ "name": "FF",
+ "displayName": "Firefox",
+ "family": "firefox",
+ "path": "/path/to/firefox",
+ "version": "1.2.3",
+ "majorVersion": "1"
+ },
+ "expect": true
+ },
+ {
+ "given": {
+ "name": "Electron",
+ "displayName": "Electron",
+ "family": "electron",
+ "path": "",
+ "version": "99.101.3",
+ "majorVersion": 99
+ },
+ "expect": true
+ },
+ {
+ "given": {
+ "name": "No display name",
+ "family": "electron"
+ },
+ "expect": "Expected `displayName` to be a non-empty string. Instead the value was: `{\"name\":\"No display name\",\"family\":\"electron\"}`"
+ },
+ {
+ "given": {
+ "name": "bad family",
+ "displayName": "Bad family browser",
+ "family": "unknown family"
+ },
+ "expect": "Expected `family` to be either electron, chrome or firefox. Instead the value was: `{\"name\":\"bad family\",\"displayName\":\"Bad family browser\",\"family\":\"unknown family\"}`"
+ }
+ ]
+}
+
+exports['undefined browsers'] = `
+Missing browsers list
+`
+
+exports['empty list of browsers'] = `
+Expected at list one browser
+`
+
+exports['browsers list with a string'] = `
+Found an error while validating the \`browsers\` list. Expected \`name\` to be a non-empty string. Instead the value was: \`"foo"\`
+`
diff --git a/packages/server/lib/browsers/index.coffee b/packages/server/lib/browsers/index.coffee
index 9c6dead6b8..5287fe6ba9 100644
--- a/packages/server/lib/browsers/index.coffee
+++ b/packages/server/lib/browsers/index.coffee
@@ -2,6 +2,7 @@ _ = require("lodash")
path = require("path")
Promise = require("bluebird")
debug = require("debug")("cypress:server:browsers")
+pluralize = require("pluralize")
utils = require("./utils")
errors = require("../errors")
fs = require("../util/fs")
@@ -36,6 +37,7 @@ cleanup = ->
instance = null
getBrowserLauncherByFamily = (family) ->
+ debug("getBrowserLauncherByFamily %o", { family })
if not isBrowserFamily(family)
debug("unknown browser family", family)
@@ -48,9 +50,13 @@ getBrowserLauncherByFamily = (family) ->
isValidPathToBrowser = (str) ->
path.basename(str) isnt str
-ensureAndGetByNameOrPath = (nameOrPath, returnAll = false) ->
- utils.getBrowsers(nameOrPath)
+ensureAndGetByNameOrPath = (nameOrPath, returnAll = false, browsers = null) ->
+ findBrowsers = if Array.isArray(browsers) then Promise.resolve(browsers) else utils.getBrowsers()
+
+ findBrowsers
.then (browsers = []) ->
+ debug("searching for browser %o", { nameOrPath, knownBrowsers: browsers })
+
## try to find the browser by name with the highest version property
sortedBrowsers = _.sortBy(browsers, ['version'])
if browser = _.findLast(sortedBrowsers, { name: nameOrPath })
@@ -93,6 +99,7 @@ module.exports = {
close: kill
getAllBrowsersWith: (nameOrPath) ->
+ debug("getAllBrowsersWith %o", { nameOrPath })
if nameOrPath
return ensureAndGetByNameOrPath(nameOrPath, true)
utils.getBrowsers()
diff --git a/packages/server/lib/browsers/utils.coffee b/packages/server/lib/browsers/utils.coffee
index 1311529098..06215e7156 100644
--- a/packages/server/lib/browsers/utils.coffee
+++ b/packages/server/lib/browsers/utils.coffee
@@ -1,9 +1,11 @@
path = require("path")
+debug = require("debug")("cypress:server:browsers:utils")
Promise = require("bluebird")
getPort = require("get-port")
launcher = require("@packages/launcher")
fs = require("../util/fs")
appData = require("../util/app_data")
+pluralize = require("pluralize")
profileCleaner = require("../util/profile_cleaner")
PATH_TO_BROWSERS = appData.path("browsers")
@@ -83,20 +85,24 @@ module.exports = {
launch: launcher.launch
getBrowsers: ->
- ## TODO: accept an options object which
- ## turns off getting electron browser?
+ debug("getBrowsers")
launcher.detect()
.then (browsers = []) ->
- version = process.versions.chrome or ""
+ debug("found browsers %o", { browsers })
- ## the internal version of Electron, which won't be detected by `launcher`
- browsers.concat({
+ version = process.versions.chrome or ""
+ majorVersion = parseInt(version.split(".")[0]) if version
+ electronBrowser = {
name: "electron"
family: "electron"
displayName: "Electron"
version: version
path: ""
- majorVersion: version.split(".")[0]
+ majorVersion: majorVersion
info: "Electron is the default browser that comes with Cypress. This is the browser that runs in headless mode. Selecting this browser is useful when debugging. The version number indicates the underlying Chromium version that Electron uses."
- })
+ }
+
+ ## the internal version of Electron, which won't be detected by `launcher`
+ debug("adding Electron browser with version %s", version)
+ browsers.concat(electronBrowser)
}
diff --git a/packages/server/lib/config.coffee b/packages/server/lib/config.coffee
index 1da615e547..3a33366918 100644
--- a/packages/server/lib/config.coffee
+++ b/packages/server/lib/config.coffee
@@ -1,10 +1,12 @@
_ = require("lodash")
+R = require("ramda")
+la = require("lazy-ass")
path = require("path")
+check = require("check-more-types")
Promise = require("bluebird")
deepDiff = require("return-deep-diff")
errors = require("./errors")
scaffold = require("./scaffold")
-findSystemNode = require("./util/find_system_node")
fs = require("./util/fs")
keys = require("./util/keys")
origin = require("./util/origin")
@@ -13,6 +15,7 @@ settings = require("./util/settings")
v = require("./util/validation")
debug = require("debug")("cypress:server:config")
pathHelpers = require("./util/path_helpers")
+findSystemNode = require("./util/find_system_node")
CYPRESS_ENV_PREFIX = "CYPRESS_"
CYPRESS_ENV_PREFIX_LENGTH = "CYPRESS_".length
@@ -47,6 +50,7 @@ folders = toWords """
videosFolder
"""
+# Public configuration properties, like "cypress.json" fields
configKeys = toWords """
animationDistanceThreshold fileServerFolder
baseUrl fixturesFolder
@@ -74,17 +78,25 @@ configKeys = toWords """
nodeVersion resolvedNodePath
"""
+# Deprecated and retired public configuration properties
breakingConfigKeys = toWords """
videoRecording
screenshotOnHeadlessFailure
trashAssetsBeforeHeadlessRuns
"""
+# Internal configuration properties the user should be able to overwrite
+systemConfigKeys = toWords """
+ browsers
+"""
+
CONFIG_DEFAULTS = {
port: null
hosts: null
morgan: true
baseUrl: null
+ # will be replaced by detected list of browsers
+ browsers: []
socketId: null
projectId: null
userAgent: null
@@ -137,7 +149,7 @@ validationRules = {
animationDistanceThreshold: v.isNumber
baseUrl: v.isFullyQualifiedUrl
blacklistHosts: v.isStringOrArrayOfStrings
- modifyObstructiveCode: v.isBoolean
+ browsers: v.isValidBrowserList
chromeWebSecurity: v.isBoolean
configFile: v.isStringOrFalse
defaultCommandTimeout: v.isNumber
@@ -147,6 +159,8 @@ validationRules = {
fixturesFolder: v.isStringOrFalse
ignoreTestFiles: v.isStringOrArrayOfStrings
integrationFolder: v.isString
+ modifyObstructiveCode: v.isBoolean
+ nodeVersion: v.isOneOf("default", "bundled", "system")
numTestsKeptInMemory: v.isNumber
pageLoadTimeout: v.isNumber
pluginsFile: v.isStringOrFalse
@@ -154,20 +168,19 @@ validationRules = {
reporter: v.isString
requestTimeout: v.isNumber
responseTimeout: v.isNumber
- testFiles: v.isStringOrArrayOfStrings
supportFile: v.isStringOrFalse
taskTimeout: v.isNumber
+ testFiles: v.isStringOrArrayOfStrings
trashAssetsBeforeRuns: v.isBoolean
userAgent: v.isString
- videoCompression: v.isNumberOrFalse
video: v.isBoolean
- videoUploadOnPasses: v.isBoolean
+ videoCompression: v.isNumberOrFalse
videosFolder: v.isString
+ videoUploadOnPasses: v.isBoolean
viewportHeight: v.isNumber
viewportWidth: v.isNumber
waitForAnimations: v.isBoolean
watchForFileChanges: v.isBoolean
- nodeVersion: v.isOneOf("default", "bundled", "system")
}
convertRelativeToAbsolutePaths = (projectRoot, obj, defaults = {}) ->
@@ -219,7 +232,8 @@ module.exports = {
_.includes(names, value)
whitelist: (obj = {}) ->
- _.pick(obj, configKeys.concat(breakingConfigKeys))
+ propertyNames = configKeys.concat(breakingConfigKeys).concat(systemConfigKeys)
+ _.pick(obj, propertyNames)
get: (projectRoot, options = {}) ->
Promise.all([
@@ -236,12 +250,14 @@ module.exports = {
})
set: (obj = {}) ->
+ debug("setting config object")
{projectRoot, projectName, config, envFile, options} = obj
## just force config to be an object
## so we dont have to do as much
## work in our tests
config ?= {}
+ debug("config is %o", config)
## flatten the object's properties
## into the master config object
@@ -255,10 +271,12 @@ module.exports = {
resolved = {}
_.extend config, _.pick(options, "configFile", "morgan", "isTextTerminal", "socketId", "report", "browsers")
+ debug("merged config with options, got %o", config)
_
.chain(@whitelist(options))
.omit("env")
+ .omit("browsers")
.each (val, key) ->
resolved[key] = "cli"
config[key] = val
@@ -319,40 +337,93 @@ module.exports = {
obj = _.clone(config)
obj.resolved = @resolveConfigValues(config, defaults, resolved)
+ debug("resolved config is %o", obj.resolved.browsers)
return obj
+ # Given an object "resolvedObj" and a list of overrides in "obj"
+ # marks all properties from "obj" inside "resolvedObj" using
+ # {value: obj.val, from: "plugin"}
+ setPluginResolvedOn: (resolvedObj, obj) ->
+ _.each obj, (val, key) =>
+ if _.isObject(val) && !_.isArray(val)
+ ## recurse setting overrides
+ ## inside of this nested objected
+ @setPluginResolvedOn(resolvedObj[key], val)
+ else
+ ## override the resolved value
+ resolvedObj[key] = {
+ value: val
+ from: "plugin"
+ }
+
updateWithPluginValues: (cfg, overrides = {}) ->
## diff the overrides with cfg
## including nested objects (env)
- diffs = deepDiff(cfg, overrides, true)
+ debug("starting config %o", cfg)
+ debug("overrides %o", overrides)
- setResolvedOn = (resolvedObj, obj) ->
- _.each obj, (val, key) ->
- if _.isObject(val) && !_.isArray(val)
- ## recurse setting overrides
- ## inside of this nested objected
- setResolvedOn(resolvedObj[key], val)
- else
- ## override the resolved value
- resolvedObj[key] = {
- value: val
- from: "plugin"
- }
+ # make sure every option returned from the plugins file
+ # passes our validation functions
+ validate overrides, (errMsg) ->
+ if cfg.pluginsFile and cfg.projectRoot
+ relativePluginsPath = path.relative(cfg.projectRoot, cfg.pluginsFile)
+ errors.throw("PLUGINS_CONFIG_VALIDATION_ERROR", relativePluginsPath, errMsg)
+ else
+ errors.throw("CONFIG_VALIDATION_ERROR", errMsg)
+
+ originalResolvedBrowsers = cfg && cfg.resolved && cfg.resolved.browsers && R.clone(cfg.resolved.browsers)
+ if not originalResolvedBrowsers
+ # have something to resolve with if plugins return nothing
+ originalResolvedBrowsers = {
+ value: cfg.browsers
+ from: "default"
+ }
+
+ diffs = deepDiff(cfg, overrides, true)
+ debug("config diffs %o", diffs)
+
+ userBrowserList = diffs && diffs.browsers && R.clone(diffs.browsers)
+ if userBrowserList
+ debug("user browser list %o", userBrowserList)
## for each override go through
## and change the resolved values of cfg
## to point to the plugin
- setResolvedOn(cfg.resolved, diffs)
+ if diffs
+ @setPluginResolvedOn(cfg.resolved, diffs)
+ debug("resolved config object %o", cfg.resolved)
## merge cfg into overrides
- _.defaultsDeep(diffs, cfg)
+ merged = _.defaultsDeep(diffs, cfg)
+ debug("merged config object %o", merged)
+ # the above _.defaultsDeep combines arrays,
+ # if diffs.browsers = [1] and cfg.browsers = [1, 2]
+ # then the merged result merged.browsers = [1, 2]
+ # which is NOT what we want
+ if Array.isArray(userBrowserList) and userBrowserList.length
+ merged.browsers = userBrowserList
+ merged.resolved.browsers.value = userBrowserList
+
+ if overrides.browsers == null
+ # null breaks everything when merging lists
+ debug("replacing null browsers with original list %o", originalResolvedBrowsers)
+ merged.browsers = cfg.browsers
+ if originalResolvedBrowsers
+ merged.resolved.browsers = originalResolvedBrowsers
+
+ debug("merged plugins config %o", merged)
+ return merged
+
+ # combines the default configuration object with values specified in the
+ # configuration file like "cypress.json". Values in configuration file
+ # overwrite the defaults.
resolveConfigValues: (config, defaults, resolved = {}) ->
- ## pick out only the keys found in configKeys
+ ## pick out only known configuration keys
_
.chain(config)
- .pick(configKeys)
+ .pick(configKeys.concat(systemConfigKeys))
.mapValues (val, key) ->
source = (s) ->
{
@@ -366,10 +437,12 @@ module.exports = {
r
else
source(r)
- when not _.isEqual(config[key], defaults[key])
- source("config")
- else
+ # "browsers" list is special, since it is dynamic by default
+ # and can only be ovewritten via plugins file
+ when _.isEqual(config[key], defaults[key]) or key == "browsers"
source("default")
+ else
+ source("config")
.value()
# instead of the built-in Node process, specify a path to 3rd party Node
diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee
index ca7f54ff99..b89accac2b 100644
--- a/packages/server/lib/errors.coffee
+++ b/packages/server/lib/errors.coffee
@@ -605,6 +605,7 @@ getMsgByType = (type, arg1 = {}, arg2) ->
Fix the error in your code and re-run your tests.
"""
+ # happens when there is an error in configuration file like "cypress.json"
when "SETTINGS_VALIDATION_ERROR"
filePath = "`#{arg1}`"
"""
@@ -612,6 +613,16 @@ getMsgByType = (type, arg1 = {}, arg2) ->
#{chalk.yellow(arg2)}
"""
+ # happens when there is an invalid config value returnes from the
+ # project's plugins file like "cypress/plugins.index.js"
+ when "PLUGINS_CONFIG_VALIDATION_ERROR"
+ filePath = "`#{arg1}`"
+ """
+ An invalid configuration value returned from the plugins file: #{chalk.blue(filePath)}
+
+ #{chalk.yellow(arg2)}
+ """
+ # general configuration error not-specific to configuration or plugins files
when "CONFIG_VALIDATION_ERROR"
"""
We found an invalid configuration value:
diff --git a/packages/server/lib/gui/events.coffee b/packages/server/lib/gui/events.coffee
index 4b5150f444..72a5ad2783 100644
--- a/packages/server/lib/gui/events.coffee
+++ b/packages/server/lib/gui/events.coffee
@@ -2,6 +2,7 @@ _ = require("lodash")
ipc = require("electron").ipcMain
shell = require("electron").shell
debug = require('debug')('cypress:server:events')
+pluralize = require("pluralize")
dialog = require("./dialog")
pkg = require("./package")
logs = require("./logs")
@@ -189,6 +190,8 @@ handleEvent = (options, bus, event, id, type, arg) ->
.catch(sendErr)
when "open:project"
+ debug("open:project")
+
onSettingsChanged = ->
bus.emit("config:changed")
@@ -208,6 +211,7 @@ handleEvent = (options, bus, event, id, type, arg) ->
browsers.getAllBrowsersWith(options.browser)
.then (browsers = []) ->
+ debug("setting found %s on the config", pluralize("browser", browsers.length, true))
options.config = _.assign(options.config, { browsers })
.then ->
chromePolicyCheck.run (err) ->
diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js
index 4173fc3abb..67df124293 100644
--- a/packages/server/lib/modes/run.js
+++ b/packages/server/lib/modes/run.js
@@ -13,7 +13,7 @@ const recordMode = require('./record')
const errors = require('../errors')
const Project = require('../project')
const Reporter = require('../reporter')
-const browsers = require('../browsers')
+const browserUtils = require('../browsers')
const openProject = require('../open_project')
const videoCapture = require('../video_capture')
const fs = require('../util/fs')
@@ -447,7 +447,7 @@ const getProjectId = Promise.method((project, id) => {
})
const getDefaultBrowserOptsByFamily = (browser, project, writeVideoFrame) => {
- la(browsers.isBrowserFamily(browser.family), 'invalid browser family in', browser)
+ la(browserUtils.isBrowserFamily(browser.family), 'invalid browser family in', browser)
if (browser.family === 'electron') {
return getElectronProps(browser.isHeaded, project, writeVideoFrame)
@@ -534,17 +534,19 @@ const onWarning = (err) => {
console.log(chalk.yellow(err.message))
}
-const openProjectCreate = (projectRoot, socketId, options) => {
+const openProjectCreate = (projectRoot, socketId, args) => {
// now open the project to boot the server
// putting our web client app in headless mode
// - NO display server logs (via morgan)
// - YES display reporter results (via mocha reporter)
- return openProject
- .create(projectRoot, options, {
+ const options = {
socketId,
morgan: false,
report: true,
- isTextTerminal: options.isTextTerminal,
+ isTextTerminal: args.isTextTerminal,
+ // pass the list of browsers we have detected when opening a project
+ // to give user's plugins file a chance to change it
+ browsers: args.browsers,
onWarning,
onError (err) {
console.log('')
@@ -558,7 +560,10 @@ const openProjectCreate = (projectRoot, socketId, options) => {
return openProject.emit('exitEarlyWithErr', err.message)
},
- })
+ }
+
+ return openProject
+ .create(projectRoot, args, options)
.catch({ portInUse: true }, (err) => {
// TODO: this needs to move to emit exitEarly
// so we record the failure in CI
@@ -587,7 +592,7 @@ const createAndOpenProject = function (socketId, options) {
}
const removeOldProfiles = () => {
- return browsers.removeOldProfiles()
+ return browserUtils.removeOldProfiles()
.catch((err) => {
// dont make removing old browsers profiles break the build
return errors.warning('CANNOT_REMOVE_OLD_BROWSER_PROFILES', err.stack)
@@ -1275,89 +1280,101 @@ module.exports = {
// ensure the project exists
// and open up the project
- return createAndOpenProject(socketId, options)
- .then(({ project, projectId, config }) => {
- debug('project created and opened with config %o', config)
+ return browserUtils.getAllBrowsersWith()
+ .then((browsers) => {
+ debug('found all system browsers %o', browsers)
+ options.browsers = browsers
- // if we have a project id and a key but record hasnt been given
- recordMode.warnIfProjectIdButNoRecordOption(projectId, options)
- recordMode.throwIfRecordParamsWithoutRecording(record, ciBuildId, parallel, group)
+ return createAndOpenProject(socketId, options)
+ .then(({ project, projectId, config }) => {
+ debug('project created and opened with config %o', config)
- if (record) {
- recordMode.throwIfNoProjectId(projectId, settings.configFile(options))
- recordMode.throwIfIncorrectCiBuildIdUsage(ciBuildId, parallel, group)
- recordMode.throwIfIndeterminateCiBuildId(ciBuildId, parallel, group)
- }
-
- return Promise.all([
- system.info(),
- browsers.ensureAndGetByNameOrPath(browserName),
- this.findSpecs(config, specPattern),
- trashAssets(config),
- removeOldProfiles(),
- ])
- .spread((sys = {}, browser = {}, specs = []) => {
- // return only what is return to the specPattern
- if (specPattern) {
- specPattern = specsUtil.getPatternRelativeToProjectRoot(specPattern, projectRoot)
- }
-
- if (!specs.length) {
- errors.throw('NO_SPECS_FOUND', config.integrationFolder, specPattern)
- }
-
- if (browser.family === 'chrome') {
- chromePolicyCheck.run(onWarning)
- }
-
- const runAllSpecs = ({ beforeSpecRun, afterSpecRun, runUrl, parallel }) => {
- return this.runSpecs({
- beforeSpecRun,
- afterSpecRun,
- projectRoot,
- specPattern,
- socketId,
- parallel,
- browser,
- project,
- runUrl,
- group,
- config,
- specs,
- sys,
- videosFolder: config.videosFolder,
- video: config.video,
- videoCompression: config.videoCompression,
- videoUploadOnPasses: config.videoUploadOnPasses,
- exit: options.exit,
- headed: options.headed,
- outputPath: options.outputPath,
- })
- .tap(renderSummaryTable(runUrl))
- }
+ // if we have a project id and a key but record hasnt been given
+ recordMode.warnIfProjectIdButNoRecordOption(projectId, options)
+ recordMode.throwIfRecordParamsWithoutRecording(record, ciBuildId, parallel, group)
if (record) {
- const { projectName } = config
-
- return recordMode.createRunAndRecordSpecs({
- key,
- sys,
- specs,
- group,
- browser,
- parallel,
- ciBuildId,
- projectId,
- projectRoot,
- projectName,
- specPattern,
- runAllSpecs,
- })
+ recordMode.throwIfNoProjectId(projectId, settings.configFile(options))
+ recordMode.throwIfIncorrectCiBuildIdUsage(ciBuildId, parallel, group)
+ recordMode.throwIfIndeterminateCiBuildId(ciBuildId, parallel, group)
}
- // not recording, can't be parallel
- return runAllSpecs({
- parallel: false,
+ // user code might have modified list of allowed browsers
+ // but be defensive about it
+ const userBrowsers = _.get(config, 'resolved.browsers.value', browsers)
+
+ // all these operations are independent and should be run in parallel to
+ // speed the initial booting time
+ return Promise.all([
+ system.info(),
+ browserUtils.ensureAndGetByNameOrPath(browserName, false, userBrowsers),
+ this.findSpecs(config, specPattern),
+ trashAssets(config),
+ removeOldProfiles(),
+ ])
+ .spread((sys = {}, browser = {}, specs = []) => {
+ // return only what is return to the specPattern
+ if (specPattern) {
+ specPattern = specsUtil.getPatternRelativeToProjectRoot(specPattern, projectRoot)
+ }
+
+ if (!specs.length) {
+ errors.throw('NO_SPECS_FOUND', config.integrationFolder, specPattern)
+ }
+
+ if (browser.family === 'chrome') {
+ chromePolicyCheck.run(onWarning)
+ }
+
+ const runAllSpecs = ({ beforeSpecRun, afterSpecRun, runUrl, parallel }) => {
+ return this.runSpecs({
+ beforeSpecRun,
+ afterSpecRun,
+ projectRoot,
+ specPattern,
+ socketId,
+ parallel,
+ browser,
+ project,
+ runUrl,
+ group,
+ config,
+ specs,
+ sys,
+ videosFolder: config.videosFolder,
+ video: config.video,
+ videoCompression: config.videoCompression,
+ videoUploadOnPasses: config.videoUploadOnPasses,
+ exit: options.exit,
+ headed: options.headed,
+ outputPath: options.outputPath,
+ })
+ .tap(renderSummaryTable(runUrl))
+ }
+
+ if (record) {
+ const { projectName } = config
+
+ return recordMode.createRunAndRecordSpecs({
+ key,
+ sys,
+ specs,
+ group,
+ browser,
+ parallel,
+ ciBuildId,
+ projectId,
+ projectRoot,
+ projectName,
+ specPattern,
+ runAllSpecs,
+ })
+ }
+
+ // not recording, can't be parallel
+ return runAllSpecs({
+ parallel: false,
+ })
})
})
})
diff --git a/packages/server/lib/open_project.coffee b/packages/server/lib/open_project.coffee
index 26173aef40..3f58e71d29 100644
--- a/packages/server/lib/open_project.coffee
+++ b/packages/server/lib/open_project.coffee
@@ -11,7 +11,7 @@ browsers = require("./browsers")
specsUtil = require("./util/specs")
preprocessor = require("./plugins/preprocessor")
-create = ->
+moduleFactory = ->
openProject = null
relaunchBrowser = null
specsWatcher = null
@@ -192,6 +192,9 @@ create = ->
@closeOpenProjectAndBrowsers()
create: (path, args = {}, options = {}) ->
+ debug("open_project create %s", path)
+ debug("and options %o", options)
+
## store the currently open project
openProject = Project(path)
@@ -209,10 +212,11 @@ create = ->
## open the project and return
## the config for the project instance
debug("opening project %s", path)
+ debug("and options %o", options)
openProject.open(options)
.return(@)
}
-module.exports = create()
-module.exports.Factory = create
+module.exports = moduleFactory()
+module.exports.Factory = moduleFactory
diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js
index 4f37e248f1..8962804a96 100644
--- a/packages/server/lib/plugins/child/run_plugins.js
+++ b/packages/server/lib/plugins/child/run_plugins.js
@@ -1,3 +1,6 @@
+// this module is responsible for loading the plugins file
+// and running the exported function to register event handlers
+// and executing any tasks that the plugin registers
const _ = require('lodash')
const debug = require('debug')('cypress:server:plugins:child')
const Promise = require('bluebird')
@@ -146,6 +149,8 @@ module.exports = (ipc, pluginsFile) => {
}
ipc.on('load', (config) => {
+ debug('plugins load file "%s"', pluginsFile)
+ debug('passing config %o', config)
load(ipc, config, pluginsFile)
})
diff --git a/packages/server/lib/project.coffee b/packages/server/lib/project.coffee
index 93d174ba37..c148e7c309 100644
--- a/packages/server/lib/project.coffee
+++ b/packages/server/lib/project.coffee
@@ -57,6 +57,7 @@ class Project extends EE
open: (options = {}) ->
debug("opening project instance %s", @projectRoot)
+ debug("project open options %o", options)
@server = Server()
_.defaults options, {
@@ -87,13 +88,17 @@ class Project extends EE
## we try to load it and it's not there. We must do this here
## else initialing the plugins will instantly fail.
if cfg.pluginsFile
+ debug("scaffolding with plugins file %s", cfg.pluginsFile)
scaffold.plugins(path.dirname(cfg.pluginsFile), cfg)
.then (cfg) =>
@_initPlugins(cfg, options)
.then (modifiedCfg) ->
- debug("plugin config yielded:", modifiedCfg)
+ debug("plugin config yielded: %o", modifiedCfg)
- return config.updateWithPluginValues(cfg, modifiedCfg)
+ updatedConfig = config.updateWithPluginValues(cfg, modifiedCfg)
+ debug("updated config: %o", updatedConfig)
+
+ return updatedConfig
.then (cfg) =>
@server.open(cfg, @, options.onWarning)
.spread (port, warning) =>
@@ -303,8 +308,10 @@ class Project extends EE
_.pick(@, "spec", "browser")
setBrowsers: (browsers = []) ->
+ debug("getting config before setting browsers %o", browsers)
@getConfig()
.then (cfg) ->
+ debug("setting config browsers to %o", browsers)
cfg.browsers = browsers
getAutomation: ->
@@ -552,7 +559,7 @@ class Project extends EE
)
@getProjectStatus = (clientProject) ->
- debug("get project status for", clientProject.id, clientProject.path)
+ debug("get project status for client id %s at path %s", clientProject.id, clientProject.path)
if not clientProject.id
debug("no project id")
diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee
index a442dd3c42..fb421f6858 100644
--- a/packages/server/lib/server.coffee
+++ b/packages/server/lib/server.coffee
@@ -126,6 +126,7 @@ class Server
e
open: (config = {}, project, onWarning) ->
+ debug("server open")
la(_.isPlainObject(config), "expected plain config object", config)
Promise.try =>
diff --git a/packages/server/lib/util/validation.js b/packages/server/lib/util/validation.js
index 2651858ef3..b2b53f2ce3 100644
--- a/packages/server/lib/util/validation.js
+++ b/packages/server/lib/util/validation.js
@@ -11,6 +11,9 @@
*/
const _ = require('lodash')
const errors = require('../errors')
+const debug = require('debug')('cypress:server:validation')
+const is = require('check-more-types')
+const { commaListsOr } = require('common-tags')
// # validation functions take a key and a value and should:
// # - return true if it passes validation
@@ -18,6 +21,13 @@ const errors = require('../errors')
const str = JSON.stringify
+/**
+ * Forms good Markdown-like string message.
+ * @param {string} key - The key that caused the error
+ * @param {string} type - The expected type name
+ * @param {any} value - The actual value
+ * @returns {string} Formatted error message
+*/
const errMsg = (key, value, type) => {
return `Expected \`${key}\` to be ${type}. Instead the value was: \`${str(
value
@@ -40,7 +50,75 @@ const { isArray } = _
const isNumber = _.isFinite
const { isString } = _
+/**
+ * Validates a single browser object.
+ * @returns {string|true} Returns `true` if the object is matching browser object schema. Returns an error message if it does not.
+ */
+const isValidBrowser = (browser) => {
+ if (!is.unemptyString(browser.name)) {
+ return errMsg('name', browser, 'a non-empty string')
+ }
+
+ const knownBrowserFamilies = ['electron', 'chrome', 'firefox']
+
+ if (!is.oneOf(knownBrowserFamilies)(browser.family)) {
+ return errMsg('family', browser, commaListsOr`either ${knownBrowserFamilies}`)
+ }
+
+ if (!is.unemptyString(browser.displayName)) {
+ return errMsg('displayName', browser, 'a non-empty string')
+ }
+
+ if (!is.unemptyString(browser.version)) {
+ return errMsg('version', browser, 'a non-empty string')
+ }
+
+ if (!is.string(browser.path)) {
+ return errMsg('path', browser, 'a string')
+ }
+
+ if (!is.string(browser.majorVersion) && !is.positive(browser.majorVersion)) {
+ return errMsg('majorVersion', browser, 'a string or a positive number')
+ }
+
+ return true
+}
+
+/**
+ * Validates the list of browsers.
+ */
+const isValidBrowserList = (key, browsers) => {
+ debug('browsers %o', browsers)
+ if (!browsers) {
+ return 'Missing browsers list'
+ }
+
+ if (!Array.isArray(browsers)) {
+ debug('browsers is not an array', typeof browsers)
+
+ return 'Browsers should be an array'
+ }
+
+ if (!browsers.length) {
+ return 'Expected at list one browser'
+ }
+
+ for (let k = 0; k < browsers.length; k += 1) {
+ const err = isValidBrowser(browsers[k])
+
+ if (err !== true) {
+ return `Found an error while validating the \`browsers\` list. ${err}`
+ }
+ }
+
+ return true
+}
+
module.exports = {
+ isValidBrowser,
+
+ isValidBrowserList,
+
isNumber (key, value) {
if (value == null || isNumber(value)) {
return true
diff --git a/packages/server/test/e2e/3_config_spec.coffee b/packages/server/test/e2e/3_config_spec.coffee
index 5b6ad6d6d0..0e239de98c 100644
--- a/packages/server/test/e2e/3_config_spec.coffee
+++ b/packages/server/test/e2e/3_config_spec.coffee
@@ -1,4 +1,8 @@
e2e = require("../support/helpers/e2e")
+Fixtures = require("../support/helpers/fixtures")
+
+configWithInvalidViewport = Fixtures.projectPath("config-with-invalid-viewport")
+configWithInvalidBrowser = Fixtures.projectPath("config-with-invalid-browser")
describe "e2e config", ->
e2e.setup({
@@ -33,3 +37,21 @@ describe "e2e config", ->
snapshot: true
expectedExitCode: 1
})
+
+ it "catches invalid viewportWidth in the configuration file", ->
+ # the test project has cypress.json with a bad setting
+ # which should show an error and exit
+ e2e.exec(@, {
+ project: configWithInvalidViewport
+ expectedExitCode: 1
+ snapshot: true
+ })
+
+ it "catches invalid browser in the configuration file", ->
+ # the test project has cypress.json with a bad browser
+ # which should show an error and exit
+ e2e.exec(@, {
+ project: configWithInvalidBrowser
+ expectedExitCode: 1
+ snapshot: true
+ })
diff --git a/packages/server/test/e2e/3_plugins_spec.coffee b/packages/server/test/e2e/3_plugins_spec.coffee
index cab9d1cfe0..5db335c917 100644
--- a/packages/server/test/e2e/3_plugins_spec.coffee
+++ b/packages/server/test/e2e/3_plugins_spec.coffee
@@ -5,10 +5,14 @@ Fixtures = require("../support/helpers/fixtures")
pluginExtension = Fixtures.projectPath("plugin-extension")
pluginConfig = Fixtures.projectPath("plugin-config")
+pluginFilterBrowsers = Fixtures.projectPath("plugin-filter-browsers")
workingPreprocessor = Fixtures.projectPath("working-preprocessor")
pluginsAsyncError = Fixtures.projectPath("plugins-async-error")
pluginsAbsolutePath = Fixtures.projectPath("plugins-absolute-path")
pluginAfterScreenshot = Fixtures.projectPath("plugin-after-screenshot")
+pluginReturnsBadConfig = Fixtures.projectPath("plugin-returns-bad-config")
+pluginReturnsEmptyBrowsersList = Fixtures.projectPath("plugin-returns-empty-browsers-list")
+pluginReturnsInvalidBrowser = Fixtures.projectPath("plugin-returns-invalid-browser")
describe "e2e plugins", ->
e2e.setup()
@@ -42,6 +46,43 @@ describe "e2e plugins", ->
expectedExitCode: 0
})
+ it "catches invalid viewportWidth returned from plugins", ->
+ # the test project returns config object with a bad value
+ e2e.exec(@, {
+ project: pluginReturnsBadConfig
+ expectedExitCode: 1
+ snapshot: true
+ })
+
+ it "catches invalid browsers list returned from plugins", ->
+ e2e.exec(@, {
+ project: pluginReturnsEmptyBrowsersList
+ expectedExitCode: 1
+ snapshot: true
+ })
+
+ it "catches invalid browser returned from plugins", ->
+ e2e.exec(@, {
+ project: pluginReturnsInvalidBrowser
+ expectedExitCode: 1
+ snapshot: true
+ })
+
+ it "can filter browsers from config", ->
+ e2e.exec(@, {
+ project: pluginFilterBrowsers
+ # the test project filters available browsers
+ # and returns a list with JUST Electron browser
+ # and we ask to run in Chrome
+ # thus the test should fail
+ browser: "chrome"
+ expectedExitCode: 1
+ snapshot: true
+ # we are interested in the actual filtered available browser name
+ # which should be "electron"
+ normalizeAvailableBrowsers: false
+ })
+
e2e.it "works with user extensions", {
browser: "chrome"
spec: "app_spec.coffee"
diff --git a/packages/server/test/integration/cypress_spec.coffee b/packages/server/test/integration/cypress_spec.coffee
index 671c076173..135b799d60 100644
--- a/packages/server/test/integration/cypress_spec.coffee
+++ b/packages/server/test/integration/cypress_spec.coffee
@@ -1,5 +1,6 @@
require("../spec_helper")
+R = require("ramda")
_ = require("lodash")
os = require("os")
cp = require("child_process")
@@ -12,6 +13,7 @@ commitInfo = require("@cypress/commit-info")
Fixtures = require("../support/helpers/fixtures")
snapshot = require("snap-shot-it")
stripAnsi = require("strip-ansi")
+debug = require("debug")("test")
pkg = require("@packages/root")
launcher = require("@packages/launcher")
extension = require("@packages/extension")
@@ -42,6 +44,7 @@ browserUtils = require("#{root}lib/browsers/utils")
chromeBrowser = require("#{root}lib/browsers/chrome")
openProject = require("#{root}lib/open_project")
env = require("#{root}lib/util/env")
+v = require("#{root}lib/util/validation")
system = require("#{root}lib/util/system")
appData = require("#{root}lib/util/app_data")
formStatePath = require("#{root}lib/util/saved_state").formStatePath
@@ -75,7 +78,9 @@ ELECTRON_BROWSER = {
name: "electron"
family: "electron"
displayName: "Electron"
- path: ""
+ path: "",
+ version: "99.101.1234",
+ majorVersion: 99
}
previousCwd = process.cwd()
@@ -129,6 +134,9 @@ describe "lib/cypress", ->
sinon.spy(errors, "logException")
sinon.spy(console, "log")
+ # to make sure our Electron browser mock object passes validation during tests
+ process.versions.chrome = ELECTRON_BROWSER.version
+
@expectExitWith = (code) =>
expect(process.exit).to.be.calledWith(code)
@@ -146,6 +154,30 @@ describe "lib/cypress", ->
## we spawn is closed down
openProject.close()
+ context "test browsers", ->
+ # sanity checks to make sure the browser objects we pass during tests
+ # all pass the internal validation function
+ it "has valid browsers", ->
+ expect(v.isValidBrowserList("browsers", TYPICAL_BROWSERS)).to.be.true
+
+ it "has valid electron browser", ->
+ expect(v.isValidBrowserList("browsers", [ELECTRON_BROWSER])).to.be.true
+
+ it "allows browser major to be a number", ->
+ browser = {
+ name: 'Edge Beta',
+ family: 'chrome',
+ displayName: 'Edge Beta',
+ version: '80.0.328.2',
+ path: '/some/path',
+ majorVersion: 80
+ }
+ expect(v.isValidBrowserList("browsers", [browser])).to.be.true
+
+ it "validates returned list", ->
+ browserUtils.getBrowsers().then (list) ->
+ expect(v.isValidBrowserList("browsers", list)).to.be.true
+
context "--get-key", ->
it "writes out key and exits on success", ->
Promise.all([
@@ -829,13 +861,20 @@ describe "lib/cypress", ->
.then =>
args = browserUtils.launch.firstCall.args
- expect(args[0]).to.eq(_.find(TYPICAL_BROWSERS, { name: "chrome" }))
+ # when we work with the browsers we set a few extra flags
+ chrome = _.find(TYPICAL_BROWSERS, { name: "chrome" })
+ launchedChrome = R.merge(chrome, {
+ isHeadless: false,
+ isHeaded: true
+ })
+
+ expect(args[0], "found and used Chrome").to.deep.eq(launchedChrome)
browserArgs = args[2]
- expect(browserArgs).to.have.length(7)
+ expect(browserArgs, "two arguments to Chrome").to.have.length(7)
- expect(browserArgs.slice(0, 4)).to.deep.eq([
+ expect(browserArgs.slice(0, 4), "arguments to Chrome").to.deep.eq([
"chrome", "foo", "bar", "baz"
])
@@ -1494,14 +1533,16 @@ describe "lib/cypress", ->
"--config-file=#{@filename}"
])
.then =>
+ debug("cypress started with config %s", @filename)
options = Events.start.firstCall.args[0]
+ debug("first call arguments %o", Events.start.firstCall.args)
Events.handleEvent(options, {}, {}, 123, "open:project", @pristinePath)
.then =>
- expect(@open).to.be.called
+ expect(@open, "open was called").to.be.called
fs.readJsonAsync(path.join(@pristinePath, @filename))
.then (json) =>
- expect(json).to.deep.equal({})
+ expect(json, "json file is empty").to.deep.equal({})
context "--cwd", ->
beforeEach ->
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress.json b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress.json
new file mode 100644
index 0000000000..71c2cfbbf0
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress.json
@@ -0,0 +1,12 @@
+{
+ "browsers": [
+ {
+ "name": "bad browser",
+ "family": "unknown family",
+ "displayName": "Bad browser",
+ "version": "no version",
+ "path": "/path/to",
+ "majorVersion": 123
+ }
+ ]
+}
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/fixtures/example.json b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/fixtures/example.json
new file mode 100644
index 0000000000..da18d9352a
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..346e8e5a9d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/integration/app_spec.coffee
@@ -0,0 +1 @@
+it "is true", ->
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/plugins/index.coffee
new file mode 100644
index 0000000000..9ff7a1ef3b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/plugins/index.coffee
@@ -0,0 +1 @@
+module.exports = (onFn, config) ->
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/support/commands.js b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/support/commands.js
new file mode 100644
index 0000000000..ca4d256f3e
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/support/index.js b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/support/index.js
new file mode 100644
index 0000000000..d68db96df2
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-browser/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress.json b/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress.json
new file mode 100644
index 0000000000..3e411ccd67
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress.json
@@ -0,0 +1,3 @@
+{
+ "viewportWidth": "foo"
+}
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..346e8e5a9d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress/integration/app_spec.coffee
@@ -0,0 +1 @@
+it "is true", ->
diff --git a/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress/plugins/index.coffee
new file mode 100644
index 0000000000..9ff7a1ef3b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/config-with-invalid-viewport/cypress/plugins/index.coffee
@@ -0,0 +1 @@
+module.exports = (onFn, config) ->
diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee
index 17e9e8e9ea..49f306c642 100644
--- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee
+++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/config_passing_spec.coffee
@@ -16,7 +16,8 @@ describe "Cypress static methods + props", ->
expect(browser.name).to.be.oneOf(["electron", "chrome", "canary", "chromium"])
expect(browser.displayName).to.be.oneOf(["Electron", "Chrome", "Canary", "Chromium"])
expect(browser.version).to.be.a("string")
- expect(browser.majorVersion).to.be.a("string")
+ # we are parsing major version, so it should be a number
+ expect(browser.majorVersion).to.be.a("number")
expect(browser.path).to.be.a("string")
switch browser.isHeadless
diff --git a/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress.json b/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..346e8e5a9d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress/integration/app_spec.coffee
@@ -0,0 +1 @@
+it "is true", ->
diff --git a/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress/plugins/index.coffee
new file mode 100644
index 0000000000..440aa8d5a3
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-filter-browsers/cypress/plugins/index.coffee
@@ -0,0 +1,18 @@
+debug = require("debug")("cypress:e2e")
+module.exports = (onFn, config) ->
+ debug("plugin file %s", __filename)
+ debug("received config with browsers %o", config.browsers)
+
+ if not Array.isArray(config.browsers)
+ throw new Error("Expected list of browsers in the config")
+ if config.browsers.length == 0
+ throw new Error("Expected at least 1 browser in the config")
+ electronBrowser = config.browsers.find (browser) -> browser.name == "electron"
+ if not electronBrowser
+ throw new Error("List of browsers passed into plugins does not include Electron browser")
+
+ changedConfig = {
+ browsers: [electronBrowser]
+ }
+ debug("returning only Electron browser from plugins %o", changedConfig)
+ return changedConfig
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress.json b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/fixtures/example.json b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/fixtures/example.json
new file mode 100644
index 0000000000..da18d9352a
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..346e8e5a9d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/integration/app_spec.coffee
@@ -0,0 +1 @@
+it "is true", ->
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/plugins/index.coffee
new file mode 100644
index 0000000000..36efbee450
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/plugins/index.coffee
@@ -0,0 +1,5 @@
+# returns object with invalid properties
+module.exports = (onFn, config) ->
+ {
+ viewportWidth: "foo"
+ }
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/support/commands.js b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/support/commands.js
new file mode 100644
index 0000000000..ca4d256f3e
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/support/index.js b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/support/index.js
new file mode 100644
index 0000000000..d68db96df2
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-bad-config/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress.json b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/fixtures/example.json b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/fixtures/example.json
new file mode 100644
index 0000000000..da18d9352a
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..346e8e5a9d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/integration/app_spec.coffee
@@ -0,0 +1 @@
+it "is true", ->
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/plugins/index.coffee
new file mode 100644
index 0000000000..1af987fcdc
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/plugins/index.coffee
@@ -0,0 +1,5 @@
+# returns invalid config - browsers list cannot be empty
+module.exports = (onFn, config) ->
+ {
+ browsers: []
+ }
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/support/commands.js b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/support/commands.js
new file mode 100644
index 0000000000..ca4d256f3e
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/support/index.js b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/support/index.js
new file mode 100644
index 0000000000..d68db96df2
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-empty-browsers-list/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress.json b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/fixtures/example.json b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/fixtures/example.json
new file mode 100644
index 0000000000..da18d9352a
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..346e8e5a9d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/integration/app_spec.coffee
@@ -0,0 +1 @@
+it "is true", ->
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee
new file mode 100644
index 0000000000..b1ac2d3696
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/plugins/index.coffee
@@ -0,0 +1,9 @@
+# returns invalid config with a browser that is invalid
+# (missing multiple properties)
+module.exports = (onFn, config) ->
+ {
+ browsers: [{
+ name: "browser name",
+ family: "chrome"
+ }]
+ }
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/support/commands.js b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/support/commands.js
new file mode 100644
index 0000000000..ca4d256f3e
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
diff --git a/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/support/index.js b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/support/index.js
new file mode 100644
index 0000000000..d68db96df2
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-returns-invalid-browser/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/packages/server/test/support/helpers/e2e.coffee b/packages/server/test/support/helpers/e2e.coffee
index 591fe83df3..ffaa7446fd 100644
--- a/packages/server/test/support/helpers/e2e.coffee
+++ b/packages/server/test/support/helpers/e2e.coffee
@@ -90,17 +90,26 @@ replaceUploadingResults = (orig, match..., offset, string) ->
return ret
normalizeStdout = (str, options = {}) ->
+ normalizeOptions = _.defaults({}, options, {normalizeAvailableBrowsers: true})
+
## remove all of the dynamic parts of stdout
## to normalize against what we expected
str = str
## /Users/jane/........../ -> //foo/bar/.projects/
## (Required when paths are printed outside of our own formatting)
.split(pathUpToProjectName).join("/foo/bar/.projects")
- .replace(availableBrowsersRe, "$1browser1, browser2, browser3")
+
+ if normalizeOptions.normalizeAvailableBrowsers
+ # usually we are not interested in the browsers detected on this particular system
+ # but some tests might filter / change the list of browsers
+ # in that case the test should pass "normalizeAvailableBrowsers:false" as options
+ str = str.replace(availableBrowsersRe, "$1browser1, browser2, browser3")
+
+ str = str
.replace(browserNameVersionRe, replaceBrowserName)
## numbers in parenths
.replace(/\s\(\d+([ms]|ms)\)/g, "")
- ## 12:35 -> XX:XX
+ ## 12:35 -> XX:XX
.replace(/(\s+?)(\d+ms|\d+:\d+:?\d+)/g, replaceDurationInTables)
.replace(/(coffee|js)-\d{3}/g, "$1-456")
## Cypress: 2.1.0 -> Cypress: 1.2.3
diff --git a/packages/server/test/unit/config_spec.coffee b/packages/server/test/unit/config_spec.coffee
index 384a88ef37..b557213425 100644
--- a/packages/server/test/unit/config_spec.coffee
+++ b/packages/server/test/unit/config_spec.coffee
@@ -765,7 +765,8 @@ describe "lib/config", ->
projectId: { value: null, from: "default" },
port: { value: 1234, from: "cli" },
hosts: { value: null, from: "default" }
- blacklistHosts: { value: null, from: "default" }
+ blacklistHosts: { value: null, from: "default" },
+ browsers: { value: [], from: "default" },
userAgent: { value: null, from: "default" }
reporter: { value: "json", from: "cli" },
reporterOptions: { value: null, from: "default" },
@@ -833,6 +834,7 @@ describe "lib/config", ->
port: { value: 2020, from: "config" },
hosts: { value: null, from: "default" }
blacklistHosts: { value: null, from: "default" }
+ browsers: { value: [], from: "default" }
userAgent: { value: null, from: "default" }
reporter: { value: "spec", from: "default" },
reporterOptions: { value: null, from: "default" },
@@ -893,12 +895,85 @@ describe "lib/config", ->
}
})
+ context ".setPluginResolvedOn", ->
+ it "resolves an object with single property", ->
+ cfg = {}
+ obj = {
+ foo: "bar"
+ }
+ config.setPluginResolvedOn(cfg, obj)
+ expect(cfg).to.deep.eq({
+ foo: {
+ value: "bar",
+ from: "plugin"
+ }
+ })
+
+ it "resolves an object with multiple properties", ->
+ cfg = {}
+ obj = {
+ foo: "bar",
+ baz: [1, 2, 3]
+ }
+ config.setPluginResolvedOn(cfg, obj)
+ expect(cfg).to.deep.eq({
+ foo: {
+ value: "bar",
+ from: "plugin"
+ },
+ baz: {
+ value: [1, 2, 3],
+ from: "plugin"
+ }
+ })
+
+ it "resolves a nested object", ->
+ # we need at least the structure
+ cfg = {
+ foo: {
+ bar: 1
+ }
+ }
+ obj = {
+ foo: {
+ bar: 42
+ }
+ }
+ config.setPluginResolvedOn(cfg, obj)
+ expect(cfg, "foo.bar gets value").to.deep.eq({
+ foo: {
+ bar: {
+ value: 42,
+ from: "plugin"
+ }
+ }
+ })
+
+ context "_.defaultsDeep", ->
+ it "merges arrays", ->
+ # sanity checks to confirm how Lodash merges arrays in defaultsDeep
+ diffs = {
+ list: [1]
+ }
+ cfg = {
+ list: [1, 2]
+ }
+ merged = _.defaultsDeep({}, diffs, cfg)
+ expect(merged, "arrays are combined").to.deep.eq({
+ list: [1, 2]
+ })
+
context ".updateWithPluginValues", ->
it "is noop when no overrides", ->
expect(config.updateWithPluginValues({foo: 'bar'}, null)).to.deep.eq({
foo: 'bar'
})
+ it "is noop with empty overrides", ->
+ expect(config.updateWithPluginValues({foo: 'bar'}, {})).to.deep.eq({
+ foo: 'bar'
+ })
+
it "updates resolved config values and returns config with overrides", ->
cfg = {
foo: "bar"
@@ -909,6 +984,7 @@ describe "lib/config", ->
a: "a"
b: "b"
}
+ # previously resolved values
resolved: {
foo: { value: "bar", from: "default" }
baz: { value: "quux", from: "cli" }
@@ -953,6 +1029,117 @@ describe "lib/config", ->
}
})
+ it "keeps the list of browsers if the plugins returns empty object", ->
+ browser = {
+ name: "fake browser name",
+ family: "chrome",
+ displayName: "My browser",
+ version: "x.y.z",
+ path: "/path/to/browser",
+ majorVersion: "x"
+ }
+
+ cfg = {
+ browsers: [browser],
+ resolved: {
+ browsers: {
+ value: [browser],
+ from: "default"
+ }
+ }
+ }
+
+ overrides = {}
+
+ expect(config.updateWithPluginValues(cfg, overrides)).to.deep.eq({
+ browsers: [browser],
+ resolved: {
+ browsers: {
+ value: [browser],
+ from: "default"
+ }
+ }
+ })
+
+ it "catches browsers=null returned from plugins", ->
+ browser = {
+ name: "fake browser name",
+ family: "chrome",
+ displayName: "My browser",
+ version: "x.y.z",
+ path: "/path/to/browser",
+ majorVersion: "x"
+ }
+
+ cfg = {
+ browsers: [browser],
+ resolved: {
+ browsers: {
+ value: [browser],
+ from: "default"
+ }
+ }
+ }
+
+ overrides = {
+ browsers: null
+ }
+
+ sinon.stub(errors, "throw")
+ config.updateWithPluginValues(cfg, overrides)
+ expect(errors.throw).to.have.been.calledWith("CONFIG_VALIDATION_ERROR")
+
+ it "allows user to filter browsers", ->
+ browserOne = {
+ name: "fake browser name",
+ family: "chrome",
+ displayName: "My browser",
+ version: "x.y.z",
+ path: "/path/to/browser",
+ majorVersion: "x"
+ }
+ browserTwo = {
+ name: "fake electron",
+ family: "electron",
+ displayName: "Electron",
+ version: "x.y.z",
+ # Electron browser is built-in, no external path
+ path: "",
+ majorVersion: "x"
+ }
+
+ cfg = {
+ browsers: [browserOne, browserTwo],
+ resolved: {
+ browsers: {
+ value: [browserOne, browserTwo],
+ from: "default"
+ }
+ }
+ }
+
+ overrides = {
+ browsers: [browserTwo]
+ }
+
+ updated = config.updateWithPluginValues(cfg, overrides)
+ expect(updated.resolved, "resolved values").to.deep.eq({
+ browsers: {
+ value: [browserTwo],
+ from: 'plugin'
+ }
+ })
+
+ expect(updated, "all values").to.deep.eq({
+ browsers: [browserTwo],
+ resolved: {
+ browsers: {
+ value: [browserTwo],
+ from: 'plugin'
+ }
+ }
+ })
+
context ".parseEnv", ->
it "merges together env from config, env from file, env from process, and env from CLI", ->
sinon.stub(config, "getProcessEnvVars").returns({
diff --git a/packages/server/test/unit/project_spec.coffee b/packages/server/test/unit/project_spec.coffee
index 92ce844d07..f7a54ec3e9 100644
--- a/packages/server/test/unit/project_spec.coffee
+++ b/packages/server/test/unit/project_spec.coffee
@@ -215,7 +215,7 @@ describe "lib/project", ->
it.skip "watches cypress.json", ->
@server.open().bind(@).then ->
expect(Watchers::watch).to.be.calledWith("/Users/brian/app/cypress.json")
-
+
# TODO: skip this for now
it.skip "passes watchers to Socket.startListening", ->
options = {}
diff --git a/packages/server/test/unit/validation_spec.coffee b/packages/server/test/unit/validation_spec.coffee
index 28f031928c..68e6504c2c 100644
--- a/packages/server/test/unit/validation_spec.coffee
+++ b/packages/server/test/unit/validation_spec.coffee
@@ -3,6 +3,57 @@ snapshot = require("snap-shot-it")
v = require("#{root}lib/util/validation")
describe "lib/util/validation", ->
+ context "#isValidBrowserList", ->
+ it "does not allow empty or not browsers", ->
+ snapshot("undefined browsers", v.isValidBrowserList("browsers"))
+ snapshot("empty list of browsers", v.isValidBrowserList("browsers", []))
+ snapshot("browsers list with a string", v.isValidBrowserList("browsers", ["foo"]))
+
+ context "#isValidBrowser", ->
+ it "passes valid browsers and forms error messages for invalid ones", ->
+ browsers = [
+ # valid browser
+ {
+ name: "Chrome",
+ displayName: "Chrome Browser",
+ family: "chrome",
+ path: "/path/to/chrome",
+ version: "1.2.3",
+ majorVersion: 1
+ },
+ # another valid browser
+ {
+ name: "FF",
+ displayName: "Firefox",
+ family: "firefox",
+ path: "/path/to/firefox",
+ version: "1.2.3",
+ majorVersion: "1"
+ },
+ # Electron is a valid browser
+ {
+ name: "Electron",
+ displayName: "Electron",
+ family: "electron",
+ path: "",
+ version: "99.101.3",
+ majorVersion: 99
+ },
+ # invalid browser, missing displayName
+ {
+ name: "No display name",
+ family: "electron"
+ },
+ {
+ name: "bad family",
+ displayName: "Bad family browser",
+ family: "unknown family"
+ }
+ ]
+ # data-driven testing - computers snapshot value for each item in the list passed through the function
+ # https://github.com/bahmutov/snap-shot-it#data-driven-testing
+ snapshot.apply(null, [v.isValidBrowser].concat(browsers))
+
context "#isOneOf", ->
it "validates a string", ->
validate = v.isOneOf("foo", "bar")