feat: pass list of browsers to plugins file (#5068)

and allow project to customize the list of browsers
This commit is contained in:
Gleb Bahmutov
2019-11-19 09:02:17 -05:00
committed by GitHub
parent 7fc110dfb6
commit b03b25c258
63 changed files with 1349 additions and 307 deletions

View File

@@ -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

View File

@@ -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": {

View File

@@ -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', () => {

View File

@@ -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"

View File

@@ -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) => (
<span key={key}>
<span className='nested nested-obj'>
<span className='key'>{key}</span>
<span className='colon'>:</span>{' '}
{'{'}
{display(value)}
</span>
<span className='line'>{'}'}{getComma(hasComma)}</span>
<br />
</span>
)
const displayNestedArr = (key, value, hasComma) => (
<span key={key}>
<span className='nested nested-arr'>
<span className='key'>{key}</span>
<span className='colon'>:</span>{' '}
{'['}
{display(value)}
</span>
<span className='line'>{']'}{getComma(hasComma)}</span>
<br />
</span>
)
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 (
<div key={key} className='line'>
{getKey(key, isArray)}
{getColon(isArray)}
<Tooltip title={obj.from || ''} placement='right' className='cy-tooltip'>
<span className={obj.from}>
{getString(obj.value)}
{`${obj.value}`}
{getString(obj.value)}
</span>
</Tooltip>
{getComma(hasComma)}
</div>
<span className="line" key={name}>
<ObjectName name={name} dimmed={isNonenumerable} />
<span>:</span>
{!expanded && (
<>
<Tooltip title={from} placement='right' className='cy-tooltip'>
<span className={cn(from, 'key-value-pair-value')}>
<span>{formattedData}</span>
</span>
</Tooltip>
</>
)}
{expanded && Array.isArray(data) && (
<span> Array ({data.length})</span>
)}
</span>
)
}
const getKey = (key, isArray) => {
return isArray ? '' : <span className='key'>{key}</span>
ObjectLabel.defaultProps = {
data: 'undefined',
}
const getColon = (isArray) => {
return isArray ? '' : <span className="colon">:{' '}</span>
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 ? <span className='comma'>,</span> : ''
const from = computeFromValue(name, path)
return (
<ObjectLabel
name={name}
data={data}
expanded={expanded}
from={from}
isNonenumerable={isNonenumerable}
/>
)
}
const data = _.reduce(obj, (acc, value, key) => Object.assign(acc, {
[key]: value.value,
}), {})
return (
<div className="config-vars">
<span>{'{'}</span>
<ObjectInspector data={data} expandLevel={1} nodeRenderer={renderNode} />
<span>{'}'}</span>
</div>
)
}
const openHelp = (e) => {
@@ -146,16 +136,12 @@ const Configuration = observer(({ project }) => (
<td>set from CLI arguments</td>
</tr>
<tr className='config-keys'>
<td><span className='plugin'>plugin</span></td>
<td><span className='plugins'>plugin</span></td>
<td>set from plugin file</td>
</tr>
</tbody>
</table>
<pre className='config-vars'>
{'{'}
{display(project.resolvedConfig)}
{'}'}
</pre>
<ConfigDisplay data={project.resolvedConfig} />
</div>
))

View File

@@ -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;
}

View File

@@ -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'),

View File

@@ -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 = <T extends HasVersion>(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<string>
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

View File

@@ -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)
})
})

View File

@@ -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',
})
)

View File

@@ -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
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

View File

@@ -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}\`
`

View File

@@ -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"}\`
`

View File

@@ -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"\`
`

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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) ->

View File

@@ -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,
})
})
})
})

View File

@@ -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

View File

@@ -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)
})

View File

@@ -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")

View File

@@ -126,6 +126,7 @@ class Server
e
open: (config = {}, project, onWarning) ->
debug("server open")
la(_.isPlainObject(config), "expected plain config object", config)
Promise.try =>

View File

@@ -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

View File

@@ -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
})

View File

@@ -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"

View File

@@ -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 ->

View File

@@ -0,0 +1,12 @@
{
"browsers": [
{
"name": "bad browser",
"family": "unknown family",
"displayName": "Bad browser",
"version": "no version",
"path": "/path/to",
"majorVersion": 123
}
]
}

View File

@@ -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"
}

View File

@@ -0,0 +1 @@
module.exports = (onFn, config) ->

View File

@@ -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) => { ... })

View File

@@ -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')

View File

@@ -0,0 +1,3 @@
{
"viewportWidth": "foo"
}

View File

@@ -0,0 +1 @@
module.exports = (onFn, config) ->

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -0,0 +1,5 @@
# returns object with invalid properties
module.exports = (onFn, config) ->
{
viewportWidth: "foo"
}

View File

@@ -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) => { ... })

View File

@@ -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')

View File

@@ -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"
}

View File

@@ -0,0 +1,5 @@
# returns invalid config - browsers list cannot be empty
module.exports = (onFn, config) ->
{
browsers: []
}

View File

@@ -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) => { ... })

View File

@@ -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')

View File

@@ -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"
}

View File

@@ -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"
}]
}

View File

@@ -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) => { ... })

View File

@@ -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')

View File

@@ -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

View File

@@ -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({

View File

@@ -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 = {}

View File

@@ -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")