feat: create config package for config validation (#18589)

This commit is contained in:
Emily Rohrbough
2021-11-09 13:19:05 -06:00
committed by GitHub
parent 628202e42b
commit 99c8d466c8
65 changed files with 5215 additions and 1203 deletions
+1
View File
@@ -34,6 +34,7 @@ system-tests/projects/e2e/cypress/integration/typescript_syntax_error_spec.ts
# cli/types is linted by tslint/dtslint
cli/types
# packages/example is not linted (think about changing this)
packages/example
+3 -1
View File
@@ -13,6 +13,9 @@ cypress.zip
Cached Theme.pak
Cached Theme Material Design.pak
# from config, compiled .js files
packages/config/lib/*.js
# from data-context, compiled .js files
packages/data-context/src/**/*.js
@@ -20,7 +23,6 @@ packages/data-context/src/**/*.js
packages/desktop-gui/cypress/videos
packages/desktop-gui/src/jsconfig.json
# from driver
packages/driver/cypress/videos
packages/driver/cypress/screenshots
+3 -1
View File
@@ -1,6 +1,8 @@
{
// To see these extensions in VS Code:
// 1. Open the Command Palette (Ctrl+Shift+P)
// 1. Open the Command Palette:
// - Non-Mac Users: (Ctrl+Shift+P)
// - Mac Users: (Cmd+Shift+P)
// 2. Select "Extensions: Show Recommended Extensions"
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
+1 -1
View File
@@ -963,7 +963,7 @@ jobs:
desktop-gui-component-tests,
cli-visual-tests,
runner-integration-tests-chrome,
runner-ct-integration-tests-chrome
runner-ct-integration-tests-chrome,
reporter-integration-tests,
- run: yarn percy build:finalize
+1
View File
@@ -7,6 +7,7 @@
"properties": {
"baseUrl": {
"type": "string",
"default": null,
"description": "Url used as prefix for cy.visit() or cy.request() commands url. Example http://localhost:3030 or https://test.my-domain.com"
},
"env": {
+2 -2
View File
@@ -17,7 +17,7 @@
"bump": "node ./scripts/binary.js bump",
"check-node-version": "node scripts/check-node-version.js",
"check-terminal": "node scripts/check-terminal.js",
"clean": "lerna run clean --parallel",
"clean": "lerna run clean --parallel --no-bail",
"clean-deps": "find . -depth -name node_modules -type d -exec rm -rf {} \\;",
"clean-untracked-files": "git clean -d -f",
"precypress:open": "yarn ensure-deps",
@@ -46,7 +46,7 @@
"stop-only": "npx stop-only --skip .cy,.publish,.projects,node_modules,dist,dist-test,fixtures,lib,bower_components,src,__snapshots__ --exclude e2e.ts,cypress-tests.ts",
"stop-only-all": "yarn stop-only --folder packages",
"pretest": "yarn ensure-deps",
"test": "yarn lerna exec yarn test --scope cypress --scope \"'@packages/{electron,extension,https-proxy,launcher,net-stubbing,network,proxy,rewriter,runner,runner-shared,socket}'\"",
"test": "yarn lerna exec yarn test --scope cypress --scope \"'@packages/{config,electron,extension,https-proxy,launcher,net-stubbing,network,proxy,rewriter,runner,runner-shared,socket}'\"",
"test-debug": "lerna exec yarn test-debug --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",
"pretest-e2e": "yarn ensure-deps",
"test-integration": "lerna exec yarn test-integration --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",
+6
View File
@@ -0,0 +1,6 @@
{
"extends": [
"../../.eslintrc.json"
],
"parser": "@typescript-eslint/parser"
}
+11
View File
@@ -0,0 +1,11 @@
# Config
The `config` package contains the configuration types and validation used in both the `server` and the `driver` for setting the Cypress configuration values.
## Testing
### Unit Tests
```bash
yarn workspace @packages/config test-unit
```
+146
View File
@@ -0,0 +1,146 @@
exports['src/index .getBreakingKeys returns list of breaking config keys 1'] = [
"blacklistHosts",
"experimentalComponentTesting",
"experimentalGetCookiesSameSite",
"experimentalNetworkStubbing",
"experimentalRunEvents",
"experimentalShadowDomSupport",
"firefoxGcInterval",
"nodeVersion",
"nodeVersion"
]
exports['src/index .getDefaultValues returns list of public config keys 1'] = {
"animationDistanceThreshold": 5,
"baseUrl": null,
"blockHosts": null,
"chromeWebSecurity": true,
"clientCertificates": [],
"component": {},
"componentFolder": "cypress/component",
"defaultCommandTimeout": 4000,
"downloadsFolder": "cypress/downloads",
"e2e": {},
"env": {},
"execTimeout": 60000,
"experimentalFetchPolyfill": false,
"experimentalInteractiveRunEvents": false,
"experimentalSessionSupport": false,
"experimentalSourceRewriting": false,
"experimentalStudio": false,
"fileServerFolder": "",
"fixturesFolder": "cypress/fixtures",
"ignoreTestFiles": "*.hot-update.js",
"includeShadowDom": false,
"integrationFolder": "cypress/integration",
"modifyObstructiveCode": true,
"numTestsKeptInMemory": 50,
"pageLoadTimeout": 60000,
"pluginsFile": "cypress/plugins",
"port": null,
"projectId": null,
"redirectionLimit": 20,
"reporter": "spec",
"reporterOptions": null,
"requestTimeout": 5000,
"resolvedNodePath": null,
"resolvedNodeVersion": null,
"responseTimeout": 30000,
"retries": {
"runMode": 0,
"openMode": 0
},
"screenshotOnRunFailure": true,
"screenshotsFolder": "cypress/screenshots",
"slowTestThreshold": 10000,
"scrollBehavior": "top",
"supportFile": "cypress/support",
"supportFolder": false,
"taskTimeout": 60000,
"testFiles": "**/*.*",
"trashAssetsBeforeRuns": true,
"userAgent": null,
"video": true,
"videoCompression": 32,
"videosFolder": "cypress/videos",
"videoUploadOnPasses": true,
"viewportHeight": 660,
"viewportWidth": 1000,
"waitForAnimations": true,
"watchForFileChanges": true,
"autoOpen": false,
"browsers": [],
"clientRoute": "/__/",
"configFile": "cypress.json",
"devServerPublicPathRoute": "/__cypress/src",
"hosts": null,
"isTextTerminal": false,
"morgan": true,
"namespace": "__cypress",
"reporterRoute": "/__cypress/reporter",
"socketId": null,
"socketIoCookie": "__socket.io",
"socketIoRoute": "/__socket.io",
"xhrRoute": "/xhrs/"
}
exports['src/index .getPublicConfigKeys returns list of public config keys 1'] = [
"animationDistanceThreshold",
"baseUrl",
"blockHosts",
"chromeWebSecurity",
"clientCertificates",
"component",
"componentFolder",
"defaultCommandTimeout",
"downloadsFolder",
"e2e",
"env",
"execTimeout",
"experimentalFetchPolyfill",
"experimentalInteractiveRunEvents",
"experimentalSessionSupport",
"experimentalSourceRewriting",
"experimentalStudio",
"fileServerFolder",
"fixturesFolder",
"ignoreTestFiles",
"includeShadowDom",
"integrationFolder",
"modifyObstructiveCode",
"nodeVersion",
"numTestsKeptInMemory",
"pageLoadTimeout",
"pluginsFile",
"port",
"projectId",
"redirectionLimit",
"reporter",
"reporterOptions",
"requestTimeout",
"resolvedNodePath",
"resolvedNodeVersion",
"responseTimeout",
"retries",
"screenshotOnRunFailure",
"screenshotsFolder",
"slowTestThreshold",
"scrollBehavior",
"supportFile",
"supportFolder",
"taskTimeout",
"testFiles",
"trashAssetsBeforeRuns",
"userAgent",
"video",
"videoCompression",
"videosFolder",
"videoUploadOnPasses",
"viewportHeight",
"viewportWidth",
"waitForAnimations",
"watchForFileChanges",
"browsers",
"hosts",
"modifyObstructiveCode"
]
@@ -0,0 +1,157 @@
exports['undefined browsers'] = `
Missing browsers list
`
exports['empty list of browsers'] = `
Expected at least 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"\`
`
exports['src/validation .isValidBrowser passes valid browsers and forms error messages for invalid ones isValidBrowser 1'] = {
"name": "isValidBrowser",
"behavior": [
{
"given": {
"name": "Chrome",
"displayName": "Chrome Browser",
"family": "chromium",
"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": "chromium",
"path": "",
"version": "99.101.3",
"majorVersion": 99
},
"expect": true
},
{
"given": {
"name": "No display name",
"family": "chromium"
},
"expect": "Expected `displayName` to be a non-empty string. Instead the value was: `{\"name\":\"No display name\",\"family\":\"chromium\"}`"
},
{
"given": {
"name": "bad family",
"displayName": "Bad family browser",
"family": "unknown family"
},
"expect": "Expected `family` to be either chromium or firefox. Instead the value was: `{\"name\":\"bad family\",\"displayName\":\"Bad family browser\",\"family\":\"unknown family\"}`"
}
]
}
exports['not one of the strings error message'] = `
Expected \`test\` to be one of these values: "foo", "bar". Instead the value was: \`"nope"\`
`
exports['number instead of string'] = `
Expected \`test\` to be one of these values: "foo", "bar". Instead the value was: \`42\`
`
exports['null instead of string'] = `
Expected \`test\` to be one of these values: "foo", "bar". Instead the value was: \`null\`
`
exports['not one of the numbers error message'] = `
Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`4\`
`
exports['string instead of a number'] = `
Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`"foo"\`
`
exports['null instead of a number'] = `
Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`null\`
`
exports['src/validation .isStringOrFalse returns error message when value is neither string nor false 1'] = `
Expected \`mockConfigKey\` to be a string or false. Instead the value was: \`null\`
`
exports['src/validation .isBoolean returns error message when value is a not a string 1'] = `
Expected \`mockConfigKey\` to be a string. Instead the value was: \`1\`
`
exports['src/validation .isString returns error message when value is a not a string 1'] = `
Expected \`mockConfigKey\` to be a string. Instead the value was: \`1\`
`
exports['src/validation .isArray returns error message when value is a non-array 1'] = `
Expected \`mockConfigKey\` to be an array. Instead the value was: \`1\`
`
exports['not string or array'] = `
Expected \`mockConfigKey\` to be a string or an array of strings. Instead the value was: \`null\`
`
exports['array of non-strings'] = `
Expected \`mockConfigKey\` to be a string or an array of strings. Instead the value was: \`[1,2,3]\`
`
exports['src/validation .isNumberOrFalse returns error message when value is a not number or false 1'] = `
Expected \`mockConfigKey\` to be a number or false. Instead the value was: \`null\`
`
exports['src/validation .isPlainObject returns error message when value is a not an object 1'] = `
Expected \`mockConfigKey\` to be a plain object. Instead the value was: \`1\`
`
exports['src/validation .isNumber returns error message when value is a not a number 1'] = `
Expected \`mockConfigKey\` to be a number. Instead the value was: \`"string"\`
`
exports['invalid retry value'] = `
Expected \`mockConfigKey\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
`
exports['invalid retry object'] = `
Expected \`mockConfigKey\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`{"fakeMode":1}\`
`
exports['src/validation .isValidClientCertificatesSet returns error message for certs not passed as an array array 1'] = `
Expected \`mockConfigKey\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
`
exports['src/validation .isValidClientCertificatesSet returns error message for certs object without url 1'] = `
Expected \`clientCertificates[0].url\` to be a URL matcher. Instead the value was: \`undefined\`
`
exports['missing https protocol'] = `
Expected \`clientCertificates[0].url\` to be an https protocol. Instead the value was: \`"http://url.com"\`
`
exports['invalid url'] = `
Expected \`clientCertificates[0].url\` to be a valid URL. Instead the value was: \`"not *"\`
`
exports['not qualified url'] = `
Expected \`mockConfigKey\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"url.com"\`
`
exports['empty string'] = `
Expected \`mockConfigKey\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`""\`
`
+104
View File
@@ -0,0 +1,104 @@
const _ = require('lodash')
const debug = require('debug')('cypress:config:validator')
const { options, breakingOptions } = require('./options')
const dashesOrUnderscoresRe = /^(_-)+/
// takes an array and creates an index object of [keyKey]: [valueKey]
const createIndex = (arr, keyKey, valueKey) => {
return _.reduce(arr, (memo, item) => {
if (item[valueKey] !== undefined) {
memo[item[keyKey]] = item[valueKey]
}
return memo
}, {})
}
const breakingKeys = _.map(breakingOptions, 'name')
const defaultValues = createIndex(options, 'name', 'defaultValue')
const publicConfigKeys = _(options).reject({ isInternal: true }).map('name').value()
const validationRules = createIndex(options, 'name', 'validation')
module.exports = {
allowed: (obj = {}) => {
const propertyNames = publicConfigKeys.concat(breakingKeys)
return _.pick(obj, propertyNames)
},
getBreakingKeys: () => {
return breakingKeys
},
getDefaultValues: (runtimeOptions = {}) => {
// Default values can be functions, in which case they are evaluated
// at runtime - for example, slowTestThreshold where the default value
// varies between e2e and component testing.
return _.mapValues(defaultValues, (value) => (typeof value === 'function' ? value(runtimeOptions) : value))
},
getPublicConfigKeys: () => {
return publicConfigKeys
},
matchesConfigKey: (key) => {
if (_.has(defaultValues, key)) {
return key
}
key = key.toLowerCase().replace(dashesOrUnderscoresRe, '')
key = _.camelCase(key)
if (_.has(defaultValues, key)) {
return key
}
},
options,
validate: (cfg, onErr) => {
debug('validating configuration')
return _.each(cfg, (value, key) => {
const validationFn = validationRules[key]
// key has a validation rule & value different from the default
if (validationFn && value !== defaultValues[key]) {
const result = validationFn(key, value)
if (result !== true) {
return onErr(result)
}
}
})
},
validateNoBreakingConfig: (cfg, onWarning, onErr) => {
breakingOptions.forEach(({ name, errorKey, newName, isWarning, value }) => {
if (cfg.hasOwnProperty(name)) {
if (value && cfg[name] !== value) {
// Bail if a value is specified but the config does not have that value.
return
}
if (isWarning) {
return onWarning(errorKey, {
name,
newName,
value,
configFile: cfg.configFile,
})
}
return onErr(errorKey, {
name,
newName,
value,
configFile: cfg.configFile,
})
}
})
},
}
@@ -1,4 +1,62 @@
const v = require('./util/validation')
const validate = require('./validation')
interface ResolvedConfigOption {
name: string
defaultValue?: any
validation: Function
isFolder?: boolean
isExperimental?: boolean
}
interface RuntimeConfigOption {
name: string
defaultValue: any
validation: Function
isInternal?: boolean
}
interface BreakingOption {
/**
* The non-passive configuration option.
*/
name: string
/**
* String to summarize the error messaging that is logged.
*/
errorKey: string
/**
* Configuration value of the configuration option to check against.
*/
value?: string
/**
* The new configuration key that is replacing the existing configuration key.
*/
newName?: string
/**
* Whether to log the error message as a warning instead of throwing an error.
*/
isWarning?: boolean
}
const isValidConfig = (key, config) => {
const status = validate.isPlainObject(key, config)
if (status !== true) {
return status
}
for (const rule of options) {
if (rule.name in config && rule.validation) {
const status = rule.validation(`${key}.${rule.name}`, config[rule.name])
if (status !== true) {
return status
}
}
}
return true
}
// NOTE:
// If you add/remove/change a config value, make sure to update the following
@@ -7,303 +65,335 @@ const v = require('./util/validation')
//
// Add options in alphabetical order for better readability
export const options = [
// TODO - add boolean attribute to indicate read-only / static vs mutable options
// that can be updated during test executions
const resolvedOptions: Array<ResolvedConfigOption> = [
{
name: 'animationDistanceThreshold',
defaultValue: 5,
validation: v.isNumber,
}, {
name: 'autoOpen',
defaultValue: false,
isInternal: true,
validation: validate.isNumber,
}, {
name: 'baseUrl',
defaultValue: null,
validation: v.isFullyQualifiedUrl,
validation: validate.isFullyQualifiedUrl,
}, {
name: 'blockHosts',
defaultValue: null,
validation: v.isStringOrArrayOfStrings,
}, {
name: 'browsers',
defaultValue: [],
validation: v.isValidBrowserList,
validation: validate.isStringOrArrayOfStrings,
}, {
name: 'chromeWebSecurity',
defaultValue: true,
validation: v.isBoolean,
}, {
name: 'clientRoute',
defaultValue: '/__/',
isInternal: true,
validation: validate.isBoolean,
}, {
name: 'clientCertificates',
defaultValue: [],
validation: v.isValidClientCertificatesSet,
validation: validate.isValidClientCertificatesSet,
}, {
name: 'component',
// runner-ct overrides
defaultValue: {},
validation: v.isValidConfig,
validation: isValidConfig,
}, {
name: 'componentFolder',
defaultValue: 'cypress/component',
validation: v.isStringOrFalse,
validation: validate.isStringOrFalse,
isFolder: true,
}, {
name: 'configFile',
defaultValue: 'cypress.json',
validation: v.isStringOrFalse,
// not truly internal, but can only be set via cli,
// so we don't consider it a "public" option
isInternal: true,
}, {
name: 'defaultCommandTimeout',
defaultValue: 4000,
validation: v.isNumber,
}, {
name: 'devServerPublicPathRoute',
defaultValue: '/__cypress/src',
isInternal: true,
validation: validate.isNumber,
}, {
name: 'downloadsFolder',
defaultValue: 'cypress/downloads',
validation: v.isString,
validation: validate.isString,
isFolder: true,
}, {
name: 'e2e',
// e2e runner overrides
defaultValue: {},
validation: v.isValidConfig,
validation: isValidConfig,
}, {
name: 'env',
validation: v.isPlainObject,
defaultValue: {},
validation: validate.isPlainObject,
}, {
name: 'execTimeout',
defaultValue: 60000,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'experimentalFetchPolyfill',
defaultValue: false,
validation: v.isBoolean,
validation: validate.isBoolean,
isExperimental: true,
}, {
name: 'experimentalInteractiveRunEvents',
defaultValue: false,
validation: v.isBoolean,
isExperimental: true,
}, {
name: 'experimentalSourceRewriting',
defaultValue: false,
validation: v.isBoolean,
isExperimental: true,
}, {
name: 'experimentalStudio',
defaultValue: false,
validation: v.isBoolean,
validation: validate.isBoolean,
isExperimental: true,
}, {
name: 'experimentalSessionSupport',
defaultValue: false,
validation: v.isBoolean,
validation: validate.isBoolean,
isExperimental: true,
}, {
name: 'experimentalSourceRewriting',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
}, {
name: 'experimentalStudio',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
}, {
name: 'fileServerFolder',
defaultValue: '',
validation: v.isString,
validation: validate.isString,
isFolder: true,
}, {
name: 'fixturesFolder',
defaultValue: 'cypress/fixtures',
validation: v.isStringOrFalse,
validation: validate.isStringOrFalse,
isFolder: true,
}, {
name: 'hosts',
defaultValue: null,
}, {
name: 'ignoreTestFiles',
defaultValue: '*.hot-update.js',
validation: v.isStringOrArrayOfStrings,
validation: validate.isStringOrArrayOfStrings,
}, {
name: 'includeShadowDom',
defaultValue: false,
validation: v.isBoolean,
validation: validate.isBoolean,
}, {
name: 'integrationFolder',
defaultValue: 'cypress/integration',
validation: v.isString,
validation: validate.isString,
isFolder: true,
}, {
name: 'isTextTerminal',
defaultValue: false,
isInternal: true,
}, {
name: 'morgan',
defaultValue: true,
isInternal: true,
}, {
name: 'modifyObstructiveCode',
defaultValue: true,
validation: v.isBoolean,
}, {
name: 'namespace',
defaultValue: '__cypress',
isInternal: true,
validation: validate.isBoolean,
}, {
name: 'nodeVersion',
validation: v.isOneOf('bundled', 'system'),
validation: validate.isOneOf('bundled', 'system'),
}, {
name: 'numTestsKeptInMemory',
defaultValue: 50,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'pageLoadTimeout',
defaultValue: 60000,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'pluginsFile',
defaultValue: 'cypress/plugins',
validation: v.isStringOrFalse,
validation: validate.isStringOrFalse,
isFolder: true,
}, {
name: 'port',
defaultValue: null,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'projectId',
defaultValue: null,
validation: v.isString,
validation: validate.isString,
}, {
name: 'redirectionLimit',
defaultValue: 20,
validation: validate.isNumber,
}, {
name: 'reporter',
defaultValue: 'spec',
validation: v.isString,
validation: validate.isString,
}, {
name: 'reporterOptions',
defaultValue: null,
}, {
name: 'reporterRoute',
defaultValue: '/__cypress/reporter',
isInternal: true,
validation: validate.isPlainObject,
}, {
name: 'requestTimeout',
defaultValue: 5000,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'resolvedNodePath',
defaultValue: null,
validation: validate.isString,
}, {
name: 'resolvedNodeVersion',
defaultValue: null,
validation: validate.isString,
}, {
name: 'responseTimeout',
defaultValue: 30000,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'retries',
defaultValue: {
runMode: 0,
openMode: 0,
},
validation: v.isValidRetriesConfig,
validation: validate.isValidRetriesConfig,
}, {
name: 'screenshotOnRunFailure',
defaultValue: true,
validation: v.isBoolean,
validation: validate.isBoolean,
}, {
name: 'screenshotsFolder',
defaultValue: 'cypress/screenshots',
validation: v.isStringOrFalse,
validation: validate.isStringOrFalse,
isFolder: true,
}, {
name: 'slowTestThreshold',
defaultValue: (options: Record<string, any>) => options.testingType === 'component' ? 250 : 10000,
validation: v.isNumber,
}, {
name: 'socketId',
defaultValue: null,
isInternal: true,
}, {
name: 'socketIoRoute',
defaultValue: '/__socket.io',
isInternal: true,
defaultValue: (options: Record<string, any> = {}) => options.testingType === 'component' ? 250 : 10000,
validation: validate.isNumber,
}, {
name: 'scrollBehavior',
defaultValue: 'top',
validation: v.isOneOf('center', 'top', 'bottom', 'nearest', false),
}, {
name: 'socketIoCookie',
defaultValue: '__socket.io',
isInternal: true,
validation: validate.isOneOf('center', 'top', 'bottom', 'nearest', false),
}, {
name: 'supportFile',
defaultValue: 'cypress/support',
validation: v.isStringOrFalse,
validation: validate.isStringOrFalse,
isFolder: true,
}, {
name: 'supportFolder',
defaultValue: false,
validation: validate.isStringOrFalse,
isFolder: true,
}, {
name: 'taskTimeout',
defaultValue: 60000,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'testFiles',
defaultValue: '**/*.*',
validation: v.isStringOrArrayOfStrings,
validation: validate.isStringOrArrayOfStrings,
}, {
name: 'trashAssetsBeforeRuns',
defaultValue: true,
validation: v.isBoolean,
}, {
name: 'unitFolder',
isFolder: true,
isInternal: true,
validation: validate.isBoolean,
}, {
name: 'userAgent',
defaultValue: null,
validation: v.isString,
validation: validate.isString,
}, {
name: 'video',
defaultValue: true,
validation: v.isBoolean,
validation: validate.isBoolean,
}, {
name: 'videoCompression',
defaultValue: 32,
validation: v.isNumberOrFalse,
validation: validate.isNumberOrFalse,
}, {
name: 'videosFolder',
defaultValue: 'cypress/videos',
validation: v.isString,
validation: validate.isString,
isFolder: true,
}, {
name: 'videoUploadOnPasses',
defaultValue: true,
validation: v.isBoolean,
validation: validate.isBoolean,
}, {
name: 'viewportHeight',
defaultValue: 660,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'viewportWidth',
defaultValue: 1000,
validation: v.isNumber,
validation: validate.isNumber,
}, {
name: 'waitForAnimations',
defaultValue: true,
validation: v.isBoolean,
validation: validate.isBoolean,
}, {
name: 'watchForFileChanges',
defaultValue: true,
validation: v.isBoolean,
validation: validate.isBoolean,
},
]
const runtimeOptions: Array<RuntimeConfigOption> = [
{
name: 'autoOpen',
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
}, {
name: 'browsers',
defaultValue: [],
validation: validate.isValidBrowserList,
}, {
name: 'clientRoute',
defaultValue: '/__/',
validation: validate.isString,
isInternal: true,
}, {
name: 'configFile',
defaultValue: 'cypress.json',
validation: validate.isStringOrFalse,
// not truly internal, but can only be set via cli,
// so we don't consider it a "public" option
isInternal: true,
}, {
name: 'devServerPublicPathRoute',
defaultValue: '/__cypress/src',
validation: validate.isString,
isInternal: true,
}, {
name: 'hosts',
defaultValue: null,
validation: validate.isPlainObject,
}, {
name: 'isTextTerminal',
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
}, {
name: 'morgan',
defaultValue: true,
validation: validate.isBoolean,
isInternal: true,
}, {
name: 'modifyObstructiveCode',
defaultValue: true,
validation: validate.isBoolean,
}, {
name: 'namespace',
defaultValue: '__cypress',
validation: validate.isString,
isInternal: true,
}, {
name: 'reporterRoute',
defaultValue: '/__cypress/reporter',
validation: validate.isString,
isInternal: true,
}, {
name: 'socketId',
defaultValue: null,
validation: validate.isString,
isInternal: true,
}, {
name: 'socketIoCookie',
defaultValue: '__socket.io',
validation: validate.isString,
isInternal: true,
}, {
name: 'socketIoRoute',
defaultValue: '/__socket.io',
validation: validate.isString,
isInternal: true,
}, {
name: 'xhrRoute',
defaultValue: '/xhrs/',
validation: validate.isString,
isInternal: true,
},
]
export const breakingOptions = [
export const options: Array<ResolvedConfigOption|RuntimeConfigOption> = [
...resolvedOptions,
...runtimeOptions,
]
export const breakingOptions: Array<BreakingOption> = [
{
name: 'blacklistHosts',
errorKey: 'RENAMED_CONFIG_OPTION',
@@ -2,7 +2,6 @@ const _ = require('lodash')
const debug = require('debug')('cypress:server:validation')
const is = require('check-more-types')
const { commaListsOr } = require('common-tags')
const configOptions = require('../config_options')
const path = require('path')
// validation functions take a key and a value and should:
@@ -10,6 +9,7 @@ const path = require('path')
// - return a error message if it fails validation
const str = JSON.stringify
const { isArray, isString, isFinite: isNumber } = _
/**
* Forms good Markdown-like string message.
@@ -36,10 +36,6 @@ const isFalse = (value) => {
return value === false
}
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.
@@ -129,26 +125,6 @@ const isPlainObject = (key, value) => {
return errMsg(key, value, 'a plain object')
}
const isValidConfig = (key, config) => {
const status = isPlainObject(key, config)
if (status !== true) {
return status
}
for (const rule of configOptions.options) {
if (rule.name in config && rule.validation) {
const status = rule.validation(`${key}.${rule.name}`, config[rule.name])
if (status !== true) {
return status
}
}
}
return true
}
const isOneOf = (...values) => {
return (key, value) => {
if (values.some((v) => {
@@ -171,7 +147,7 @@ const isOneOf = (...values) => {
* Validates whether the supplied set of cert information is valid
* @returns {string|true} Returns `true` if the information set is valid. Returns an error message if it is not.
*/
const isValidClientCertificatesSet = (key, certsForUrls) => {
const isValidClientCertificatesSet = (_key, certsForUrls) => {
debug('clientCerts: %o', certsForUrls)
if (!Array.isArray(certsForUrls)) {
@@ -266,8 +242,6 @@ module.exports = {
isValidRetriesConfig,
isValidConfig,
isPlainObject,
isNumber (key, value) {
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@packages/config",
"version": "0.0.0-development",
"description": "Config contains the configuration types and validation function used in the cypress electron application.",
"private": true,
"main": "lib/index.js",
"scripts": {
"build-prod": "tsc --project .",
"clean": "rm lib/options.js",
"test": "yarn test-unit",
"test-debug": "yarn test-unit --inspect-brk=5566",
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register -extension=.js,.ts test/unit/*spec.* --exit"
},
"dependencies": {
"check-more-types": "2.24.0",
"common-tags": "1.8.0",
"debug": "4.3.2",
"lodash": "4.17.21"
},
"devDependencies": {
"@packages/ts": "0.0.0-development",
"chai": "1.10.0",
"mocha": "7.0.1",
"sinon": "7.3.1",
"sinon-chai": "3.3.0",
"snap-shot-it": "7.9.3"
},
"files": [
"lib"
]
}
+149
View File
@@ -0,0 +1,149 @@
const chai = require('chai')
const snapshot = require('snap-shot-it')
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
const configUtil = require('../../lib/index')
chai.use(sinonChai)
const { expect } = chai
describe('src/index', () => {
describe('.allowed', () => {
it('returns filter config only containing allowed keys', () => {
const keys = configUtil.allowed({
'baseUrl': 'https://url.com',
'blacklistHosts': 'breaking option',
'devServerPublicPathRoute': 'internal key',
'random': 'not a config option',
})
expect(keys).to.deep.eq({
'baseUrl': 'https://url.com',
'blacklistHosts': 'breaking option',
})
})
})
describe('.getBreakingKeys', () => {
it('returns list of breaking config keys', () => {
const breakingKeys = configUtil.getBreakingKeys()
expect(breakingKeys).to.include('blacklistHosts')
snapshot(breakingKeys)
})
})
describe('.getDefaultValues', () => {
it('returns list of public config keys', () => {
const defaultValues = configUtil.getDefaultValues()
expect(defaultValues).to.deep.include({
defaultCommandTimeout: 4000,
scrollBehavior: 'top',
watchForFileChanges: true,
})
expect(defaultValues.env).to.deep.eq({})
snapshot(defaultValues)
})
})
describe('.getPublicConfigKeys', () => {
it('returns list of public config keys', () => {
const publicConfigKeys = configUtil.getPublicConfigKeys()
expect(publicConfigKeys).to.include('blockHosts')
expect(publicConfigKeys).to.not.include('devServerPublicPathRoute')
snapshot(publicConfigKeys)
})
})
describe('.matchesConfigKey', () => {
it('returns key when config key has a default value', () => {
const normalizedKey = configUtil.matchesConfigKey('testFiles')
expect(normalizedKey).to.eq('testFiles')
})
it('returns normalized key when config key has a default value', () => {
let normalizedKey = configUtil.matchesConfigKey('EXEC_TIMEOUT')
expect(normalizedKey).to.eq('execTimeout')
normalizedKey = configUtil.matchesConfigKey('Base-url')
expect(normalizedKey).to.eq('baseUrl')
normalizedKey = configUtil.matchesConfigKey('TEST-FILES')
expect(normalizedKey).to.eq('testFiles')
})
it('returns nothing when config key does not has a default value', () => {
let normalizedKey = configUtil.matchesConfigKey('random')
expect(normalizedKey).to.be.undefined
})
})
describe('.validate', () => {
it('validates config', () => {
const errorFn = sinon.spy()
configUtil.validate({
'baseUrl': 'https://',
}, errorFn)
expect(errorFn).to.have.been.callCount(0)
})
it('calls error callback if config is invalid', () => {
const errorFn = sinon.spy()
configUtil.validate({
'baseUrl': ' ',
}, errorFn)
expect(errorFn).to.have.been.calledWithMatch(/Expected `baseUrl`/)
})
})
describe('.validateNoBreakingConfig', () => {
it('calls warning callback if config contains breaking option that warns', () => {
const warningFn = sinon.spy()
const errorFn = sinon.spy()
configUtil.validateNoBreakingConfig({
'experimentalNetworkStubbing': 'should break',
configFile: 'config.js',
}, warningFn, errorFn)
expect(warningFn).to.have.been.calledOnceWith('EXPERIMENTAL_NETWORK_STUBBING_REMOVED', {
name: 'experimentalNetworkStubbing',
newName: undefined,
value: undefined,
configFile: 'config.js',
})
expect(errorFn).to.have.been.callCount(0)
})
it('calls error callback if config contains breaking option that should throw an error', () => {
const warningFn = sinon.spy()
const errorFn = sinon.spy()
configUtil.validateNoBreakingConfig({
'blacklistHosts': 'should break',
configFile: 'config.js',
}, warningFn, errorFn)
expect(warningFn).to.have.been.callCount(0)
expect(errorFn).to.have.been.calledOnceWith('RENAMED_CONFIG_OPTION', {
name: 'blacklistHosts',
newName: 'blockHosts',
value: undefined,
configFile: 'config.js',
})
})
})
})
@@ -0,0 +1,391 @@
const snapshot = require('snap-shot-it')
const { expect } = require('chai')
const validation = require('../../lib/validation')
describe('src/validation', () => {
const mockKey = 'mockConfigKey'
describe('.isValidClientCertificatesSet', () => {
it('returns error message for certs not passed as an array array', () => {
const result = validation.isValidRetriesConfig(mockKey, '1')
expect(result).to.not.be.true
snapshot(result)
})
it('returns error message for certs object without url', () => {
const result = validation.isValidClientCertificatesSet(mockKey, [
{ name: 'cert' },
])
expect(result).to.not.be.true
snapshot(result)
})
it('returns error message for certs url not matching *', () => {
let result = validation.isValidClientCertificatesSet(mockKey, [
{ url: 'http://url.com' },
])
expect(result).to.not.be.true
snapshot('missing https protocol', result)
result = validation.isValidClientCertificatesSet(mockKey, [
{ url: 'not *' },
])
expect(result).to.not.be.true
snapshot('invalid url', result)
})
})
describe('.isValidBrowser', () => {
it('passes valid browsers and forms error messages for invalid ones', () => {
const browsers = [
// valid browser
{
name: 'Chrome',
displayName: 'Chrome Browser',
family: 'chromium',
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: 'chromium',
path: '',
version: '99.101.3',
majorVersion: 99,
},
// invalid browser, missing displayName
{
name: 'No display name',
family: 'chromium',
},
{
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
return snapshot.apply(null, [validation.isValidBrowser].concat(browsers))
})
})
describe('.isValidBrowserList', () => {
it('does not allow empty or not browsers', () => {
snapshot('undefined browsers', validation.isValidBrowserList('browsers'))
snapshot('empty list of browsers', validation.isValidBrowserList('browsers', []))
return snapshot('browsers list with a string', validation.isValidBrowserList('browsers', ['foo']))
})
})
describe('.isValidRetriesConfig', () => {
it('returns true for valid retry value', () => {
let result = validation.isValidRetriesConfig(mockKey, null)
expect(result).to.be.true
result = validation.isValidRetriesConfig(mockKey, 2)
expect(result).to.be.true
})
it('returns true for valid retry objects', () => {
let result = validation.isValidRetriesConfig(mockKey, { runMode: 1 })
expect(result).to.be.true
result = validation.isValidRetriesConfig(mockKey, { openMode: 1 })
expect(result).to.be.true
result = validation.isValidRetriesConfig(mockKey, {
runMode: 3,
openMode: 0,
})
expect(result).to.be.true
})
it('returns error message for invalid retry config', () => {
let result = validation.isValidRetriesConfig(mockKey, '1')
expect(result).to.not.be.true
snapshot('invalid retry value', result)
result = validation.isValidRetriesConfig(mockKey, { fakeMode: 1 })
expect(result).to.not.be.true
snapshot('invalid retry object', result)
})
})
describe('.isPlainObject', () => {
it('returns true for value=null', () => {
const result = validation.isPlainObject(mockKey, null)
expect(result).to.be.true
})
it('returns true for value=number', () => {
const result = validation.isPlainObject(mockKey, { foo: 'bar' })
expect(result).to.be.true
})
it('returns error message when value is a not an object', () => {
const result = validation.isPlainObject(mockKey, 1)
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isNumber', () => {
it('returns true for value=null', () => {
const result = validation.isNumber(mockKey, null)
expect(result).to.be.true
})
it('returns true for value=number', () => {
const result = validation.isNumber(mockKey, 1)
expect(result).to.be.true
})
it('returns error message when value is a not a number', () => {
const result = validation.isNumber(mockKey, 'string')
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isNumberOrFalse', () => {
it('returns true for value=number', () => {
const result = validation.isNumberOrFalse(mockKey, 1)
expect(result).to.be.true
})
it('returns true for value=false', () => {
const result = validation.isNumberOrFalse(mockKey, false)
expect(result).to.be.true
})
it('returns error message when value is a not number or false', () => {
const result = validation.isNumberOrFalse(mockKey, null)
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isFullyQualifiedUrl', () => {
it('returns true for value=null', () => {
const result = validation.isFullyQualifiedUrl(mockKey, null)
expect(result).to.be.true
})
it('returns true for value=qualified urls', () => {
let result = validation.isFullyQualifiedUrl(mockKey, 'https://url.com')
expect(result).to.be.true
result = validation.isFullyQualifiedUrl(mockKey, 'http://url.com')
expect(result).to.be.true
})
it('returns error message when value is a not qualified url', () => {
let result = validation.isFullyQualifiedUrl(mockKey, 'url.com')
expect(result).to.not.be.true
snapshot('not qualified url', result)
result = validation.isFullyQualifiedUrl(mockKey, '')
expect(result).to.not.be.true
snapshot('empty string', result)
})
})
describe('.isBoolean', () => {
it('returns true for value=null', () => {
const result = validation.isBoolean(mockKey, null)
expect(result).to.be.true
})
it('returns true for value=true', () => {
const result = validation.isBoolean(mockKey, true)
expect(result).to.be.true
})
it('returns true for value=false', () => {
const result = validation.isBoolean(mockKey, false)
expect(result).to.be.true
})
it('returns error message when value is a not a string', () => {
const result = validation.isString(mockKey, 1)
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isString', () => {
it('returns true for value=null', () => {
const result = validation.isString(mockKey, null)
expect(result).to.be.true
})
it('returns true for value=array', () => {
const result = validation.isString(mockKey, 'string')
expect(result).to.be.true
})
it('returns error message when value is a not a string', () => {
const result = validation.isString(mockKey, 1)
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isArray', () => {
it('returns true for value=null', () => {
const result = validation.isArray(mockKey, null)
expect(result).to.be.true
})
it('returns true for value=array', () => {
const result = validation.isArray(mockKey, [1, 2, 3])
expect(result).to.be.true
})
it('returns error message when value is a non-array', () => {
const result = validation.isArray(mockKey, 1)
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isStringOrFalse', () => {
it('returns true for value=string', () => {
const result = validation.isStringOrFalse(mockKey, 'string')
expect(result).to.be.true
})
it('returns true for value=false', () => {
const result = validation.isStringOrFalse(mockKey, false)
expect(result).to.be.true
})
it('returns error message when value is neither string nor false', () => {
const result = validation.isStringOrFalse(mockKey, null)
expect(result).to.not.be.true
snapshot(result)
})
})
describe('.isStringOrArrayOfStrings', () => {
it('returns true for value=string', () => {
const result = validation.isStringOrArrayOfStrings(mockKey, 'string')
expect(result).to.be.true
})
it('returns true for value=array of strings', () => {
const result = validation.isStringOrArrayOfStrings(mockKey, ['string', 'other'])
expect(result).to.be.true
})
it('returns error message when value is neither string nor array of string', () => {
let result = validation.isStringOrArrayOfStrings(mockKey, null)
expect(result).to.not.be.true
snapshot('not string or array', result)
result = validation.isStringOrArrayOfStrings(mockKey, [1, 2, 3])
expect(result).to.not.be.true
snapshot('array of non-strings', result)
})
})
describe('.isOneOf', () => {
it('validates a string', () => {
const validate = validation.isOneOf('foo', 'bar')
expect(validate).to.be.a('function')
expect(validate('test', 'foo')).to.be.true
expect(validate('test', 'bar')).to.be.true
// different value
let msg = validate('test', 'nope')
expect(msg).to.not.be.true
snapshot('not one of the strings error message', msg)
msg = validate('test', 42)
expect(msg).to.not.be.true
snapshot('number instead of string', msg)
msg = validate('test', null)
expect(msg).to.not.be.true
return snapshot('null instead of string', msg)
})
it('validates a number', () => {
const validate = validation.isOneOf(1, 2, 3)
expect(validate).to.be.a('function')
expect(validate('test', 1)).to.be.true
expect(validate('test', 3)).to.be.true
// different value
let msg = validate('test', 4)
expect(msg).to.not.be.true
snapshot('not one of the numbers error message', msg)
msg = validate('test', 'foo')
expect(msg).to.not.be.true
snapshot('string instead of a number', msg)
msg = validate('test', null)
expect(msg).to.not.be.true
return snapshot('null instead of a number', msg)
})
})
})
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../ts/tsconfig.json",
"include": [
"lib/*",
],
}
@@ -1,6 +1,5 @@
const Cookie = require('js-cookie')
const { stripIndent } = require('common-tags')
const helpers = require('../../support/helpers')
const { _, Promise, $ } = Cypress
@@ -694,7 +693,7 @@ describe('src/cy/commands/navigation', () => {
})
it('does not support file:// protocol', {
baseUrl: '',
baseUrl: null,
}, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.contain('`cy.visit()` failed because the \'file://...\' protocol is not supported by Cypress.')
@@ -1439,7 +1438,7 @@ describe('src/cy/commands/navigation', () => {
cy.visit('http://google.com:3500/fixtures/generic.html')
})
it('throws attemping to visit 2 unique ip addresses', function (done) {
it('throws attempting to visit 2 unique ip addresses', function (done) {
const $autIframe = cy.state('$autIframe')
const load = () => {
@@ -1971,12 +1970,10 @@ describe('src/cy/commands/navigation', () => {
})
describe('errors', () => {
helpers.registerCypressConfigBackupRestore()
beforeEach(function () {
this.logs = []
cy.on('log:added', (attrs, log) => {
cy.on('log:added', (_attrs, log) => {
this.lastLog = log
this.logs.push(log)
})
@@ -1984,42 +1981,53 @@ describe('src/cy/commands/navigation', () => {
return null
})
it('can time out', function (done) {
let thenCalled = false
describe('can time out', () => {
let pageLoadTimeout
cy.on('fail', (err) => {
const { lastLog } = this
// visit, window, page loading
expect(this.logs.length).to.eq(3)
expect(err.message).to.include('Your page did not fire its `load` event within `50ms`.')
expect(lastLog.get('name')).to.eq('page load')
expect(lastLog.get('error')).to.eq(err)
return Promise
.delay(100)
.then(() => {
expect(cy.state('onPageLoadErr')).to.be.null
expect(cy.isStopped()).to.be.true // make sure we ran our cleanup routine
expect(thenCalled).to.be.false
done()
})
before(() => {
pageLoadTimeout = Cypress.config().pageLoadTimeout
})
cy
.visit('/fixtures/jquery.html')
.window().then((win) => {
Cypress.config('pageLoadTimeout', 50)
after(() => {
Cypress.config('pageLoadTimeout', pageLoadTimeout)
})
const $a = win.$('<a href=\'/timeout?ms=500\'>jquery</a>')
.appendTo(win.document.body)
it('times out', function (done) {
let thenCalled = false
causeSynchronousBeforeUnload($a)
cy.on('fail', (err) => {
const { lastLog } = this
return null
}).wrap(null).then(() => {
thenCalled = true
// visit, window, page loading
expect(this.logs.length).to.eq(3)
expect(err.message).to.include('Your page did not fire its `load` event within `50ms`.')
expect(lastLog.get('name')).to.eq('page load')
expect(lastLog.get('error')).to.eq(err)
return Promise
.delay(100)
.then(() => {
expect(cy.state('onPageLoadErr')).to.be.null
expect(cy.isStopped()).to.be.true // make sure we ran our cleanup routine
expect(thenCalled).to.be.false
done()
})
})
cy
.visit('/fixtures/jquery.html')
.window().then((win) => {
Cypress.config('pageLoadTimeout', 50)
const $a = win.$('<a href=\'/timeout?ms=500\'>jquery</a>')
.appendTo(win.document.body)
causeSynchronousBeforeUnload($a)
return null
}).wrap(null).then(() => {
thenCalled = true
})
})
})
@@ -2086,8 +2094,7 @@ describe('src/cy/commands/navigation', () => {
}
})
cy
.visit('/fixtures/jquery.html')
cy.visit('/fixtures/jquery.html')
// make get timeout after only 200ms
.get('#does-not-exist', { timeout: 200 }).should('have.class', 'foo')
@@ -820,7 +820,7 @@ describe('src/cy/commands/request', () => {
})
it('throws when url is not FQDN', {
baseUrl: '',
baseUrl: null,
}, function (done) {
cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns('')
@@ -840,7 +840,7 @@ describe('src/cy/commands/request', () => {
})
it('throws when url is not FQDN, notes that configFile is disabled', {
baseUrl: '',
baseUrl: null,
configFile: false,
}, function (done) {
cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns('')
@@ -860,7 +860,7 @@ describe('src/cy/commands/request', () => {
})
it('throws when url is not FQDN, notes that configFile is non-default', {
baseUrl: '',
baseUrl: null,
configFile: 'foo.json',
}, function (done) {
cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns('')
@@ -274,7 +274,7 @@ describe('per-test config', () => {
})
})
describe('in mulitple nested suites', () => {
describe('in multiple nested suites', () => {
describe('config in suite', {
foo: true,
}, () => {
@@ -295,7 +295,7 @@ describe('per-test config', () => {
})
})
describe('emtpy config', {}, () => {
describe('empty config', {}, () => {
it('empty config in test', {}, () => {
expect(true).ok
})
@@ -8,18 +8,6 @@ const getQueueNames = () => {
return _.map(cy.queue, 'name')
}
const registerCypressConfigBackupRestore = () => {
let originalConfig
beforeEach(() => {
originalConfig = _.clone(Cypress.config())
})
afterEach(() => {
Cypress.config(originalConfig)
})
}
function allowTsModuleStubbing () {
// eslint-disable-next-line no-undef
__webpack_require__.d = function (exports, name, getter) {
@@ -41,6 +29,5 @@ function allowTsModuleStubbing () {
module.exports = {
getQueueNames,
getFirstSubjectByName,
registerCypressConfigBackupRestore,
allowTsModuleStubbing,
}
+3 -1
View File
@@ -7,7 +7,8 @@
"cypress:open": "node ../../scripts/cypress open",
"cypress:run": "node ../../scripts/cypress run",
"postinstall": "patch-package",
"start": "node -e 'console.log(require(`chalk`).red(`\nError:\n\tRunning \\`yarn start\\` is no longer needed for driver/cypress tests.\n\tWe now automatically spawn the server in the pluginsFile.\n\tChanges to the server will be watched and reloaded automatically.`))'"
"start": "node -e 'console.log(require(`chalk`).red(`\nError:\n\tRunning \\`yarn start\\` is no longer needed for driver/cypress tests.\n\tWe now automatically spawn the server in the pluginsFile.\n\tChanges to the server will be watched and reloaded automatically.`))'",
"test": "yarn cypress:run"
},
"dependencies": {},
"devDependencies": {
@@ -17,6 +18,7 @@
"@cypress/unique-selector": "0.4.2",
"@cypress/webpack-preprocessor": "0.0.0-development",
"@cypress/what-is-circular": "1.0.1",
"@packages/config": "0.0.0-development",
"@packages/network": "0.0.0-development",
"@packages/resolve-dist": "0.0.0-development",
"@packages/runner": "0.0.0-development",
+1 -1
View File
@@ -510,7 +510,7 @@ export default {
}
const assert = function (...args) {
// if we've temporarily overriden assertions
// if we've temporarily overridden assertions
// then just bail early with this function
const fn = cy.state('overrideAssert') || assertFn
@@ -281,7 +281,6 @@ export default function (Commands, Cypress, cy, state) {
return Commands.addAllSync({
spy,
stub,
})
}
+1 -1
View File
@@ -160,7 +160,7 @@ export default function (Commands, Cypress, cy, state, config) {
// stuff, or handling this in the runner itself?
// Cypress sessions will clear cookies on its own before each test
Cypress.on('test:before:run:async', () => {
if (!Cypress.config.experimentalSessionSupport) {
if (!Cypress.config('experimentalSessionSupport')) {
return getAndClear()
}
})
@@ -427,7 +427,7 @@ export default function (Commands, Cypress, cy, state, config) {
// if a screenshot has not been taken (by cy.screenshot()) in the test
// that failed, we can bypass UI-changing and pixel-checking (simple: true)
// otheriwse, we need to do all the standard checks
// otherwise, we need to do all the standard checks
// to make sure the UI is in the right place (simple: false)
screenshotConfig.capture = 'runner'
+1 -1
View File
@@ -33,7 +33,7 @@ const eventHasReturnValue = (e) => {
const val = e.returnValue
// return false if val is an empty string
// of if its undinefed
// of if its undefined
if (val === '' || _.isUndefined(val)) {
return false
}
+1 -1
View File
@@ -101,7 +101,7 @@ export default {
// are not and the retry code is happening between
// runnables which is bad likely due to the issue below
//
// bug in bluebird with not propagating cancelations
// bug in bluebird with not propagating cancellations
// fast enough in a series of promises
// https://github.com/petkaantonov/bluebird/issues/1424
return state('canceled') || state('error') || runnableHasChanged()
+94 -18
View File
@@ -1,17 +1,72 @@
import _ from 'lodash'
import $errUtils from '../cypress/error_utils'
function mutateConfiguration (testConfigOverride, config, env) {
const globalConfig = _.clone(config())
// See Test Config Overrides in ../../../../cli/types/cypress.d.ts
type ResolvedTestConfigOverride = {
/**
* The list of test config overrides and the invocation details used to add helpful
* error messaging to consumers if a test override fails validation.
*/
testConfigList: Array<TestConfig>
/**
* The test config overrides that will apply to the test if it passes validation.
* */
unverifiedTestConfig: Object
}
type TestConfig = {
overrides: {
browser?: Object
}
invocationDetails: {
stack: Object
}
};
type ConfigOverrides = {
env: Object | undefined
};
function setConfig (testConfigList: Array<TestConfig>, config, localConfigOverrides: ConfigOverrides = { env: undefined }) {
testConfigList.forEach(({ overrides: testConfigOverride, invocationDetails }) => {
if (_.isArray(testConfigOverride)) {
setConfig(testConfigOverride, config, localConfigOverrides)
} else {
delete testConfigOverride.browser
try {
config(testConfigOverride)
} catch (e) {
let err = $errUtils.errByPath('config.invalid_test_override', {
errMsg: e.message,
})
err.stack = $errUtils.stackWithReplacedProps({ stack: invocationDetails.stack }, err)
throw err
}
localConfigOverrides = { ...localConfigOverrides, ...testConfigOverride }
}
})
return localConfigOverrides
}
function mutateConfiguration (testConfig: ResolvedTestConfigOverride, config, env) {
const { testConfigList } = testConfig || []
let globalConfig = _.clone(config())
const localConfigOverrides = setConfig(testConfigList, config)
// only store the global config values that updated
globalConfig = _.pick(globalConfig, Object.keys(localConfigOverrides))
const globalEnv = _.clone(env())
delete testConfigOverride.browser
config(testConfigOverride)
const localConfigOverridesBackup = _.clone(localConfigOverrides)
const localTestConfig = config()
const localTestConfigBackup = _.clone(localTestConfig)
if (testConfigOverride.env) {
env(testConfigOverride.env)
if (localConfigOverrides.env) {
env(localConfigOverrides.env)
}
const localTestEnv = env()
@@ -23,10 +78,15 @@ function mutateConfiguration (testConfigOverride, config, env) {
// TODO: (NEXT_BREAKING) always restore configuration
// do not allow global mutations inside test
const restoreConfigFn = function () {
_.each(localTestConfig, (val, key) => {
if (localTestConfigBackup[key] !== val) {
_.each(localConfigOverrides, (val, key) => {
if (localConfigOverridesBackup[key] !== val) {
globalConfig[key] = val
}
// explicitly set to undefined if config wasn't previously defined
if (!globalConfig.hasOwnProperty(key)) {
globalConfig[key] = undefined
}
})
_.each(localTestEnv, (val, key) => {
@@ -35,8 +95,9 @@ function mutateConfiguration (testConfigOverride, config, env) {
}
})
config.reset()
// reset test config overrides
config(globalConfig)
env.reset()
env(globalEnv)
}
@@ -46,30 +107,45 @@ function mutateConfiguration (testConfigOverride, config, env) {
// this is called during test onRunnable time
// in order to resolve the test config upfront before test runs
export function getResolvedTestConfigOverride (test) {
// note: must return as an object to meet the dashboard recording API
export function getResolvedTestConfigOverride (test): ResolvedTestConfigOverride {
let curParent = test.parent
const testConfig = [test._testConfig]
const testConfigList = [{
overrides: test._testConfig,
invocationDetails: test.invocationDetails,
}]
while (curParent) {
if (curParent._testConfig) {
testConfig.push(curParent._testConfig)
testConfigList.unshift({
overrides: curParent._testConfig,
invocationDetails: curParent.invocationDetails,
})
}
curParent = curParent.parent
}
return _.reduceRight(testConfig, (acc, opts) => _.extend(acc, opts), {})
const testConfig = {
testConfigList: testConfigList.filter((opt) => opt.overrides !== undefined),
// collect test overrides to send to the dashboard api when @packages/server is ran in record mode
unverifiedTestConfig: _.reduce(testConfigList, (acc, opts) => _.extend(acc, opts.overrides), {}),
}
return testConfig
}
class TestConfigOverride {
private restoreTestConfigFn: Nullable<() => void> = null
restoreAndSetTestConfigOverrides (test, config, env) {
if (this.restoreTestConfigFn) this.restoreTestConfigFn()
const resolvedTestConfig = test._testConfig || {}
this.restoreTestConfigFn = mutateConfiguration(resolvedTestConfig, config, env)
if (Object.keys(resolvedTestConfig.unverifiedTestConfig).length > 0) {
this.restoreTestConfigFn = mutateConfiguration(resolvedTestConfig, config, env)
}
}
}
+10 -7
View File
@@ -1,5 +1,6 @@
// @ts-nocheck
import { validate } from '@packages/config'
import _ from 'lodash'
import $ from 'jquery'
import * as blobUtil from 'blob-util'
@@ -116,7 +117,7 @@ class $Cypress {
// normalize this into boolean
config.isTextTerminal = !!config.isTextTerminal
// we asumme we're interactive based on whether or
// we assume we're interactive based on whether or
// not we're in a text terminal, but we keep this
// as a separate property so we can potentially
// slice up the behavior
@@ -142,7 +143,12 @@ class $Cypress {
this.state = $SetterGetter.create({})
this.originalConfig = _.cloneDeep(config)
this.config = $SetterGetter.create(config)
this.config = $SetterGetter.create(config, (config) => {
validate(config, (errMsg) => {
throw new this.state('specWindow').Error(errMsg)
})
})
this.env = $SetterGetter.create(env)
this.getTestRetries = function () {
const testRetries = this.config('retries')
@@ -269,6 +275,8 @@ class $Cypress {
return this.emit('stop')
case 'cypress:config':
// emit config event used to:
// - trigger iframe viewport update
return this.emit('config', args[0])
case 'runner:start':
@@ -390,16 +398,12 @@ class $Cypress {
return this.runner.onRunnableRun(...args)
case 'runner:test:before:run':
// get back to a clean slate
this.cy.reset(...args)
if (this.config('isTextTerminal')) {
// needed for handling test retries
this.emit('mocha', 'test:before:run', args[0])
}
this.emit('test:before:run', ...args)
break
case 'runner:test:before:run:async':
@@ -423,7 +427,6 @@ class $Cypress {
}
break
case 'cy:before:all:screenshots':
return this.emit('before:all:screenshots', ...args)
+3 -5
View File
@@ -142,7 +142,7 @@ export default {
return enqueuedCmd = obj
}
// only check for command enqueing when none
// only check for command enqueuing when none
// of our args are functions else commands
// like cy.then or cy.each would always fail
// since they return promises and queue more
@@ -224,8 +224,7 @@ export default {
command.set({ subject })
// end / snapshot our logs
// if they need it
// end / snapshot our logs if they need it
command.finishLogs()
// reset the nestedIndex back to null
@@ -234,8 +233,7 @@ export default {
// also reset recentlyReady back to null
state('recentlyReady', null)
// we're finished with the current command
// so set it back to null
// we're finished with the current command so set it back to null
state('current', null)
state('subject', subject)
+35 -29
View File
@@ -3,11 +3,14 @@
/* eslint-disable prefer-rest-params */
import _ from 'lodash'
import Promise from 'bluebird'
import debugFn from 'debug'
import { registerFetch } from 'unfetch'
import $dom from '../dom'
import $utils from './utils'
import $errUtils from './error_utils'
import $stackUtils from './stack_utils'
import $Chai from '../cy/chai'
import $Xhrs from '../cy/xhrs'
import $jQuery from '../cy/jquery'
@@ -31,8 +34,6 @@ import { $Command } from './command'
import $CommandQueue from './command_queue'
import $VideoRecorder from '../cy/video-recorder'
import $TestConfigOverrides from '../cy/testConfigOverrides'
import debugFn from 'debug'
import { registerFetch } from 'unfetch'
const debugErrors = debugFn('cypress:driver:errors')
@@ -222,7 +223,7 @@ export default {
Cypress.action('app:window:before:unload', e)
// return undefined so our beforeunload handler
// doesnt trigger a confirmation dialog
// doesn't trigger a confirmation dialog
return undefined
},
onUnload (e) {
@@ -330,7 +331,7 @@ export default {
// we look at whether or not nestedIndex is a number, because if it
// is then we need to insert inside of our commands, else just push
// it onto the end of the queu
// it onto the end of the queue
const index = _.isNumber(nestedIndex) ? nestedIndex : queue.length
queue.insert(index, $Command.create(obj))
@@ -541,7 +542,7 @@ export default {
_.extend(cy, {
id: _.uniqueId('cy'),
// synchrounous querying
// synchronous querying
$$,
state,
@@ -704,29 +705,35 @@ export default {
return doneEarly()
},
reset (attrs, test) {
const s = state()
// reset is called before each test
reset (test) {
try {
const s = state()
const backup = {
window: s.window,
document: s.document,
$autIframe: s.$autIframe,
specWindow: s.specWindow,
activeSessions: s.activeSessions,
const backup = {
window: s.window,
document: s.document,
$autIframe: s.$autIframe,
specWindow: s.specWindow,
activeSessions: s.activeSessions,
}
// reset state back to empty object
state.reset()
// and then restore these backed up props
state(backup)
queue.reset()
queue.clear()
timers.reset()
testConfigOverrides.restoreAndSetTestConfigOverrides(test, Cypress.config, Cypress.env)
cy.removeAllListeners()
} catch (err) {
fail(err)
}
// reset state back to empty object
state.reset()
// and then restore these backed up props
state(backup)
queue.reset()
queue.clear()
timers.reset()
testConfigOverrides.restoreAndSetTestConfigOverrides(test, Cypress.config, Cypress.env)
return cy.removeAllListeners()
},
addCommandSync (name, fn) {
@@ -1034,8 +1041,7 @@ export default {
const originalDone = arguments[0]
arguments[0] = (done = function (err) {
// TODO: handle no longer error
// when ended early
// TODO: handle no longer error when ended early
doneEarly()
originalDone(err)
@@ -1056,7 +1062,7 @@ export default {
// if we returned a value from fn
// and enqueued some new commands
// and the value isnt currently cy
// and the value isn't currently cy
// or a promise
if (ret &&
(queue.length > currentLength) &&
@@ -245,6 +245,17 @@ export default {
},
},
config: {
invalid_argument: {
message: `Setting the config via ${cmd('Cypress.config')} failed with the following validation error:\n\n{{errMsg}}`,
docsUrl: 'https://on.cypress.io/config',
},
'invalid_test_override': {
message: `The config override passed to your test has the following validation error:\n\n{{errMsg}}`,
docsUrl: 'https://on.cypress.io/config',
},
},
contains: {
empty_string: {
message: `${cmd('contains')} cannot be passed an empty string.`,
+2 -1
View File
@@ -205,7 +205,7 @@ const throwErr = (err, options = {}) => {
let { onFail, errProps } = options
// assume onFail is a command if
//# onFail is present and isnt a function
//# onFail is present and isn't a function
if (onFail && !_.isFunction(onFail)) {
const command = onFail
@@ -543,6 +543,7 @@ const logError = (Cypress, handlerType, err, handled = false) => {
}
export default {
stackWithReplacedProps,
appendErrMsg,
createUncaughtException,
cypressErr,
+9 -6
View File
@@ -50,7 +50,7 @@ function overloadMochaFnForConfig (fnName, specWindow) {
const fnType = fnName === 'it' || fnName === 'specify' ? 'Test' : 'Suite'
function overrideFn (fn) {
function overrideMochaFn (fn) {
specWindow[fnName] = fn()
specWindow[fnName]['only'] = fn('only')
specWindow[fnName]['skip'] = fn('skip')
@@ -58,7 +58,7 @@ function overloadMochaFnForConfig (fnName, specWindow) {
if (specWindow[`x${fnName}`]) specWindow[`x${fnName}`] = specWindow[fnName]['skip']
}
overrideFn(function (subFn) {
const replacementFn = function (subFn) {
return function (...args) {
/**
* @type {Cypress.Cypress}
@@ -94,6 +94,7 @@ function overloadMochaFnForConfig (fnName, specWindow) {
const ret = origFn.apply(this, mochaArgs)
// attached testConfigOverrides will executes on `runner:test:before:run` event
ret._testConfig = _testConfig
return ret
@@ -101,7 +102,9 @@ function overloadMochaFnForConfig (fnName, specWindow) {
return origFn.apply(this, args)
}
})
}
overrideMochaFn(replacementFn)
}
const ui = (specWindow, _mocha, config) => {
@@ -355,7 +358,7 @@ const patchSuiteAddTest = (specWindow, config) => {
const test = args[0]
if (!test.invocationDetails) {
test.invocationDetails = $stackUtils.getInvocationDetails(specWindow, config).details
test.invocationDetails = $stackUtils.getInvocationDetails(specWindow, config)
}
const ret = suiteAddTest.apply(this, args)
@@ -387,7 +390,7 @@ const patchSuiteAddSuite = (specWindow, config) => {
const suite = args[0]
if (!suite.invocationDetails) {
suite.invocationDetails = $stackUtils.getInvocationDetails(specWindow, config).details
suite.invocationDetails = $stackUtils.getInvocationDetails(specWindow, config)
}
return suiteAddSuite.apply(this, args)
@@ -439,7 +442,7 @@ const patchSuiteHooks = (specWindow, config) => {
if (!hook.invocationDetails) {
const invocationDetails = $stackUtils.getInvocationDetails(specWindow, config)
hook.invocationDetails = invocationDetails.details
hook.invocationDetails = invocationDetails
invocationStack = invocationDetails.stack
}
+46 -46
View File
@@ -17,6 +17,8 @@ const mochaCtxKeysRe = /^(_runnable|test)$/
const betweenQuotesRe = /\"(.+?)\"/
const HOOKS = 'beforeAll beforeEach afterEach afterAll'.split(' ')
const TEST_BEFORE_RUN_ASYNC_EVENT = 'runner:test:before:run:async'
// event fired before hooks and test execution
const TEST_BEFORE_RUN_EVENT = 'runner:test:before:run'
const TEST_AFTER_RUN_EVENT = 'runner:test:after:run'
@@ -48,8 +50,8 @@ const fired = (event, runnable) => {
const testBeforeRunAsync = (test, Cypress) => {
return Promise.try(() => {
if (!fired('runner:test:before:run:async', test)) {
return fire('runner:test:before:run:async', test, Cypress)
if (!fired(TEST_BEFORE_RUN_ASYNC_EVENT, test)) {
return fire(TEST_BEFORE_RUN_ASYNC_EVENT, test, Cypress)
}
})
}
@@ -73,7 +75,7 @@ const testAfterRun = (test, Cypress) => {
// if the test:after:run listener throws it's likely spec code
// Since the test status has already been emitted this can't affect the test status.
// Let's just log the error to console
// TODO: revist when we handle uncaught exceptions/rejections between tests
// TODO: revisit when we handle uncaught exceptions/rejections between tests
// eslint-disable-next-line no-console
console.error(e)
}
@@ -336,7 +338,7 @@ const isRootSuite = (suite) => {
}
const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, getTests) => {
// bail if our _runner doesnt have a hook.
// bail if our _runner doesn't have a hook.
// useful in tests
if (!_runner.hook) {
return
@@ -581,10 +583,6 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getRun
wrappedRunnable._testConfig = cfg
}
if (cfg.slowTestThreshold) {
runnable.slow(cfg.slowTestThreshold)
}
wrappedRunnable._titlePath = runnable.titlePath()
}
@@ -918,9 +916,9 @@ const _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, se
// })
/**
* Mocha retry event is only fired in Mocha version 6+
* https://github.com/mochajs/mocha/commit/2a76dd7589e4a1ed14dd2a33ab89f182e4c4a050
*/
* Mocha retry event is only fired in Mocha version 6+
* https://github.com/mochajs/mocha/commit/2a76dd7589e4a1ed14dd2a33ab89f182e4c4a050
*/
_runner.on('retry', (test, err) => {
test.err = $errUtils.wrapErr(err)
Cypress.action('runner:retry', wrap(test), test.err)
@@ -1137,7 +1135,7 @@ export default {
}
const onRunnable = (r) => {
// set defualt retries at onRunnable time instead of onRunnableRun
// set default retries at onRunnable time instead of onRunnableRun
return _runnables.push(r)
}
@@ -1217,6 +1215,7 @@ export default {
const isAfterEachHook = isHook && !!hookName.match(/after each/)
const retryAbleRunnable = isTest || isBeforeEachHook || isAfterEachHook
const willRetry = (test._currentRetry < test._retries) && retryAbleRunnable
const isTestConfigOverride = !fired(TEST_BEFORE_RUN_EVENT, test)
const fail = function () {
return err
@@ -1231,13 +1230,19 @@ export default {
test.final = false
}
if (willRetry && isBeforeEachHook) {
if (isTestConfigOverride) {
// let the runner handle the error
delete runnable.err
}
if ((willRetry || isTestConfigOverride) && isBeforeEachHook) {
delete runnable.err
test._retriesBeforeEachFailedTestFn = test.fn
// this prevents afterEach hooks that exist at a deeper level than the failing one from running
// we will always skip remaining beforeEach hooks since they will always be same level or deeper
test._skipHooksWithLevelGreaterThan = runnable.titlePath().length
setHookFailureProps(test, runnable, err)
test.fn = function () {
throw err
@@ -1247,7 +1252,7 @@ export default {
}
if (willRetry && isAfterEachHook) {
// if we've already failed this attempt from an afterEach hook then we've already enqueud another attempt
// if we've already failed this attempt from an afterEach hook then we've already enqueued another attempt
// so return early
if (test._retriedFromAfterEachHook) {
return noFail()
@@ -1374,10 +1379,9 @@ export default {
}
// closure for calculating the actual
// runtime of a runnables fn exection duration
// runtime of a runnables fn execution duration
// and also the run of the runnable:after:run:async event
let lifecycleStart
let wallClockStartedAt = null
let wallClockEnd = null
let fnDurationStart = null
let fnDurationEnd = null
@@ -1387,7 +1391,7 @@ export default {
// when this is a hook, capture the real start
// date so we can calculate our test's duration
// including all of its hooks
wallClockStartedAt = new Date()
const wallClockStartedAt = new Date()
if (!test.wallClockStartedAt) {
// if we don't have lifecycle timings yet
@@ -1398,27 +1402,13 @@ export default {
test.wallClockStartedAt = wallClockStartedAt
}
// if this isnt a hook, then the name is 'test'
const hookName = runnable.type === 'hook' ? getHookName(runnable) : 'test'
const isHook = runnable.type === 'hook'
// if this isn't a hook, then the name is 'test'
const hookName = isHook ? getHookName(runnable) : 'test'
// set hook id to hook id or test id
const hookId = runnable.type === 'hook' ? runnable.hookId : runnable.id
// if we haven't yet fired this event for this test
// that means that we need to reset the previous state
// of cy - since we now have a new 'test' and all of the
// associated _runnables will share this state
if (!fired(TEST_BEFORE_RUN_EVENT, test)) {
fire(TEST_BEFORE_RUN_EVENT, test, Cypress)
// this is the earliest we can set test._retries since test:before:run
// will load in testConfigOverrides (per test configuration)
const retries = Cypress.getTestRetries() ?? -1
test._retries = retries
}
const isHook = runnable.type === 'hook'
const hookId = isHook ? runnable.hookId : runnable.id
const isAfterEachHook = isHook && hookName.match(/after each/)
const isBeforeEachHook = isHook && hookName.match(/before each/)
@@ -1522,22 +1512,32 @@ export default {
})
}
cy.state('duringUserTestExecution', false)
// our runnable is about to run, so let cy know. this enables
// us to always have a correct runnable set even when we are
// running lifecycle events
// and also get back a function result handler that we use as
// an async seam
cy.setRunnable(runnable, hookId)
// TODO: handle promise timeouts here!
// whenever any runnable is about to run
// we figure out what test its associated to
// if its a hook, and then we fire the
// test:before:run:async action if its not
// been fired before for this test
return testBeforeRunAsync(test, Cypress)
return Promise.try(() => {
if (!fired(TEST_BEFORE_RUN_EVENT, test)) {
cy.reset(test)
test.slow(Cypress.config('slowTestThreshold'))
test._retries = Cypress.getTestRetries() ?? -1
fire(TEST_BEFORE_RUN_EVENT, test, Cypress)
}
cy.state('duringUserTestExecution', false)
// our runnable is about to run, so let cy know. this enables
// us to always have a correct runnable set even when we are
// running lifecycle events
// and also get back a function result handler that we use as
// an async seam
cy.setRunnable(runnable, hookId)
})
.then(() => {
return testBeforeRunAsync(test, Cypress)
})
.catch((err) => {
// TODO: if our async tasks fail
// then allow us to cause the test
+7 -5
View File
@@ -1,4 +1,4 @@
import _ from 'lodash'
import { extend, isObject, isString } from 'lodash'
const reset = (state = {}) => {
// perf loop
@@ -11,7 +11,7 @@ const reset = (state = {}) => {
// a basic object setter / getter class
export default {
create: (state = {}) => {
create: (state = {}, validate?) => {
const get = (key?) => {
if (key) {
return state[key]
@@ -24,7 +24,7 @@ export default {
let obj
let ret
if (_.isObject(key)) {
if (isObject(key)) {
obj = key
ret = obj
} else {
@@ -33,7 +33,9 @@ export default {
ret = value
}
_.extend(state, obj)
validate && validate(obj)
extend(state, obj)
return ret
}
@@ -44,7 +46,7 @@ export default {
case 0:
return get()
case 1:
if (_.isString(key)) {
if (isString(key)) {
return get(key)
}
+6 -6
View File
@@ -100,17 +100,16 @@ const getInvocationDetails = (specWindow, config) => {
// firefox throws a different stack than chromium
// which includes stackframes from cypress_runner.js.
// So we drop the lines until we get to the spec stackframe (incldues __cypress/tests)
// So we drop the lines until we get to the spec stackframe (includes __cypress/tests)
if (specWindow.Cypress && specWindow.Cypress.isBrowser('firefox')) {
stack = stackWithLinesDroppedFromMarker(stack, '__cypress/tests', true)
}
const details = getSourceDetailsForFirstLine(stack, config('projectRoot'))
const details = getSourceDetailsForFirstLine(stack, config('projectRoot')) || {}
return {
details,
stack,
}
details.stack = stack
return details
}
}
@@ -383,6 +382,7 @@ const normalizedUserInvocationStack = (userInvocationStack) => {
export default {
replacedStack,
getCodeFrame,
getCodeFrameFromSource,
getSourceStack,
getStackLines,
getSourceDetailsForFirstLine,
+1 -1
View File
@@ -203,7 +203,7 @@ export function loadClientCertificateConfig (config) {
clientCertificateStore.clear()
// The basic validation of the certificate configuration has already been done by this point
// within the 'isValidClientCertificatesSet' function within packages/server/lib/util/validation.js
// within the 'isValidClientCertificatesSet' function within packages/config/src/validation.js
if (clientCertificates) {
clientCertificates.forEach((item) => {
debug(`loading client cert at index ${index}`)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,89 +0,0 @@
exports['not one of the strings error message'] = `
Expected \`test\` to be one of these values: "foo", "bar". Instead the value was: \`"nope"\`
`
exports['number instead of string'] = `
Expected \`test\` to be one of these values: "foo", "bar". Instead the value was: \`42\`
`
exports['null instead of string'] = `
Expected \`test\` to be one of these values: "foo", "bar". Instead the value was: \`null\`
`
exports['not one of the numbers error message'] = `
Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`4\`
`
exports['string instead of a number'] = `
Expected \`test\` to be one of these values: 1, 2, 3. Instead the value was: \`"foo"\`
`
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": "chromium",
"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": "chromium",
"path": "",
"version": "99.101.3",
"majorVersion": 99
},
"expect": true
},
{
"given": {
"name": "No display name",
"family": "chromium"
},
"expect": "Expected `displayName` to be a non-empty string. Instead the value was: `{\"name\":\"No display name\",\"family\":\"chromium\"}`"
},
{
"given": {
"name": "bad family",
"displayName": "Bad family browser",
"family": "unknown family"
},
"expect": "Expected `family` to be either chromium 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 least 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"\`
`
+36 -130
View File
@@ -2,6 +2,7 @@ import _ from 'lodash'
import path from 'path'
import Promise from 'bluebird'
import deepDiff from 'return-deep-diff'
import configUtils from '@packages/config'
import errors from './errors'
import scaffold from './scaffold'
@@ -14,7 +15,6 @@ import pathHelpers from './util/path_helpers'
const debug = Debug('cypress:server:config')
import { options, breakingOptions } from './config_options'
import { getProcessEnvVars, CYPRESS_SPECIAL_ENV_VARS } from './util/config'
export const RESOLVED_FROM = ['plugin', 'env', 'default', 'runtime', 'config'] as const
@@ -30,26 +30,9 @@ export type ResolvedConfigurationOptions = Partial<{
[x in keyof Cypress.ResolvedConfigOptions]: ResolvedFromConfig
}>
const dashesOrUnderscoresRe = /^(_-)+/
const folders = _(configUtils.options).filter({ isFolder: true }).map('name').value()
// takes an array and creates an index object of [keyKey]: [valueKey]
const createIndex = (arr, keyKey, valueKey) => {
return _.reduce(arr, (memo, item) => {
if (item[valueKey] !== undefined) {
memo[item[keyKey]] = item[valueKey]
}
return memo
}, {})
}
const publicConfigKeys = _(options).reject({ isInternal: true }).map('name').value()
const breakingKeys = _.map(breakingOptions, 'name')
const folders = _(options).filter({ isFolder: true }).map('name').value()
const validationRules = createIndex(options, 'name', 'validation')
const defaultValues: Record<string, any> = createIndex(options, 'name', 'defaultValue')
const convertRelativeToAbsolutePaths = (projectRoot, obj, defaults = {}) => {
const convertRelativeToAbsolutePaths = (projectRoot, obj) => {
return _.reduce(folders, (memo, folder) => {
const val = obj[folder]
@@ -62,54 +45,9 @@ const convertRelativeToAbsolutePaths = (projectRoot, obj, defaults = {}) => {
, {})
}
const validateNoBreakingConfig = (config) => {
breakingOptions.forEach(({ name, errorKey, newName, isWarning, value }) => {
if (config.hasOwnProperty(name)) {
if (value && config[name] !== value) {
// Bail if a value is specified but the config does not have that value.
return
}
if (isWarning) {
return errors.warning(errorKey, {
name,
newName,
value,
configFile: config.configFile,
})
}
return errors.throw(errorKey, {
name,
newName,
value,
configFile: config.configFile,
})
}
})
}
const validate = (cfg, onErr) => {
return _.each(cfg, (value, key) => {
const validationFn = validationRules[key]
// does this key have a validation rule?
if (validationFn) {
// and is the value different from the default?
if (value !== defaultValues[key]) {
const result = validationFn(key, value)
if (result !== true) {
return onErr(result)
}
}
}
})
}
const validateFile = (file) => {
return (settings) => {
return validate(settings, (errMsg) => {
return configUtils.validate(settings, (errMsg) => {
return errors.throw('SETTINGS_VALIDATION_ERROR', file, errMsg)
})
}
@@ -123,8 +61,7 @@ const hideSpecialVals = function (val, key) {
return val
}
// an object with a few utility methods
// for easy stubbing from unit tests
// an object with a few utility methods for easy stubbing from unit tests
export const utils = {
resolveModule (name) {
return require.resolve(name)
@@ -187,10 +124,6 @@ export const utils = {
},
}
export function getConfigKeys () {
return publicConfigKeys
}
export function isValidCypressInternalEnvValue (value) {
// names of config environments, see "config/app.yml"
const names = ['development', 'test', 'staging', 'production']
@@ -198,12 +131,6 @@ export function isValidCypressInternalEnvValue (value) {
return _.includes(names, value)
}
export function allowed (obj = {}) {
const propertyNames = publicConfigKeys.concat(breakingKeys)
return _.pick(obj, propertyNames)
}
export function get (projectRoot, options = {}) {
return Promise.all([
settings.read(projectRoot, options).then(validateFile('cypress.json')),
@@ -224,8 +151,7 @@ export function set (obj: Record<string, any> = {}) {
debug('setting config object')
let { projectRoot, projectName, config, envFile, options } = obj
// just force config to be an object
// so we dont have to do as much
// just force config to be an object so we dont have to do as much
// work in our tests
if (config == null) {
config = {}
@@ -233,8 +159,7 @@ export function set (obj: Record<string, any> = {}) {
debug('config is %o', config)
// flatten the object's properties
// into the master config object
// flatten the object's properties into the master config object
config.envFile = envFile
config.projectRoot = projectRoot
config.projectName = projectName
@@ -251,7 +176,7 @@ export function mergeDefaults (config: Record<string, any> = {}, options: Record
debug('merged config with options, got %o', config)
_
.chain(allowed(options))
.chain(configUtils.allowed(options))
.omit('env')
.omit('browsers')
.each((val, key) => {
@@ -268,18 +193,15 @@ export function mergeDefaults (config: Record<string, any> = {}, options: Record
config.baseUrl = url.replace(/\/\/+$/, '/')
}
_.defaults(config, defaultValues)
const defaultsForRuntime = configUtils.getDefaultValues(options)
// Default values can be functions, in which case they are evaluated
// at runtime - for example, slowTestThreshold where the default value
// varies between e2e and component testing.
config = _.mapValues(config, (value) => (typeof value === 'function' ? value(options) : value))
_.defaultsDeep(config, defaultsForRuntime)
// split out our own app wide env from user env variables
// and delete envFile
config.env = parseEnv(config, options.env, resolved)
config.cypressEnv = process.env['CYPRESS_INTERNAL_ENV']
config.cypressEnv = process.env.CYPRESS_INTERNAL_ENV
debug('using CYPRESS_INTERNAL_ENV %s', config.cypressEnv)
if (!isValidCypressInternalEnvValue(config.cypressEnv)) {
errors.throw('INVALID_CYPRESS_INTERNAL_ENV', config.cypressEnv)
@@ -297,36 +219,35 @@ export function mergeDefaults (config: Record<string, any> = {}, options: Record
config.numTestsKeptInMemory = 0
}
config = setResolvedConfigValues(config, defaultValues, resolved, options)
config = setResolvedConfigValues(config, defaultsForRuntime, resolved)
if (config.port) {
config = setUrls(config)
}
config = setAbsolutePaths(config, defaultValues)
config = setAbsolutePaths(config)
config = setParentTestsPaths(config)
config = setNodeBinary(config, options.args?.userNodePath, options.args?.userNodeVersion)
// validate config again here so that we catch
// configuration errors coming from the CLI overrides
// or env var overrides
validate(config, (errMsg) => {
// validate config again here so that we catch configuration errors coming
// from the CLI overrides or env var overrides
configUtils.validate(_.omit(config, 'browsers'), (errMsg) => {
return errors.throw('CONFIG_VALIDATION_ERROR', errMsg)
})
validateNoBreakingConfig(config)
configUtils.validateNoBreakingConfig(config, errors.warning, errors.throw)
return setSupportFileAndFolder(config)
.then(setPluginsFile)
return setSupportFileAndFolder(config, defaultsForRuntime)
.then((obj) => setPluginsFile(obj, defaultsForRuntime))
.then(setScaffoldPaths)
}
export function setResolvedConfigValues (config, defaults, resolved, options) {
export function setResolvedConfigValues (config, defaults, resolved) {
const obj = _.clone(config)
obj.resolved = resolveConfigValues(config, defaults, resolved, options)
obj.resolved = resolveConfigValues(config, defaults, resolved)
debug('resolved config is %o', obj.resolved.browsers)
return obj
@@ -361,7 +282,7 @@ export function updateWithPluginValues (cfg, overrides) {
// make sure every option returned from the plugins file
// passes our validation functions
validate(overrides, (errMsg) => {
configUtils.validate(overrides, (errMsg) => {
if (cfg.pluginsFile && cfg.projectRoot) {
const relativePluginsPath = path.relative(cfg.projectRoot, cfg.pluginsFile)
@@ -431,11 +352,11 @@ export function updateWithPluginValues (cfg, overrides) {
// combines the default configuration object with values specified in the
// configuration file like "cypress.json". Values in configuration file
// overwrite the defaults.
export function resolveConfigValues (config, defaults, resolved = {}, options = {}) {
export function resolveConfigValues (config, defaults, resolved = {}) {
// pick out only known configuration keys
return _
.chain(config)
.pick(publicConfigKeys)
.pick(configUtils.getPublicConfigKeys())
.mapValues((val, key) => {
let r
const source = (s: ResolvedConfigurationOptionSource): ResolvedFromConfig => {
@@ -455,11 +376,9 @@ export function resolveConfigValues (config, defaults, resolved = {}, options =
return source(r)
}
const defaultValue = typeof defaults[key] === 'function' ? defaults[key](options) : defaults[key]
if (!(!_.isEqual(config[key], defaultValue) && key !== 'browsers')) {
if (!(!_.isEqual(config[key], defaults[key]) && key !== 'browsers')) {
// "browsers" list is special, since it is dynamic by default
// and can only be ovewritten via plugins file
// and can only be overwritten via plugins file
return source('default')
}
@@ -497,7 +416,7 @@ export function setScaffoldPaths (obj) {
}
// async function
export function setSupportFileAndFolder (obj) {
export function setSupportFileAndFolder (obj, defaults) {
if (!obj.supportFile) {
return Promise.resolve(obj)
}
@@ -531,7 +450,7 @@ export function setSupportFileAndFolder (obj) {
return fs.pathExists(obj.supportFile)
.then((found) => {
if (!found) {
errors.throw('SUPPORT_FILE_NOT_FOUND', obj.supportFile, obj.configFile || defaultValues.configFile)
errors.throw('SUPPORT_FILE_NOT_FOUND', obj.supportFile, obj.configFile || defaults.configFile)
}
return debug('switching to found file %s', obj.supportFile)
@@ -539,7 +458,7 @@ export function setSupportFileAndFolder (obj) {
}).catch({ code: 'MODULE_NOT_FOUND' }, () => {
debug('support JS module %s does not load', sf)
const loadingDefaultSupportFile = sf === path.resolve(obj.projectRoot, defaultValues.supportFile)
const loadingDefaultSupportFile = sf === path.resolve(obj.projectRoot, defaults.supportFile)
return utils.discoverModuleFile({
filename: sf,
@@ -548,7 +467,7 @@ export function setSupportFileAndFolder (obj) {
})
.then((result) => {
if (result === null) {
const configFile = obj.configFile || defaultValues.configFile
const configFile = obj.configFile || defaults.configFile
return errors.throw('SUPPORT_FILE_NOT_FOUND', path.resolve(obj.projectRoot, sf), configFile)
}
@@ -584,7 +503,7 @@ export function setSupportFileAndFolder (obj) {
// * and the pluginsFile is NOT set to the default
// - throw an error, because it should be there if the user
// explicitly set it
export const setPluginsFile = Promise.method((obj) => {
export const setPluginsFile = Promise.method((obj, defaults) => {
if (!obj.pluginsFile) {
return obj
}
@@ -607,7 +526,7 @@ export const setPluginsFile = Promise.method((obj) => {
}).catch({ code: 'MODULE_NOT_FOUND' }, () => {
debug('plugins module does not exist %o', { pluginsFile })
const isLoadingDefaultPluginsFile = pluginsFile === path.resolve(obj.projectRoot, defaultValues.pluginsFile)
const isLoadingDefaultPluginsFile = pluginsFile === path.resolve(obj.projectRoot, defaults.pluginsFile)
return utils.discoverModuleFile({
filename: pluginsFile,
@@ -645,7 +564,7 @@ export function setParentTestsPaths (obj) {
return obj
}
export function setAbsolutePaths (obj, defaults) {
export function setAbsolutePaths (obj) {
let pr
obj = _.clone(obj)
@@ -658,7 +577,7 @@ export function setAbsolutePaths (obj, defaults) {
// obj.fileServerFolder = path.resolve(pr, obj.fileServerFolder)
// and do the same for all the rest
_.extend(obj, convertRelativeToAbsolutePaths(pr, obj, defaults))
_.extend(obj, convertRelativeToAbsolutePaths(pr, obj))
}
return obj
@@ -703,26 +622,13 @@ export function parseEnv (cfg: Record<string, any>, envCLI: Record<string, any>,
envCLI = envCLI != null ? envCLI : {}
const matchesConfigKey = function (key) {
if (_.has(defaultValues, key)) {
return key
}
key = key.toLowerCase().replace(dashesOrUnderscoresRe, '')
key = _.camelCase(key)
if (_.has(defaultValues, key)) {
return key
}
}
const configFromEnv = _.reduce(envProc, (memo: string[], val, key) => {
let cfgKey: string
cfgKey = matchesConfigKey(key)
cfgKey = configUtils.matchesConfigKey(key)
if (cfgKey) {
// only change the value if it hasnt been
// only change the value if it hasn't been
// set by the CLI. override default + config
if (resolved[cfgKey] !== 'cli') {
cfg[cfgKey] = val
+3 -1
View File
@@ -11,6 +11,7 @@ require('./environment')
const Promise = require('bluebird')
const debug = require('debug')('cypress:server:cypress')
const { getPublicConfigKeys } = require('@packages/config')
const argsUtils = require('./util/args')
const chalk = require('chalk')
const { openProject } = require('../lib/open_project')
@@ -29,8 +30,9 @@ const exit = (code = 0) => {
}
const showWarningForInvalidConfig = (options) => {
const publicConfigKeys = getPublicConfigKeys()
const invalidConfigOptions = require('lodash').keys(options.config).reduce((invalid, option) => {
if (!require('./config').getConfigKeys().find((configKey) => configKey === option)) {
if (!publicConfigKeys.find((configKey) => configKey === option)) {
invalid.push(option)
}
+1 -1
View File
@@ -633,7 +633,7 @@ const getMsgByType = function (type, arg1 = {}, arg2, arg3) {
We found an invalid value in the file: ${chalk.blue(filePath)}
${chalk.yellow(arg2)}`
// happens when there is an invalid config value returned from the
// happens when there is an invalid config value is returned from the
// project's plugins file like "cypress/plugins.index.js"
case 'PLUGINS_CONFIG_VALIDATION_ERROR':
filePath = `\`${arg1}\``
+2 -1
View File
@@ -7,6 +7,7 @@ const debugCiInfo = require('debug')('cypress:server:record:ci-info')
const Promise = require('bluebird')
const isForkPr = require('is-fork-pr')
const commitInfo = require('@cypress/commit-info')
const api = require('../api')
const logger = require('../logger')
const errors = require('../errors')
@@ -749,7 +750,7 @@ const createRunAndRecordSpecs = (options = {}) => {
return _.pick({
...v,
clientId: v.id,
config: v._testConfig || null,
config: v._testConfig?.unverifiedTestConfig || null,
title: v._titlePath,
hookIds: v.hooks.map((hook) => hook.hookId),
},
+2 -1
View File
@@ -6,6 +6,7 @@ import path from 'path'
import browsers from './browsers'
import pkg from '@packages/root'
import { allowed } from '@packages/config'
import { ServerCt } from './server-ct'
import { SocketCt } from './socket-ct'
import { SocketE2E } from './socket-e2e'
@@ -410,7 +411,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
// allowed config values to
// prevent tampering with the
// internals and breaking cypress
const allowedCfg = config.allowed(cfg)
const allowedCfg = allowed(cfg)
const modifiedCfg = await plugins.init(allowedCfg, {
projectRoot: this.projectRoot,
+3 -2
View File
@@ -4,8 +4,9 @@ const is = require('check-more-types')
const path = require('path')
const debug = require('debug')('cypress:server:args')
const minimist = require('minimist')
const { getPublicConfigKeys } = require('@packages/config')
const coerceUtil = require('./coerce')
const configUtil = require('../config')
const proxyUtil = require('./proxy')
const errors = require('../errors')
@@ -344,7 +345,7 @@ module.exports = {
}
// get a list of the available config keys
const configKeys = configUtil.getConfigKeys()
const configKeys = getPublicConfigKeys()
// and if any of our options match this
const configValues = _.pick(options, configKeys)
-3
View File
@@ -108,9 +108,6 @@ module.exports = {
// // strip off the unit part
// spec = path.relative("unit", spec)
// // now simply resolve this with our unitFolder
// // which makes it an absolute path
// path.join(config.unitFolder, spec)
debug('returning default path %s', spec)
return spec
+1 -1
View File
@@ -188,7 +188,7 @@ export function read (projectRoot, options: SettingsOptions = {}) {
return config
})
}).catch((err) => {
debug('an error occured when reading config', err)
debug('an error occurred when reading config', err)
if (errors.isCypressErr(err)) {
throw err
}
+1
View File
@@ -128,6 +128,7 @@
"@cypress/json-schemas": "5.39.0",
"@cypress/sinon-chai": "2.9.1",
"@ffprobe-installer/ffprobe": "1.1.0",
"@packages/config": "0.0.0-development",
"@packages/desktop-gui": "0.0.0-development",
"@packages/electron": "0.0.0-development",
"@packages/example": "0.0.0-development",
@@ -16,6 +16,8 @@ const pkg = require('@packages/root')
const detect = require('@packages/launcher/lib/detect')
const launch = require('@packages/launcher/lib/browsers')
const extension = require('@packages/extension')
const v = require('@packages/config/lib/validation')
const argsUtil = require(`${root}lib/util/args`)
const { fs } = require(`${root}lib/util/fs`)
const ciProvider = require(`${root}lib/util/ci_provider`)
@@ -43,7 +45,6 @@ const browserUtils = require(`${root}lib/browsers/utils`)
const chromeBrowser = require(`${root}lib/browsers/chrome`)
const { openProject } = require(`${root}lib/open_project`)
const env = require(`${root}lib/util/env`)
const v = require(`${root}lib/util/validation`)
const system = require(`${root}lib/util/system`)
const appData = require(`${root}lib/util/app_data`)
const electronApp = require('../../lib/util/electron-app')
+38 -44
View File
@@ -2,15 +2,16 @@ require('../spec_helper')
const _ = require('lodash')
const debug = require('debug')('test')
const Fixtures = require('@tooling/system-tests/lib/fixtures')
const config = require(`${root}lib/config`)
const errors = require(`${root}lib/errors`)
const configUtil = require(`${root}lib/util/config`)
const scaffold = require(`${root}lib/scaffold`)
const Fixtures = require('@tooling/system-tests/lib/fixtures')
let settings = require(`${root}lib/util/settings`)
describe('lib/config', () => {
beforeEach(function () {
before(function () {
this.env = process.env
process.env = _.omit(process.env, 'CYPRESS_DEBUG')
@@ -18,7 +19,7 @@ describe('lib/config', () => {
Fixtures.scaffold()
})
afterEach(function () {
after(function () {
process.env = this.env
})
@@ -986,18 +987,6 @@ describe('lib/config', () => {
})
})
context('.getConfigKeys', () => {
beforeEach(function () {
this.includes = (key) => {
expect(config.getConfigKeys()).to.include(key)
}
})
it('includes blockHosts', function () {
return this.includes('blockHosts')
})
})
context('.resolveConfigValues', () => {
beforeEach(function () {
this.expected = function (obj) {
@@ -1070,9 +1059,8 @@ describe('lib/config', () => {
cfg.projectRoot = '/foo/bar/'
return config.mergeDefaults(cfg, options)
.then((val) => val[prop])
.then((result) => {
expect(result).to.deep.eq(value)
.then((mergedConfig) => {
expect(mergedConfig[prop]).to.deep.eq(value)
})
}
})
@@ -1302,9 +1290,9 @@ describe('lib/config', () => {
})
it('can override socketId in options', () => {
return config.mergeDefaults({ projectRoot: '/foo/bar/' }, { socketId: 1234 })
return config.mergeDefaults({ projectRoot: '/foo/bar/' }, { socketId: '1234' })
.then((cfg) => {
expect(cfg.socketId).to.eq(1234)
expect(cfg.socketId).to.eq('1234')
})
})
@@ -1461,6 +1449,8 @@ describe('lib/config', () => {
projectId: { value: null, from: 'default' },
redirectionLimit: { value: 20, from: 'default' },
reporter: { value: 'json', from: 'cli' },
resolvedNodePath: { value: null, from: 'default' },
resolvedNodeVersion: { value: null, from: 'default' },
reporterOptions: { value: null, from: 'default' },
requestTimeout: { value: 5000, from: 'default' },
responseTimeout: { value: 30000, from: 'default' },
@@ -1469,6 +1459,7 @@ describe('lib/config', () => {
screenshotsFolder: { value: 'cypress/screenshots', from: 'default' },
slowTestThreshold: { value: 10000, from: 'default' },
supportFile: { value: 'cypress/support', from: 'default' },
supportFolder: { value: false, from: 'default' },
taskTimeout: { value: 60000, from: 'default' },
testFiles: { value: '**/*.*', from: 'default' },
trashAssetsBeforeRuns: { value: true, from: 'default' },
@@ -1567,6 +1558,8 @@ describe('lib/config', () => {
projectId: { value: 'projectId123', from: 'env' },
redirectionLimit: { value: 20, from: 'default' },
reporter: { value: 'spec', from: 'default' },
resolvedNodePath: { value: null, from: 'default' },
resolvedNodeVersion: { value: null, from: 'default' },
reporterOptions: { value: null, from: 'default' },
requestTimeout: { value: 5000, from: 'default' },
responseTimeout: { value: 30000, from: 'default' },
@@ -1575,6 +1568,7 @@ describe('lib/config', () => {
screenshotsFolder: { value: 'cypress/screenshots', from: 'default' },
slowTestThreshold: { value: 10000, from: 'default' },
supportFile: { value: 'cypress/support', from: 'default' },
supportFolder: { value: false, from: 'default' },
taskTimeout: { value: 60000, from: 'default' },
testFiles: { value: '**/*.*', from: 'default' },
trashAssetsBeforeRuns: { value: true, from: 'default' },
@@ -2015,6 +2009,12 @@ describe('lib/config', () => {
})
context('.setSupportFileAndFolder', () => {
const mockSupportDefaults = {
supportFile: 'cypress/support',
supportFolder: false,
configFile: 'cypress.json',
}
it('does nothing if supportFile is falsey', () => {
const obj = {
projectRoot: '/_test-output/path/to/project',
@@ -2034,7 +2034,7 @@ describe('lib/config', () => {
supportFile: 'test/unit/config_spec.js',
})
return config.setSupportFileAndFolder(obj)
return config.setSupportFileAndFolder(obj, mockSupportDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2052,7 +2052,7 @@ describe('lib/config', () => {
supportFile: 'cypress/support',
})
return config.setSupportFileAndFolder(obj)
return config.setSupportFileAndFolder(obj, mockSupportDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2070,7 +2070,7 @@ describe('lib/config', () => {
supportFile: 'cypress/support',
})
return config.setSupportFileAndFolder(obj)
return config.setSupportFileAndFolder(obj, mockSupportDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2087,7 +2087,7 @@ describe('lib/config', () => {
supportFile: 'does/not/exist',
})
return config.setSupportFileAndFolder(obj)
return config.setSupportFileAndFolder(obj, mockSupportDefaults)
.catch((err) => {
expect(err.message).to.include('The support file is missing or invalid.')
})
@@ -2108,7 +2108,7 @@ describe('lib/config', () => {
supportFile: 'cypress/support',
})
return config.setSupportFileAndFolder(obj)
return config.setSupportFileAndFolder(obj, mockSupportDefaults)
.then((result) => {
debug('result is', result)
@@ -2135,7 +2135,7 @@ describe('lib/config', () => {
supportFile: 'cypress/support.ts',
})
return config.setSupportFileAndFolder(obj)
return config.setSupportFileAndFolder(obj, mockSupportDefaults)
.then((result) => {
debug('result is', result)
@@ -2149,6 +2149,11 @@ describe('lib/config', () => {
})
context('.setPluginsFile', () => {
const mockPluginDefaults = {
pluginsFile: 'cypress/plugins',
configFile: 'cypress.json',
}
it('does nothing if pluginsFile is falsey', () => {
const obj = {
projectRoot: '/_test-output/path/to/project',
@@ -2168,7 +2173,7 @@ describe('lib/config', () => {
pluginsFile: `${projectRoot}/cypress/plugins`,
}
return config.setPluginsFile(obj)
return config.setPluginsFile(obj, mockPluginDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2185,7 +2190,7 @@ describe('lib/config', () => {
pluginsFile: `${projectRoot}/cypress/plugins`,
}
return config.setPluginsFile(obj)
return config.setPluginsFile(obj, mockPluginDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2209,7 +2214,7 @@ describe('lib/config', () => {
pluginsFile: pluginsFolder,
}
return config.setPluginsFile(obj)
return config.setPluginsFile(obj, mockPluginDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2226,7 +2231,7 @@ describe('lib/config', () => {
pluginsFile: `${projectRoot}/cypress/plugins`,
})
return config.setPluginsFile(obj)
return config.setPluginsFile(obj, mockPluginDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2243,7 +2248,7 @@ describe('lib/config', () => {
pluginsFile: 'does/not/exist',
}
return config.setPluginsFile(obj)
return config.setPluginsFile(obj, mockPluginDefaults)
.catch((err) => {
expect(err.message).to.include('The plugins file is missing or invalid.')
})
@@ -2264,7 +2269,7 @@ describe('lib/config', () => {
pluginsFile,
}
return config.setPluginsFile(obj)
return config.setPluginsFile(obj, mockPluginDefaults)
.then((result) => {
expect(result).to.eql({
projectRoot,
@@ -2309,17 +2314,6 @@ describe('lib/config', () => {
expect(config.setAbsolutePaths({})).to.deep.eq({})
})
// it "resolves fileServerFolder with projectRoot", ->
// obj = {
// projectRoot: "/_test-output/path/to/project"
// fileServerFolder: "foo"
// }
// expect(config.setAbsolutePaths(obj)).to.deep.eq({
// projectRoot: "/_test-output/path/to/project"
// fileServerFolder: "/_test-output/path/to/project/foo"
// })
it('does not mutate existing obj', () => {
const obj = {}
@@ -2337,7 +2331,7 @@ describe('lib/config', () => {
expect(config.setAbsolutePaths(obj)).to.deep.eq(obj)
})
return ['fileServerFolder', 'fixturesFolder', 'integrationFolder', 'unitFolder', 'supportFile', 'pluginsFile'].forEach((folder) => {
return ['fileServerFolder', 'fixturesFolder', 'integrationFolder', 'supportFile', 'pluginsFile'].forEach((folder) => {
it(`converts relative ${folder} to absolute path`, () => {
const obj = {
projectRoot: '/_test-output/path/to/project',
@@ -1,110 +0,0 @@
require('../spec_helper')
const snapshot = require('snap-shot-it')
const 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', []))
return snapshot('browsers list with a string', v.isValidBrowserList('browsers', ['foo']))
})
})
context('#isValidBrowser', () => {
it('passes valid browsers and forms error messages for invalid ones', () => {
const browsers = [
// valid browser
{
name: 'Chrome',
displayName: 'Chrome Browser',
family: 'chromium',
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: 'chromium',
path: '',
version: '99.101.3',
majorVersion: 99,
},
// invalid browser, missing displayName
{
name: 'No display name',
family: 'chromium',
},
{
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
return snapshot.apply(null, [v.isValidBrowser].concat(browsers))
})
})
context('#isOneOf', () => {
it('validates a string', () => {
const validate = v.isOneOf('foo', 'bar')
expect(validate).to.be.a('function')
expect(validate('test', 'foo')).to.be.true
expect(validate('test', 'bar')).to.be.true
// different value
let msg = validate('test', 'nope')
expect(msg).to.not.be.true
snapshot('not one of the strings error message', msg)
msg = validate('test', 42)
expect(msg).to.not.be.true
snapshot('number instead of string', msg)
msg = validate('test', null)
expect(msg).to.not.be.true
return snapshot('null instead of string', msg)
})
it('validates a number', () => {
const validate = v.isOneOf(1, 2, 3)
expect(validate).to.be.a('function')
expect(validate('test', 1)).to.be.true
expect(validate('test', 3)).to.be.true
// different value
let msg = validate('test', 4)
expect(msg).to.not.be.true
snapshot('not one of the numbers error message', msg)
msg = validate('test', 'foo')
expect(msg).to.not.be.true
snapshot('string instead of a number', msg)
msg = validate('test', null)
expect(msg).to.not.be.true
return snapshot('null instead of a number', msg)
})
})
})
@@ -46,7 +46,7 @@ We dynamically generated a new test to display this failure.
Pending: 0
Skipped: 0
Screenshots: 1
Video: true
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-invalid-browser.js
@@ -58,13 +58,6 @@ We dynamically generated a new test to display this failure.
ght error was detected outside of a test (failed).png
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/testConfigOverrides-invalid-bro (X second)
wser.js.mp4
====================================================================================================
(Run Finished)
@@ -142,3 +135,760 @@ exports['testConfigOverrides / has originalTitle when skip due to browser config
`
exports['testConfigOverrides / fails when passing invalid config values - [chrome,electron]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (testConfigOverrides-invalid.js)
Searched: cypress/integration/testConfigOverrides-invalid.js
Running: testConfigOverrides-invalid.js (1 of 1)
1) inline test config override throws error
2) inline test config override throws error when executed within cy cmd
context config overrides throws error
3) runs
nested contexts overrides throws error at the correct line number
4) 1st test fails on overrides
5) 2nd test fails on overrides
nested contexts
test override
6) throws error at the correct line number
does execute 2nd test
throws error correctly when before hook
7) "before all" hook for "test config override throws error"
throws error correctly when beforeEach hook
8) test config override throws error
1 passing
8 failing
1) inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`""\`
[stack trace lines]
2) inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"null"\`
[stack trace lines]
3) context config overrides throws error
runs:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
[stack trace lines]
4) nested contexts overrides throws error at the correct line number
1st test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
[stack trace lines]
5) nested contexts overrides throws error at the correct line number
2nd test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
[stack trace lines]
6) nested contexts
test override
throws error at the correct line number:
CypressError: The config override passed to your test has the following validation error:
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
Error
[stack trace lines]
7) throws error correctly when before hook
"before all" hook for "test config override throws error":
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Because this error occurred during a \`before all\` hook we are skipping the remaining tests in the current suite: \`throws error correctly when...\`
Error
[stack trace lines]
8) throws error correctly when beforeEach hook
test config override throws error:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
[stack trace lines]
(Results)
Tests: 9
Passing: 1
Failing: 8
Pending: 0
Skipped: 0
Screenshots: 2
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-invalid.js
(Screenshots)
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-invalid.js/inline test conf (1280x720)
ig override throws error (failed).png
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-invalid.js/inline test conf (1280x720)
ig override throws error when executed within cy cmd (failed).png
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
testConfigOverrides-invalid.js XX:XX 9 1 8 - -
1 of 1 failed (100%) XX:XX 9 1 8 - -
`
exports['testConfigOverrides / fails when passing invalid config values with beforeEach - [chrome,electron]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (testConfigOverrides-before-invalid.js)
Searched: cypress/integration/testConfigOverrides-before-invalid.js
Running: testConfigOverrides-before-invalid.js (1 of 1)
runs all tests
1) inline test config override throws error
2) inline test config override throws error when executed within cy cmd
context config overrides throws error
3) runs
nested contexts overrides throws error at the correct line number
4) 1st test fails on overrides
5) 2nd test fails on overrides
nested contexts
test override
6) throws error at the correct line number
does execute 2nd test
throws error correctly when before hook
7) "before all" hook for "test config override throws error"
throws error correctly when beforeEach hook
8) test config override throws error
1 passing
8 failing
1) runs all tests
inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`""\`
[stack trace lines]
2) runs all tests
inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"null"\`
[stack trace lines]
3) runs all tests
context config overrides throws error
runs:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
[stack trace lines]
4) runs all tests
nested contexts overrides throws error at the correct line number
1st test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
[stack trace lines]
5) runs all tests
nested contexts overrides throws error at the correct line number
2nd test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
[stack trace lines]
6) runs all tests
nested contexts
test override
throws error at the correct line number:
CypressError: The config override passed to your test has the following validation error:
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
Error
[stack trace lines]
7) runs all tests
throws error correctly when before hook
"before all" hook for "test config override throws error":
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Because this error occurred during a \`before all\` hook we are skipping the remaining tests in the current suite: \`throws error correctly when...\`
Error
[stack trace lines]
8) runs all tests
throws error correctly when beforeEach hook
test config override throws error:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
[stack trace lines]
(Results)
Tests: 9
Passing: 1
Failing: 8
Pending: 0
Skipped: 0
Screenshots: 2
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-before-invalid.js
(Screenshots)
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-before-invalid.js/runs all (1280x720)
tests -- inline test config override throws error (failed).png
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-before-invalid.js/runs all (1280x720)
tests -- inline test config override throws error when executed within cy cmd (f
ailed).png
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
testConfigOverrides-before-invalid. XX:XX 9 1 8 - -
js
1 of 1 failed (100%) XX:XX 9 1 8 - -
`
exports['testConfigOverrides / correctly fails when invalid config values for it.only [chrome,electron]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (testConfigOverrides-only-invalid.js)
Searched: cypress/integration/testConfigOverrides-only-invalid.js
Running: testConfigOverrides-only-invalid.js (1 of 1)
nested contexts
test override
1) throws error at the correct line number
0 passing
1 failing
1) nested contexts
test override
throws error at the correct line number:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
[stack trace lines]
(Results)
Tests: 1
Passing: 0
Failing: 1
Pending: 0
Skipped: 0
Screenshots: 0
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-only-invalid.js
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
testConfigOverrides-only-invalid.js XX:XX 1 - 1 - -
1 of 1 failed (100%) XX:XX 1 - 1 - -
`
exports['testConfigOverrides / fails when passing invalid config values - [firefox]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (testConfigOverrides-invalid.js)
Searched: cypress/integration/testConfigOverrides-invalid.js
Running: testConfigOverrides-invalid.js (1 of 1)
1) inline test config override throws error
2) inline test config override throws error when executed within cy cmd
context config overrides throws error
3) runs
nested contexts overrides throws error at the correct line number
4) 1st test fails on overrides
5) 2nd test fails on overrides
nested contexts
test override
6) throws error at the correct line number
does execute 2nd test
throws error correctly when before hook
7) "before all" hook for "test config override throws error"
throws error correctly when beforeEach hook
8) test config override throws error
1 passing
8 failing
1) inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`""\`
[stack trace lines]
2) inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"null"\`
[stack trace lines]
3) context config overrides throws error
runs:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
4) nested contexts overrides throws error at the correct line number
1st test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
5) nested contexts overrides throws error at the correct line number
2nd test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
6) nested contexts
test override
throws error at the correct line number:
CypressError: The config override passed to your test has the following validation error:
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
[stack trace lines]
7) throws error correctly when before hook
"before all" hook for "test config override throws error":
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Because this error occurred during a \`before all\` hook we are skipping the remaining tests in the current suite: \`throws error correctly when...\`
[stack trace lines]
8) throws error correctly when beforeEach hook
test config override throws error:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
(Results)
Tests: 9
Passing: 1
Failing: 8
Pending: 0
Skipped: 0
Screenshots: 2
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-invalid.js
(Screenshots)
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-invalid.js/inline test conf (1280x720)
ig override throws error (failed).png
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-invalid.js/inline test conf (1280x720)
ig override throws error when executed within cy cmd (failed).png
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
testConfigOverrides-invalid.js XX:XX 9 1 8 - -
1 of 1 failed (100%) XX:XX 9 1 8 - -
`
exports['testConfigOverrides / fails when passing invalid config values with beforeEach - [firefox]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (testConfigOverrides-before-invalid.js)
Searched: cypress/integration/testConfigOverrides-before-invalid.js
Running: testConfigOverrides-before-invalid.js (1 of 1)
runs all tests
1) inline test config override throws error
2) inline test config override throws error when executed within cy cmd
context config overrides throws error
3) runs
nested contexts overrides throws error at the correct line number
4) 1st test fails on overrides
5) 2nd test fails on overrides
nested contexts
test override
6) throws error at the correct line number
does execute 2nd test
throws error correctly when before hook
7) "before all" hook for "test config override throws error"
throws error correctly when beforeEach hook
8) test config override throws error
1 passing
8 failing
1) runs all tests
inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`""\`
[stack trace lines]
2) runs all tests
inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"null"\`
[stack trace lines]
3) runs all tests
context config overrides throws error
runs:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
4) runs all tests
nested contexts overrides throws error at the correct line number
1st test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
5) runs all tests
nested contexts overrides throws error at the correct line number
2nd test fails on overrides:
CypressError: The config override passed to your test has the following validation error:
Expected \`defaultCommandTimeout\` to be a number. Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
6) runs all tests
nested contexts
test override
throws error at the correct line number:
CypressError: The config override passed to your test has the following validation error:
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
[stack trace lines]
7) runs all tests
throws error correctly when before hook
"before all" hook for "test config override throws error":
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
Because this error occurred during a \`before all\` hook we are skipping the remaining tests in the current suite: \`throws error correctly when...\`
[stack trace lines]
8) runs all tests
throws error correctly when beforeEach hook
test config override throws error:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
(Results)
Tests: 9
Passing: 1
Failing: 8
Pending: 0
Skipped: 0
Screenshots: 2
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-before-invalid.js
(Screenshots)
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-before-invalid.js/runs all (1280x720)
tests -- inline test config override throws error (failed).png
- /XXX/XXX/XXX/cypress/screenshots/testConfigOverrides-before-invalid.js/runs all (1280x720)
tests -- inline test config override throws error when executed within cy cmd (f
ailed).png
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
testConfigOverrides-before-invalid. XX:XX 9 1 8 - -
js
1 of 1 failed (100%) XX:XX 9 1 8 - -
`
exports['testConfigOverrides / correctly fails when invalid config values for it.only [firefox]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (testConfigOverrides-only-invalid.js)
Searched: cypress/integration/testConfigOverrides-only-invalid.js
Running: testConfigOverrides-only-invalid.js (1 of 1)
nested contexts
test override
1) throws error at the correct line number
0 passing
1 failing
1) nested contexts
test override
throws error at the correct line number:
CypressError: The config override passed to your test has the following validation error:
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls. Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
(Results)
Tests: 1
Passing: 0
Failing: 1
Pending: 0
Skipped: 0
Screenshots: 0
Video: false
Duration: X seconds
Spec Ran: testConfigOverrides-only-invalid.js
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
testConfigOverrides-only-invalid.js XX:XX 1 - 1 - -
1 of 1 failed (100%) XX:XX 1 - 1 - -
`
-3
View File
@@ -72,7 +72,6 @@ const routeHandlers = {
req: 'postRunInstanceRequest@2.1.0',
resSchema: 'postRunInstanceResponse@2.1.0',
res: (req, res) => {
console.log(mockServerState.allSpecs.length, mockServerState.specs.length)
const response = {
...postRunInstanceResponse,
spec: mockServerState.specs.shift() || null,
@@ -80,8 +79,6 @@ const routeHandlers = {
totalInstances: mockServerState.allSpecs.length,
}
console.log('response', response)
return res.json(response)
},
},
+2 -1
View File
@@ -17,6 +17,7 @@ const Bluebird = require('bluebird')
const debug = require('debug')('cypress:system-tests')
const httpsProxy = require('@packages/https-proxy')
const Fixtures = require('./fixtures')
const { allowDestroy } = require(`@packages/server/lib/util/server_destroy`)
const cypress = require(`@packages/server/lib/cypress`)
const screenshots = require(`@packages/server/lib/screenshots`)
@@ -588,7 +589,7 @@ const localItFn = function (title: string, opts: ItOptions) {
throw new Error('systemTests.it(...) must be passed a title as the first argument')
}
// LOGIC FOR AUTOGENERATING DYNAMIC TESTS
// LOGIC FOR AUTO-GENERATING DYNAMIC TESTS
// - create multiple tests for each default browser
// - if browser is specified in options:
// ...skip the tests for each default browser if that browser
@@ -0,0 +1,60 @@
describe('runs all tests', () => {
before(() => {
})
const shouldNotExecute = () => {
throw new Error('Test Override validation should have failed & it block should not have executed.')
}
it('inline test config override throws error', () => {
Cypress.config('baseUrl', '')
})
it('inline test config override throws error when executed within cy cmd', () => {
cy.then(() => {
Cypress.config('baseUrl', 'null')
})
})
describe('context config overrides throws error', { retries: '1' }, () => {
it('runs', () => {
shouldNotExecute()
})
})
describe('nested contexts overrides throws error at the correct line number', { defaultCommandTimeout: '500' }, () => {
it('1st test fails on overrides', () => {
shouldNotExecute()
})
it('2nd test fails on overrides', () => {
shouldNotExecute()
})
})
describe('nested contexts ', () => {
describe('test override', () => {
it('throws error at the correct line number', { baseUrl: 'not_an_http_url' }, () => {
shouldNotExecute()
})
it('does execute 2nd test', () => {
expect(1).to.eq(1)
})
})
})
describe('throws error correctly when before hook', () => {
before(() => {})
it('test config override throws error', { retries: '1' }, () => {
shouldNotExecute()
})
})
describe('throws error correctly when beforeEach hook', () => {
beforeEach(() => {})
it('test config override throws error', { retries: '1' }, () => {
shouldNotExecute()
})
})
})
@@ -0,0 +1,55 @@
const shouldNotExecute = () => {
throw new Error('Test Override validation should have failed & it block should not have executed.')
}
it('inline test config override throws error', () => {
Cypress.config('baseUrl', '')
})
it('inline test config override throws error when executed within cy cmd', () => {
cy.then(() => {
Cypress.config('baseUrl', 'null')
})
})
describe('context config overrides throws error', { retries: '1' }, () => {
it('runs', () => {
shouldNotExecute()
})
})
describe('nested contexts overrides throws error at the correct line number', { defaultCommandTimeout: '500' }, () => {
it('1st test fails on overrides', () => {
shouldNotExecute()
})
it('2nd test fails on overrides', () => {
shouldNotExecute()
})
})
describe('nested contexts ', () => {
describe('test override', () => {
it('throws error at the correct line number', { baseUrl: 'not_an_http_url' }, () => {
shouldNotExecute()
})
it('does execute 2nd test', () => {
expect(1).to.eq(1)
})
})
})
describe('throws error correctly when before hook', () => {
before(() => {})
it('test config override throws error', { retries: '1' }, () => {
shouldNotExecute()
})
})
describe('throws error correctly when beforeEach hook', () => {
beforeEach(() => {})
it('test config override throws error', { retries: '1' }, () => {
shouldNotExecute()
})
})
@@ -0,0 +1,22 @@
const shouldNotExecute = () => {
throw new Error('Test Override validation should have failed & it block should not have executed.')
}
it('first should not run', () => {
shouldNotExecute()
})
describe('second should not run', () => {
it('test', () => {
shouldNotExecute()
})
})
describe('nested contexts ', { retries: '1' }, () => {
describe('test override', () => {
// eslint-disable-next-line mocha/no-exclusive-tests
it.only('throws error at the correct line number', { baseUrl: 'not_an_http_url' }, () => {
shouldNotExecute()
})
})
})
@@ -1,7 +1,7 @@
it('has expected resolvedNodePath and resolvedNodeVersion', () => {
expect(Cypress.config('nodeVersion')).to.eq('bundled')
expect(Cypress.config('resolvedNodePath')).to.be.undefined
expect(Cypress.config('resolvedNodePath')).to.be.null
expect(Cypress.config('resolvedNodeVersion')).to.eq(Cypress.env('expectedNodeVersion'))
})
+1 -1
View File
@@ -61,7 +61,7 @@ describe('e2e record', () => {
// spec 1
`POST /runs/${runId}/instances`,
// no instances/:id/tests becuase spec failed during eval
// no instances/:id/tests because spec failed during eval
`POST /instances/${instanceId}/results`,
'PUT /videos/video.mp4',
`PUT /instances/${instanceId}/stdout`,
+5 -3
View File
@@ -155,11 +155,13 @@ describe('e2e reporters', () => {
},
processEnv: {
MOCHA_COLORS: 1,
CI: 1,
CIRCLECI: true,
},
}).then((result) => {
expect(result.stdout.match(/passes inherited(.*)/)[1]).to.contain('\u001b[33m')
expect(result.stdout.match(/passes quickly(.*)/)[1]).not.to.contain('\u001b[33m')
expect(result.stdout.match(/passes slowly(.*)/)[1]).to.contain('\u001b[33m')
expect(result.stdout.match(/passes inherited(.*)/)[1], 'when verifying "passes inherited" test time colors').to.contain('\u001b[33m')
expect(result.stdout.match(/passes quickly(.*)/)[1], 'when verifying "passes quickly" test time colors').not.to.contain('\u001b[33m')
expect(result.stdout.match(/passes slowly(.*)/)[1], 'when verifying "passes slowly" test time color').to.contain('\u001b[33m')
})
})
})
+42 -1
View File
@@ -14,7 +14,9 @@ describe('testConfigOverrides', () => {
spec: 'testConfigOverrides-invalid-browser.js',
snapshot: true,
expectedExitCode: 1,
config: {
video: false,
},
})
systemTests.it('has originalTitle when skip due to browser config', {
@@ -30,4 +32,43 @@ describe('testConfigOverrides', () => {
expect(results.runs[0].tests[0].title).deep.eq(['suite', 'has invalid testConfigOverrides'])
},
})
// window.Error throws differently for firefox. break into
// browser permutations for snapshot comparisons
const permutations = [
['chrome', 'electron'],
['firefox'],
]
permutations.forEach((browserList) => {
systemTests.it(`fails when passing invalid config values - [${browserList}]`, {
spec: 'testConfigOverrides-invalid.js',
snapshot: true,
browser: browserList,
expectedExitCode: 8,
config: {
video: false,
},
})
systemTests.it(`fails when passing invalid config values with beforeEach - [${browserList}]`, {
spec: 'testConfigOverrides-before-invalid.js',
snapshot: true,
browser: browserList,
expectedExitCode: 8,
config: {
video: false,
},
})
systemTests.it(`correctly fails when invalid config values for it.only [${browserList}]`, {
spec: 'testConfigOverrides-only-invalid.js',
snapshot: true,
browser: browserList,
expectedExitCode: 1,
config: {
video: false,
},
})
})
})