diff --git a/package.json b/package.json index cca80030cf0b..11c78836947d 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "@types/send": "^0.17.1", "@types/sinon-chai": "3.2.3", "@types/through2": "^2.0.36", + "@types/underscore.string": "0.0.38", "@typescript-eslint/eslint-plugin": "4.18.0", "@typescript-eslint/parser": "4.18.0", "@urql/introspection": "^0.3.0", diff --git a/packages/config/package.json b/packages/config/package.json index 3b8fbb510566..e8c31a04093f 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -27,9 +27,11 @@ "debug": "^4.3.2", "fs-extra": "^9.1.0", "lodash": "^4.17.21", - "recast": "0.20.4" + "recast": "0.20.4", + "return-deep-diff": "0.4.0" }, "devDependencies": { + "@packages/errors": "0.0.0-development", "@packages/root": "0.0.0-development", "@packages/ts": "0.0.0-development", "@packages/types": "0.0.0-development", diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 95f8b23d9d7a..5b42e02b3041 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -2,4 +2,8 @@ // babel transforms, etc. into client-side usage of the config code export * from './browser' +export * from './project' + export { addProjectIdToCypressConfig, addToCypressConfig, addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions, defineConfigAvailable } from './ast-utils/addToCypressConfig' + +export * from './utils' diff --git a/packages/config/src/project/index.ts b/packages/config/src/project/index.ts new file mode 100644 index 000000000000..9c68ce2f7c03 --- /dev/null +++ b/packages/config/src/project/index.ts @@ -0,0 +1,133 @@ +import _ from 'lodash' +import Debug from 'debug' +import deepDiff from 'return-deep-diff' + +import errors, { ConfigValidationFailureInfo, CypressError } from '@packages/errors' +import type { + ResolvedFromConfig, TestingType, FullConfig, +} from '@packages/types' + +import { + validate, + validateNoBreakingConfig, +} from '../browser' +import { + setPluginResolvedOn, + mergeDefaults, +} from './utils' + +const debug = Debug('cypress:config:project') + +// TODO: any -> SetupFullConfigOptions in data-context/src/data/ProjectConfigManager.ts +export function setupFullConfigWithDefaults (obj: any = {}, getFilesByGlob: any): Promise { + debug('setting config object %o', obj) + let { projectRoot, projectName, config, envFile, options, cliConfig } = obj + + // just force config to be an object so we dont have to do as much + // work in our tests + if (config == null) { + config = {} + } + + debug('config is %o', config) + + // flatten the object's properties into the master config object + config.envFile = envFile + config.projectRoot = projectRoot + config.projectName = projectName + + // @ts-ignore + return mergeDefaults(config, options, cliConfig, getFilesByGlob) +} + +// TODO: update types from data-context/src/data/ProjectLifecycleManager.ts +// updateWithPluginValues(config: FullConfig, modifiedConfig: Partial, testingType: TestingType): FullConfig +export function updateWithPluginValues (cfg: FullConfig, modifiedConfig: any, testingType: TestingType): FullConfig { + if (!modifiedConfig) { + modifiedConfig = {} + } + + debug('updateWithPluginValues %o', { cfg, modifiedConfig }) + + // make sure every option returned from the plugins file + // passes our validation functions + validate(modifiedConfig, (validationResult: ConfigValidationFailureInfo | string) => { + let configFile = cfg.configFile! + + if (_.isString(validationResult)) { + return errors.throwErr('CONFIG_VALIDATION_MSG_ERROR', 'configFile', configFile, validationResult) + } + + return errors.throwErr('CONFIG_VALIDATION_ERROR', 'configFile', configFile, validationResult) + }) + + debug('validate that there is no breaking config options added by setupNodeEvents') + + function makeSetupError (cyError: CypressError) { + cyError.name = `Error running ${testingType}.setupNodeEvents()` + + return cyError + } + + validateNoBreakingConfig(modifiedConfig, errors.warning, (err, options) => { + throw makeSetupError(errors.get(err, options)) + }, testingType) + + validateNoBreakingConfig(modifiedConfig[testingType], errors.warning, (err, options) => { + throw makeSetupError(errors.get(err, { + ...options, + name: `${testingType}.${options.name}`, + })) + }, testingType) + + const originalResolvedBrowsers = _.cloneDeep(cfg?.resolved?.browsers) ?? { + value: cfg.browsers, + from: 'default', + } as ResolvedFromConfig + + const diffs = deepDiff(cfg, modifiedConfig, true) + + debug('config diffs %o', diffs) + + const userBrowserList = diffs && diffs.browsers && _.cloneDeep(diffs.browsers) + + if (userBrowserList) { + debug('user browser list %o', userBrowserList) + } + + // for each override go through + // and change the resolved values of cfg + // to point to the plugin + if (diffs) { + debug('resolved config before diffs %o', cfg.resolved) + setPluginResolvedOn(cfg.resolved, diffs) + debug('resolved config object %o', cfg.resolved) + } + + // merge cfg into overrides + const merged = _.defaultsDeep(diffs, cfg) + + debug('merged config object %o', merged) + + // the above _.defaultsDeep combines arrays, + // if diffs.browsers = [1] and cfg.browsers = [1, 2] + // then the merged result merged.browsers = [1, 2] + // which is NOT what we want + if (Array.isArray(userBrowserList) && userBrowserList.length) { + merged.browsers = userBrowserList + merged.resolved.browsers.value = userBrowserList + } + + if (modifiedConfig.browsers === null) { + // null breaks everything when merging lists + debug('replacing null browsers with original list %o', originalResolvedBrowsers) + merged.browsers = cfg.browsers + if (originalResolvedBrowsers) { + merged.resolved.browsers = originalResolvedBrowsers + } + } + + debug('merged plugins config %o', merged) + + return merged +} diff --git a/packages/config/src/project/types.ts b/packages/config/src/project/types.ts new file mode 100644 index 000000000000..d0cbc4ff8c7b --- /dev/null +++ b/packages/config/src/project/types.ts @@ -0,0 +1 @@ +export type Config = Record diff --git a/packages/config/src/project/utils.ts b/packages/config/src/project/utils.ts new file mode 100644 index 000000000000..be927d357086 --- /dev/null +++ b/packages/config/src/project/utils.ts @@ -0,0 +1,538 @@ +import Bluebird from 'bluebird' +import Debug from 'debug' +import fs from 'fs-extra' +import _ from 'lodash' +import path from 'path' + +import type { + ResolvedFromConfig, + ResolvedConfigurationOptionSource, +} from '@packages/types' +import errors, { ConfigValidationFailureInfo, CypressError } from '@packages/errors' + +import type { Config } from './types' + +import { + allowed, + getDefaultValues, + matchesConfigKey, + getPublicConfigKeys, + validate, + validateNoBreakingConfig, +} from '../browser' +import { hideKeys, setUrls, coerce } from '../utils' +import { options } from '../options' + +const debug = Debug('cypress:config:project:utils') + +const hideSpecialVals = function (val: string, key: string) { + if (_.includes(CYPRESS_SPECIAL_ENV_VARS, key)) { + return hideKeys(val) + } + + return val +} + +// an object with a few utility methods for easy stubbing from unit tests +export const utils = { + getProcessEnvVars (obj: NodeJS.ProcessEnv) { + return _.reduce(obj, (memo: Record, value: string | undefined, key: string) => { + if (!value) { + return memo + } + + if (isCypressEnvLike(key)) { + memo[removeEnvPrefix(key)] = coerce(value) + } + + return memo + }, {}) + }, + + resolveModule (name: string) { + return require.resolve(name) + }, + + // returns: + // false - if the file should not be set + // string - found filename + // null - if there is an error finding the file + discoverModuleFile (options: { + filename: string + projectRoot: string + }) { + debug('discover module file %o', options) + const { filename } = options + + // they have it explicitly set, so it should be there + return fs.pathExists(filename) + .then((found) => { + if (found) { + debug('file exists, assuming it will load') + + return filename + } + + debug('could not find %o', { filename }) + + return null + }) + }, +} + +const CYPRESS_ENV_PREFIX = 'CYPRESS_' + +const CYPRESS_ENV_PREFIX_LENGTH = CYPRESS_ENV_PREFIX.length + +export const CYPRESS_RESERVED_ENV_VARS = [ + 'CYPRESS_INTERNAL_ENV', +] + +export const CYPRESS_SPECIAL_ENV_VARS = [ + 'RECORD_KEY', +] + +const isCypressEnvLike = (key: string) => { + return _.chain(key) + .invoke('toUpperCase') + .startsWith(CYPRESS_ENV_PREFIX) + .value() && + !_.includes(CYPRESS_RESERVED_ENV_VARS, key) +} + +const removeEnvPrefix = (key: string) => { + return key.slice(CYPRESS_ENV_PREFIX_LENGTH) +} + +export function parseEnv (cfg: Record, cliEnvs: Record, resolved: Record = {}) { + const envVars: any = (resolved.env = {}) + + const resolveFrom = (from: string, obj = {}) => { + return _.each(obj, (val, key) => { + return envVars[key] = { + value: val, + from, + } + }) + } + + const configEnv = cfg.env != null ? cfg.env : {} + const envFile = cfg.envFile != null ? cfg.envFile : {} + let processEnvs = utils.getProcessEnvVars(process.env) || {} + + cliEnvs = cliEnvs != null ? cliEnvs : {} + + const configFromEnv = _.reduce(processEnvs, (memo: string[], val, key) => { + const cfgKey = matchesConfigKey(key) + + if (cfgKey) { + // only change the value if it hasn't been + // set by the CLI. override default + config + if (resolved[cfgKey] !== 'cli') { + cfg[cfgKey] = val + resolved[cfgKey] = { + value: val, + from: 'env', + } as ResolvedFromConfig + } + + memo.push(key) + } + + return memo + }, []) + + processEnvs = _.chain(processEnvs) + .omit(configFromEnv) + .mapValues(hideSpecialVals) + .value() + + resolveFrom('config', configEnv) + resolveFrom('envFile', envFile) + resolveFrom('env', processEnvs) + resolveFrom('cli', cliEnvs) + + // configEnvs is from cypress.config.{js,ts,mjs,cjs} + // envFile is from cypress.env.json + // processEnvs is from process env vars + // cliEnvs is from CLI arguments + return _.extend(configEnv, envFile, processEnvs, cliEnvs) +} + +// combines the default configuration object with values specified in the +// configuration file like "cypress.{ts|js}". Values in configuration file +// overwrite the defaults. +export function resolveConfigValues (config: Config, defaults: Record, resolved: any = {}) { + // pick out only known configuration keys + return _ + .chain(config) + .pick(getPublicConfigKeys()) + .mapValues((val, key) => { + const source = (s: ResolvedConfigurationOptionSource): ResolvedFromConfig => { + return { + value: val, + from: s, + } + } + + const r = resolved[key] + + if (r) { + if (_.isObject(r)) { + return r + } + + return source(r) + } + + if (_.isEqual(config[key], defaults[key]) || key === 'browsers') { + // "browsers" list is special, since it is dynamic by default + // and can only be overwritten via plugins file + return source('default') + } + + return source('config') + }) + .value() +} + +// Given an object "resolvedObj" and a list of overrides in "obj" +// marks all properties from "obj" inside "resolvedObj" using +// {value: obj.val, from: "plugin"} +export function setPluginResolvedOn (resolvedObj: Record, obj: Record): any { + return _.each(obj, (val, key) => { + if (_.isObject(val) && !_.isArray(val) && resolvedObj[key]) { + // recurse setting overrides + // inside of objected + return setPluginResolvedOn(resolvedObj[key], val) + } + + const valueFrom: ResolvedFromConfig = { + value: val, + from: 'plugin', + } + + resolvedObj[key] = valueFrom + }) +} + +export function setAbsolutePaths (obj: Config) { + obj = _.clone(obj) + + // if we have a projectRoot + const pr = obj.projectRoot + + if (pr) { + // reset fileServerFolder to be absolute + // obj.fileServerFolder = path.resolve(pr, obj.fileServerFolder) + + // and do the same for all the rest + _.extend(obj, convertRelativeToAbsolutePaths(pr, obj)) + } + + return obj +} + +const folders = _(options).filter({ isFolder: true }).map('name').value() + +const convertRelativeToAbsolutePaths = (projectRoot: string, obj: Config) => { + return _.reduce(folders, (memo: Record, folder) => { + const val = obj[folder] + + if ((val != null) && (val !== false)) { + memo[folder] = path.resolve(projectRoot, val) + } + + return memo + }, {}) +} + +// instead of the built-in Node process, specify a path to 3rd party Node +export const setNodeBinary = (obj: Config, userNodePath?: string, userNodeVersion?: string) => { + // if execPath isn't found we weren't executed from the CLI and should used the bundled node version. + if (userNodePath && userNodeVersion && obj.nodeVersion !== 'bundled') { + obj.resolvedNodePath = userNodePath + obj.resolvedNodeVersion = userNodeVersion + + return obj + } + + obj.resolvedNodeVersion = process.versions.node + + return obj +} + +export function relativeToProjectRoot (projectRoot: string, file: string) { + if (!file.startsWith(projectRoot)) { + return file + } + + // captures leading slash(es), both forward slash and back slash + const leadingSlashRe = /^[\/|\\]*(?![\/|\\])/ + + return file.replace(projectRoot, '').replace(leadingSlashRe, '') +} + +// async function +export async function setSupportFileAndFolder (obj: Config, getFilesByGlob: any) { + if (!obj.supportFile) { + return Bluebird.resolve(obj) + } + + obj = _.clone(obj) + + const supportFilesByGlob = await getFilesByGlob(obj.projectRoot, obj.supportFile) + + if (supportFilesByGlob.length > 1) { + return errors.throwErr('MULTIPLE_SUPPORT_FILES_FOUND', obj.supportFile, supportFilesByGlob) + } + + if (supportFilesByGlob.length === 0) { + if (obj.resolved.supportFile.from === 'default') { + return errors.throwErr('DEFAULT_SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, obj.supportFile)) + } + + return errors.throwErr('SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, obj.supportFile)) + } + + // TODO move this logic to find support file into util/path_helpers + const sf: string = supportFilesByGlob[0]! + + debug(`setting support file ${sf}`) + debug(`for project root ${obj.projectRoot}`) + + return Bluebird + .try(() => { + // resolve full path with extension + obj.supportFile = utils.resolveModule(sf) + + return debug('resolved support file %s', obj.supportFile) + }).then(() => { + if (!checkIfResolveChangedRootFolder(obj.supportFile, sf)) { + return + } + + debug('require.resolve switched support folder from %s to %s', sf, obj.supportFile) + // this means the path was probably symlinked, like + // /tmp/foo -> /private/tmp/foo + // which can confuse the rest of the code + // switch it back to "normal" file + const supportFileName = path.basename(obj.supportFile) + const base = sf?.endsWith(supportFileName) ? path.dirname(sf) : sf + + obj.supportFile = path.join(base || '', supportFileName) + + return fs.pathExists(obj.supportFile) + .then((found) => { + if (!found) { + errors.throwErr('SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, obj.supportFile)) + } + + return debug('switching to found file %s', obj.supportFile) + }) + }).catch({ code: 'MODULE_NOT_FOUND' }, () => { + debug('support JS module %s does not load', sf) + + return utils.discoverModuleFile({ + filename: sf, + projectRoot: obj.projectRoot, + }) + .then((result) => { + if (result === null) { + return errors.throwErr('SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, sf)) + } + + debug('setting support file to %o', { result }) + obj.supportFile = result + + return obj + }) + }) + .then(() => { + if (obj.supportFile) { + // set config.supportFolder to its directory + obj.supportFolder = path.dirname(obj.supportFile) + debug(`set support folder ${obj.supportFolder}`) + } + + return obj + }) +} + +export function mergeDefaults ( + config: Config = {}, + options: Record = {}, + cliConfig: Record = {}, + getFilesByGlob: any, +) { + const resolved: any = {} + const { testingType } = options + + config.rawJson = _.cloneDeep(config) + + _.extend(config, _.pick(options, 'configFile', 'morgan', 'isTextTerminal', 'socketId', 'report', 'browsers')) + debug('merged config with options, got %o', config) + + _ + .chain(allowed({ ...cliConfig, ...options })) + .omit('env') + .omit('browsers') + .each((val: any, key) => { + // If users pass in testing-type specific keys (eg, specPattern), + // we want to merge this with what we've read from the config file, + // rather than override it entirely. + if (typeof config[key] === 'object' && typeof val === 'object') { + if (Object.keys(val).length) { + resolved[key] = 'cli' + config[key] = { ...config[key], ...val } + } + } else { + resolved[key] = 'cli' + config[key] = val + } + }).value() + + let url = config.baseUrl + + if (url) { + // replace multiple slashes at the end of string to single slash + // so http://localhost/// will be http://localhost/ + // https://regexr.com/48rvt + config.baseUrl = url.replace(/\/\/+$/, '/') + } + + const defaultsForRuntime = getDefaultValues(options) + + _.defaultsDeep(config, defaultsForRuntime) + + let additionalIgnorePattern = config.additionalIgnorePattern + + if (testingType === 'component' && config.e2e && config.e2e.specPattern) { + additionalIgnorePattern = config.e2e.specPattern + } + + config = { + ...config, + ...config[testingType], + additionalIgnorePattern, + } + + // split out our own app wide env from user env variables + // and delete envFile + config.env = parseEnv(config, { ...cliConfig.env, ...options.env }, resolved) + + config.cypressEnv = process.env.CYPRESS_INTERNAL_ENV + debug('using CYPRESS_INTERNAL_ENV %s', config.cypressEnv) + if (!isValidCypressInternalEnvValue(config.cypressEnv)) { + throw errors.throwErr('INVALID_CYPRESS_INTERNAL_ENV', config.cypressEnv) + } + + delete config.envFile + + // when headless + if (config.isTextTerminal && !process.env.CYPRESS_INTERNAL_FORCE_FILEWATCH) { + // dont ever watch for file changes + config.watchForFileChanges = false + + // and forcibly reset numTestsKeptInMemory + // to zero + config.numTestsKeptInMemory = 0 + } + + config = setResolvedConfigValues(config, defaultsForRuntime, resolved) + + if (config.port) { + config = setUrls(config) + } + + // validate config again here so that we catch configuration errors coming + // from the CLI overrides or env var overrides + validate(_.omit(config, 'browsers'), (validationResult: ConfigValidationFailureInfo | string) => { + // return errors.throwErr('CONFIG_VALIDATION_ERROR', errMsg) + if (_.isString(validationResult)) { + return errors.throwErr('CONFIG_VALIDATION_MSG_ERROR', null, null, validationResult) + } + + return errors.throwErr('CONFIG_VALIDATION_ERROR', null, null, validationResult) + }) + + config = setAbsolutePaths(config) + + config = setNodeBinary(config, options.userNodePath, options.userNodeVersion) + + debug('validate that there is no breaking config options before setupNodeEvents') + + function makeConfigError (cyError: CypressError) { + cyError.name = `Obsolete option used in config object` + + return cyError + } + + validateNoBreakingConfig(config[testingType], errors.warning, (err, options) => { + throw makeConfigError(errors.get(err, { ...options, name: `${testingType}.${options.name}` })) + }, testingType) + + validateNoBreakingConfig(config, errors.warning, (err, ...args) => { + throw makeConfigError(errors.get(err, ...args)) + }, testingType) + + // TODO: https://github.com/cypress-io/cypress/issues/23093 + // testIsolation should equal 'strict' by default when experimentalSessionAndOrigin=true + // Once experimentalSessionAndOrigin is made GA, remove this logic and update the defaultValue + // to be be 'strict' + if (testingType === 'e2e' && config.experimentalSessionAndOrigin) { + if (config.rawJson.testIsolation) { + config.resolved.testIsolation.from = 'config' + } else { + config.testIsolation = 'strict' + config.resolved.testIsolation.value = 'strict' + config.resolved.testIsolation.from === 'default' + } + } + + // We need to remove the nested propertied by testing type because it has been + // flattened/compacted based on the current testing type that is selected + // making the config only available with the properties that are valid, + // also, having the correct values that can be used in the setupNodeEvents + delete config['e2e'] + delete config['component'] + delete config['resolved']['e2e'] + delete config['resolved']['component'] + + return setSupportFileAndFolder(config, getFilesByGlob) +} + +function isValidCypressInternalEnvValue (value: string) { + // names of config environments, see "config/app.yml" + const names = ['development', 'test', 'staging', 'production'] + + return _.includes(names, value) +} + +function setResolvedConfigValues (config: Config, defaults: any, resolved: any) { + const obj = _.clone(config) + + obj.resolved = resolveConfigValues(config, defaults, resolved) + debug('resolved config is %o', obj.resolved.browsers) + + return obj +} + +// require.resolve walks the symlinks, which can really change +// the results. For example +// /tmp/foo is symlink to /private/tmp/foo on Mac +// thus resolving /tmp/foo to find /tmp/foo/index.js +// can return /private/tmp/foo/index.js +// which can really confuse the rest of the code. +// Detect this switch by checking if the resolution of absolute +// paths moved the prefix +// +// Good case: no switcheroo, return false +// /foo/bar -> /foo/bar/index.js +// Bad case: return true +// /tmp/foo/bar -> /private/tmp/foo/bar/index.js +export const checkIfResolveChangedRootFolder = (resolved: string, initial: string) => { + return path.isAbsolute(resolved) && + path.isAbsolute(initial) && + !resolved.startsWith(initial) +} diff --git a/packages/server/lib/util/coerce.ts b/packages/config/src/utils.ts similarity index 64% rename from packages/server/lib/util/coerce.ts rename to packages/config/src/utils.ts index 5eb86ef6db38..be69b9524e92 100644 --- a/packages/server/lib/util/coerce.ts +++ b/packages/config/src/utils.ts @@ -1,5 +1,42 @@ import _ from 'lodash' -import toBoolean from 'underscore.string/toBoolean' +import { toBoolean } from 'underscore.string' +import * as uri from '@packages/network/lib/uri' + +export const hideKeys = (token?: string | number | boolean) => { + if (!token) { + return + } + + if (typeof token !== 'string') { + // maybe somehow we passes key=true? + // https://github.com/cypress-io/cypress/issues/14571 + return + } + + return [ + token.slice(0, 5), + token.slice(-5), + ].join('...') +} + +export function setUrls (obj: any) { + obj = _.clone(obj) + + // TODO: rename this to be proxyServer + const proxyUrl = `http://localhost:${obj.port}` + + const rootUrl = obj.baseUrl + ? uri.origin(obj.baseUrl) + : proxyUrl + + return { + ...obj, + proxyUrl, + browserUrl: rootUrl + obj.clientRoute, + reporterUrl: rootUrl + obj.reporterRoute, + xhrUrl: `${obj.namespace}${obj.xhrRoute}`, + } +} // https://github.com/cypress-io/cypress/issues/6810 const toArray = (value: any) => { @@ -36,7 +73,7 @@ const fromJson = (value: string) => { } } -export const coerce = (value: string) => { +export const coerce = (value: any) => { const num = _.toNumber(value) if (_.invoke(num, 'toString') === value) { @@ -63,3 +100,7 @@ export const coerce = (value: string) => { return value } + +export const isResolvedConfigPropDefault = (config: Record, prop: string) => { + return config.resolved[prop].from === 'default' +} diff --git a/packages/config/test/project/index.spec.ts b/packages/config/test/project/index.spec.ts new file mode 100644 index 000000000000..82350d2a115b --- /dev/null +++ b/packages/config/test/project/index.spec.ts @@ -0,0 +1,194 @@ +import { expect } from 'chai' + +import errors from '@packages/errors' + +import { updateWithPluginValues } from '../../src/project' + +describe('config/src/project/index', () => { + context('.updateWithPluginValues', () => { + it('is noop when no overrides', () => { + expect(updateWithPluginValues({ foo: 'bar' } as any, null as any, 'e2e')).to.deep.eq({ + foo: 'bar', + }) + }) + + it('is noop with empty overrides', () => { + expect(updateWithPluginValues({ foo: 'bar' } as any, {} as any, 'e2e')).to.deep.eq({ + foo: 'bar', + }) + }) + + it('updates resolved config values and returns config with overrides', () => { + const cfg = { + foo: 'bar', + baz: 'quux', + quux: 'foo', + lol: 1234, + env: { + a: 'a', + b: 'b', + }, + // previously resolved values + resolved: { + foo: { value: 'bar', from: 'default' }, + baz: { value: 'quux', from: 'cli' }, + quux: { value: 'foo', from: 'default' }, + lol: { value: 1234, from: 'env' }, + env: { + a: { value: 'a', from: 'config' }, + b: { value: 'b', from: 'config' }, + }, + }, + } + + const overrides = { + baz: 'baz', + quux: ['bar', 'quux'], + env: { + b: 'bb', + c: 'c', + }, + } + + expect(updateWithPluginValues(cfg as any, overrides, 'e2e')).to.deep.eq({ + foo: 'bar', + baz: 'baz', + lol: 1234, + quux: ['bar', 'quux'], + env: { + a: 'a', + b: 'bb', + c: 'c', + }, + resolved: { + foo: { value: 'bar', from: 'default' }, + baz: { value: 'baz', from: 'plugin' }, + quux: { value: ['bar', 'quux'], from: 'plugin' }, + lol: { value: 1234, from: 'env' }, + env: { + a: { value: 'a', from: 'config' }, + b: { value: 'bb', from: 'plugin' }, + c: { value: 'c', from: 'plugin' }, + }, + }, + }) + }) + + it('keeps the list of browsers if the plugins returns empty object', () => { + const browser = { + name: 'fake browser name', + family: 'chromium', + displayName: 'My browser', + version: 'x.y.z', + path: '/path/to/browser', + majorVersion: 'x', + } + + const cfg = { + browsers: [browser], + resolved: { + browsers: { + value: [browser], + from: 'default', + }, + }, + } + + const overrides = {} + + expect(updateWithPluginValues(cfg as any, overrides, 'e2e')).to.deep.eq({ + browsers: [browser], + resolved: { + browsers: { + value: [browser], + from: 'default', + }, + }, + }) + }) + + it('catches browsers=null returned from plugins', () => { + const browser = { + name: 'fake browser name', + family: 'chromium', + displayName: 'My browser', + version: 'x.y.z', + path: '/path/to/browser', + majorVersion: 'x', + } + + const cfg = { + projectRoot: '/foo/bar', + browsers: [browser], + resolved: { + browsers: { + value: [browser], + from: 'default', + }, + }, + } + + const overrides = { + browsers: null, + } + + sinon.stub(errors, 'throwErr') + updateWithPluginValues(cfg as any, overrides, 'e2e') + + expect(errors.throwErr).to.have.been.calledWith('CONFIG_VALIDATION_MSG_ERROR') + }) + + it('allows user to filter browsers', () => { + const browserOne = { + name: 'fake browser name', + family: 'chromium', + displayName: 'My browser', + version: 'x.y.z', + path: '/path/to/browser', + majorVersion: 'x', + } + const browserTwo = { + name: 'fake electron', + family: 'chromium', + displayName: 'Electron', + version: 'x.y.z', + // Electron browser is built-in, no external path + path: '', + majorVersion: 'x', + } + + const cfg = { + browsers: [browserOne, browserTwo], + resolved: { + browsers: { + value: [browserOne, browserTwo], + from: 'default', + }, + }, + } + + const overrides = { + browsers: [browserTwo], + } + + const updated = updateWithPluginValues(cfg as any, overrides, 'e2e') + + expect(updated.resolved, 'resolved values').to.deep.eq({ + browsers: { + value: [browserTwo], + from: 'plugin', + }, + }) + + expect(updated, 'all values').to.deep.eq({ + browsers: [browserTwo], + resolved: { + browsers: { + value: [browserTwo], + from: 'plugin', + }, + }, + }) + }) + }) +}) diff --git a/packages/config/test/project/utils.spec.ts b/packages/config/test/project/utils.spec.ts new file mode 100644 index 000000000000..3b8fab921591 --- /dev/null +++ b/packages/config/test/project/utils.spec.ts @@ -0,0 +1,1267 @@ +import '@packages/server/test/spec_helper' + +import _ from 'lodash' +import { expect } from 'chai' +import sinon from 'sinon' +import stripAnsi from 'strip-ansi' +import Debug from 'debug' +import os from 'node:os' + +import errors from '@packages/errors' +import Fixtures from '@tooling/system-tests' + +import { + checkIfResolveChangedRootFolder, + parseEnv, + utils, + resolveConfigValues, + setPluginResolvedOn, + setAbsolutePaths, + setNodeBinary, + relativeToProjectRoot, + setSupportFileAndFolder, + mergeDefaults, +} from '../../src/project/utils' +import path from 'node:path' + +const debug = Debug('test') + +describe('config/src/project/utils', () => { + before(function () { + this.env = process.env; + + (process as any).env = _.omit(process.env, 'CYPRESS_DEBUG') + + Fixtures.scaffold() + }) + + after(function () { + process.env = this.env + }) + + afterEach(() => { + sinon.restore() + }) + + describe('checkIfResolveChangedRootFolder', () => { + it('ignores non-absolute paths', () => { + expect(checkIfResolveChangedRootFolder('foo/index.js', 'foo')).to.be.false + }) + + it('handles paths that do not switch', () => { + expect(checkIfResolveChangedRootFolder('/foo/index.js', '/foo')).to.be.false + }) + + it('detects path switch', () => { + expect(checkIfResolveChangedRootFolder('/private/foo/index.js', '/foo')).to.be.true + }) + }) + + context('.getProcessEnvVars', () => { + it('returns process envs prefixed with cypress', () => { + const envs = { + CYPRESS_BASE_URL: 'value', + RANDOM_ENV: 'ignored', + } as unknown as NodeJS.ProcessEnv + + expect(utils.getProcessEnvVars(envs)).to.deep.eq({ + BASE_URL: 'value', + }) + }) + + it('does not return CYPRESS_RESERVED_ENV_VARS', () => { + const envs = { + CYPRESS_INTERNAL_ENV: 'value', + } as unknown as NodeJS.ProcessEnv + + expect(utils.getProcessEnvVars(envs)).to.deep.eq({}) + }); + + ['cypress_', 'CYPRESS_'].forEach((key) => { + it(`reduces key: ${key}`, () => { + const obj = { + cypress_host: 'http://localhost:8888', + foo: 'bar', + env: '123', + } as unknown as NodeJS.ProcessEnv + + obj[`${key}version`] = '0.12.0' + + expect(utils.getProcessEnvVars(obj)).to.deep.eq({ + host: 'http://localhost:8888', + version: '0.12.0', + }) + }) + }) + + it('does not merge reserved environment variables', () => { + const obj = { + CYPRESS_INTERNAL_ENV: 'production', + CYPRESS_FOO: 'bar', + CYPRESS_CRASH_REPORTS: '0', + CYPRESS_PROJECT_ID: 'abc123', + } as NodeJS.ProcessEnv + + expect(utils.getProcessEnvVars(obj)).to.deep.eq({ + FOO: 'bar', + PROJECT_ID: 'abc123', + CRASH_REPORTS: 0, + }) + }) + }) + + context('environment name check', () => { + it('throws an error for unknown CYPRESS_INTERNAL_ENV', async () => { + sinon.stub(errors, 'throwErr').withArgs('INVALID_CYPRESS_INTERNAL_ENV', 'foo-bar'); + (process as any).env.CYPRESS_INTERNAL_ENV = 'foo-bar' + const cfg = { + projectRoot: '/foo/bar/', + supportFile: false, + } + const options = {} + + const getFilesByGlob = sinon.stub().returns(['path/to/file']) + + try { + await mergeDefaults(cfg, options, {}, getFilesByGlob) + } catch { + // + } + + expect(errors.throwErr).have.been.calledOnce + }) + + it('allows production CYPRESS_INTERNAL_ENV', async () => { + sinon.stub(errors, 'throwErr') + process.env.CYPRESS_INTERNAL_ENV = 'production' + const cfg = { + projectRoot: '/foo/bar/', + supportFile: false, + } + const options = {} + + const getFilesByGlob = sinon.stub().returns(['path/to/file']) + + await mergeDefaults(cfg, options, {}, getFilesByGlob) + + expect(errors.throwErr).not.to.be.called + }) + }) + + context('.parseEnv', () => { + it('merges together env from config, env from file, env from process, and env from CLI', () => { + sinon.stub(utils, 'getProcessEnvVars').returns({ + version: '0.12.1', + user: 'bob', + }) + + const obj = { + env: { + version: '0.10.9', + project: 'todos', + host: 'localhost', + baz: 'quux', + }, + + envFile: { + host: 'http://localhost:8888', + user: 'brian', + foo: 'bar', + }, + } + + const envCLI = { + version: '0.14.0', + project: 'pie', + } + + expect(parseEnv(obj, envCLI)).to.deep.eq({ + version: '0.14.0', + project: 'pie', + host: 'http://localhost:8888', + user: 'bob', + foo: 'bar', + baz: 'quux', + }) + }) + }) + + context('.resolveConfigValues', () => { + beforeEach(function () { + this.expected = function (obj) { + const merged = resolveConfigValues(obj.config, obj.defaults, obj.resolved) + + expect(merged).to.deep.eq(obj.final) + } + }) + + it('sets baseUrl to default', function () { + return this.expected({ + config: { baseUrl: null }, + defaults: { baseUrl: null }, + resolved: {}, + final: { + baseUrl: { + value: null, + from: 'default', + }, + }, + }) + }) + + it('sets baseUrl to config', function () { + return this.expected({ + config: { baseUrl: 'localhost' }, + defaults: { baseUrl: null }, + resolved: {}, + final: { + baseUrl: { + value: 'localhost', + from: 'config', + }, + }, + }) + }) + + it('does not change existing resolved values', function () { + return this.expected({ + config: { baseUrl: 'localhost' }, + defaults: { baseUrl: null }, + resolved: { baseUrl: 'cli' }, + final: { + baseUrl: { + value: 'localhost', + from: 'cli', + }, + }, + }) + }) + + it('ignores values not found in configKeys', function () { + return this.expected({ + config: { baseUrl: 'localhost', foo: 'bar' }, + defaults: { baseUrl: null }, + resolved: { baseUrl: 'cli' }, + final: { + baseUrl: { + value: 'localhost', + from: 'cli', + }, + }, + }) + }) + }) + + context('.setPluginResolvedOn', () => { + it('resolves an object with single property', () => { + const cfg = {} + const obj = { + foo: 'bar', + } + + setPluginResolvedOn(cfg, obj) + + expect(cfg).to.deep.eq({ + foo: { + value: 'bar', + from: 'plugin', + }, + }) + }) + + it('resolves an object with multiple properties', () => { + const cfg = {} + const obj = { + foo: 'bar', + baz: [1, 2, 3], + } + + setPluginResolvedOn(cfg, obj) + + expect(cfg).to.deep.eq({ + foo: { + value: 'bar', + from: 'plugin', + }, + baz: { + value: [1, 2, 3], + from: 'plugin', + }, + }) + }) + + it('resolves a nested object', () => { + // we need at least the structure + const cfg = { + foo: { + bar: 1, + }, + } + const obj = { + foo: { + bar: 42, + }, + } + + setPluginResolvedOn(cfg, obj) + + expect(cfg, 'foo.bar gets value').to.deep.eq({ + foo: { + bar: { + value: 42, + from: 'plugin', + }, + }, + }) + }) + + // https://github.com/cypress-io/cypress/issues/7959 + it('resolves a single object', () => { + const cfg = { + } + const obj = { + foo: { + bar: { + baz: 42, + }, + }, + } + + setPluginResolvedOn(cfg, obj) + + expect(cfg).to.deep.eq({ + foo: { + from: 'plugin', + value: { + bar: { + baz: 42, + }, + }, + }, + }) + }) + }) + + context('_.defaultsDeep', () => { + it('merges arrays', () => { + // sanity checks to confirm how Lodash merges arrays in defaultsDeep + const diffs = { + list: [1], + } + const cfg = { + list: [1, 2], + } + const merged = _.defaultsDeep({}, diffs, cfg) + + expect(merged, 'arrays are combined').to.deep.eq({ + list: [1, 2], + }) + }) + }) + + context('.setAbsolutePaths', () => { + it('is noop without projectRoot', () => { + expect(setAbsolutePaths({})).to.deep.eq({}) + }) + + it('does not mutate existing obj', () => { + const obj = {} + + expect(setAbsolutePaths(obj)).not.to.eq(obj) + }) + + it('ignores non special *folder properties', () => { + const obj = { + projectRoot: '/_test-output/path/to/project', + blehFolder: 'some/rando/path', + foo: 'bar', + baz: 'quux', + } + + expect(setAbsolutePaths(obj)).to.deep.eq(obj) + }) + + return ['fileServerFolder', 'fixturesFolder'].forEach((folder) => { + it(`converts relative ${folder} to absolute path`, () => { + const obj = { + projectRoot: '/_test-output/path/to/project', + } + + obj[folder] = 'foo/bar' + + const expected = { + projectRoot: '/_test-output/path/to/project', + } + + expected[folder] = '/_test-output/path/to/project/foo/bar' + + expect(setAbsolutePaths(obj)).to.deep.eq(expected) + }) + }) + }) + + context('.setNodeBinary', () => { + beforeEach(function () { + this.nodeVersion = process.versions.node + }) + + it('sets bundled Node ver if nodeVersion != system', function () { + const obj = setNodeBinary({ + nodeVersion: 'bundled', + }) + + expect(obj).to.deep.eq({ + nodeVersion: 'bundled', + resolvedNodeVersion: this.nodeVersion, + }) + }) + + it('sets cli Node ver if nodeVersion = system', function () { + const obj = setNodeBinary({ + nodeVersion: 'system', + }, '/foo/bar/node', '1.2.3') + + expect(obj).to.deep.eq({ + nodeVersion: 'system', + resolvedNodeVersion: '1.2.3', + resolvedNodePath: '/foo/bar/node', + }) + }) + + it('sets bundled Node ver and if nodeVersion = system and userNodePath undefined', function () { + const obj = setNodeBinary({ + nodeVersion: 'system', + }, undefined, '1.2.3') + + expect(obj).to.deep.eq({ + nodeVersion: 'system', + resolvedNodeVersion: this.nodeVersion, + }) + }) + + it('sets bundled Node ver and if nodeVersion = system and userNodeVersion undefined', function () { + const obj = setNodeBinary({ + nodeVersion: 'system', + }, '/foo/bar/node') + + expect(obj).to.deep.eq({ + nodeVersion: 'system', + resolvedNodeVersion: this.nodeVersion, + }) + }) + }) + + describe('relativeToProjectRoot', () => { + context('posix', () => { + it('returns path of file relative to projectRoot', () => { + const projectRoot = '/root/projects' + const supportFile = '/root/projects/cypress/support/e2e.js' + + expect(relativeToProjectRoot(projectRoot, supportFile)).to.eq('cypress/support/e2e.js') + }) + }) + + context('windows', () => { + it('returns path of file relative to projectRoot', () => { + const projectRoot = `\\root\\projects` + const supportFile = `\\root\\projects\\cypress\\support\\e2e.js` + + expect(relativeToProjectRoot(projectRoot, supportFile)).to.eq(`cypress\\support\\e2e.js`) + }) + }) + }) + + context('.setSupportFileAndFolder', () => { + it('does nothing if supportFile is falsey', () => { + const obj = { + projectRoot: '/_test-output/path/to/project', + } + + const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + expect(result).to.eql(obj) + }) + }) + + it('sets the full path to the supportFile and supportFolder if it exists', () => { + const projectRoot = process.cwd() + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: 'test/project/utils.spec.ts', + }) + + const getFilesByGlob = sinon.stub().returns([path.join(projectRoot, obj.supportFile)]) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + expect(result).to.eql({ + projectRoot, + supportFile: `${projectRoot}/test/project/utils.spec.ts`, + supportFolder: `${projectRoot}/test/project`, + }) + }) + }) + + it('sets the supportFile to default e2e.js if it does not exist, support folder does not exist, and supportFile is the default', () => { + const projectRoot = Fixtures.projectPath('no-scaffolding') + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: 'cypress/support/e2e.js', + }) + + const getFilesByGlob = sinon.stub().returns([path.join(projectRoot, obj.supportFile)]) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + expect(result).to.eql({ + projectRoot, + supportFile: `${projectRoot}/cypress/support/e2e.js`, + supportFolder: `${projectRoot}/cypress/support`, + }) + }) + }) + + it('finds support file in project path that contains glob syntax', () => { + const projectRoot = Fixtures.projectPath('project-with-(glob)-[chars]') + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: 'cypress/support/e2e.js', + }) + + const getFilesByGlob = sinon.stub().returns([path.join(projectRoot, obj.supportFile)]) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + expect(result).to.eql({ + projectRoot, + supportFile: `${projectRoot}/cypress/support/e2e.js`, + supportFolder: `${projectRoot}/cypress/support`, + }) + }) + }) + + it('sets the supportFile to false if it does not exist, support folder exists, and supportFile is the default', () => { + const projectRoot = Fixtures.projectPath('empty-folders') + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: false, + }) + + const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + expect(result).to.eql({ + projectRoot, + supportFile: false, + }) + }) + }) + + it('throws error if supportFile is not default and does not exist', () => { + const projectRoot = process.cwd() + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: 'does/not/exist', + resolved: { + supportFile: { + value: 'does/not/exist', + from: 'default', + }, + }, + }) + + const getFilesByGlob = sinon.stub().returns([]) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .catch((err) => { + expect(stripAnsi(err.message)).to.include('Your project does not contain a default supportFile') + }) + }) + + it('sets the supportFile to index.ts if it exists (without ts require hook)', () => { + const projectRoot = Fixtures.projectPath('ts-proj') + const supportFolder = `${projectRoot}/cypress/support` + const supportFilename = `${supportFolder}/index.ts` + + const e: Error & { code?: string } = new Error('Cannot resolve TS file by default') + + e.code = 'MODULE_NOT_FOUND' + sinon.stub(utils, 'resolveModule').withArgs(supportFilename).throws(e) + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: 'cypress/support/index.ts', + }) + + const getFilesByGlob = sinon.stub().returns([path.join(projectRoot, obj.supportFile)]) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + debug('result is', result) + + expect(result).to.eql({ + projectRoot, + supportFolder, + supportFile: supportFilename, + }) + }) + }) + + it('uses custom TS supportFile if it exists (without ts require hook)', () => { + const projectRoot = Fixtures.projectPath('ts-proj-custom-names') + const supportFolder = `${projectRoot}/cypress` + const supportFilename = `${supportFolder}/support.ts` + + const e: Error & { code?: string } = new Error('Cannot resolve TS file by default') + + e.code = 'MODULE_NOT_FOUND' + sinon.stub(utils, 'resolveModule').withArgs(supportFilename).throws(e) + + const obj = setAbsolutePaths({ + projectRoot, + supportFile: 'cypress/support.ts', + }) + + const getFilesByGlob = sinon.stub().returns([path.join(projectRoot, obj.supportFile)]) + + return setSupportFileAndFolder(obj, getFilesByGlob) + .then((result) => { + debug('result is', result) + + expect(result).to.eql({ + projectRoot, + supportFolder, + supportFile: supportFilename, + }) + }) + }) + }) + + context('.mergeDefaults', () => { + beforeEach(function () { + this.getFilesByGlob = sinon.stub().returns(['path/to/file']) + + this.defaults = (prop, value, cfg: any = {}, options = {}) => { + cfg.projectRoot = '/foo/bar/' + + return mergeDefaults({ ...cfg, supportFile: cfg.supportFile ?? false }, options, {}, this.getFilesByGlob) + .then((mergedConfig) => { + expect(mergedConfig[prop]).to.deep.eq(value) + }) + } + }) + + it('slowTestThreshold=10000 for e2e', function () { + return this.defaults('slowTestThreshold', 10000, {}, { testingType: 'e2e' }) + }) + + it('slowTestThreshold=250 for component', function () { + return this.defaults('slowTestThreshold', 250, {}, { testingType: 'component' }) + }) + + it('port=null', function () { + return this.defaults('port', null) + }) + + it('projectId=null', function () { + return this.defaults('projectId', null) + }) + + it('autoOpen=false', function () { + return this.defaults('autoOpen', false) + }) + + it('browserUrl=http://localhost:2020/__/', function () { + return this.defaults('browserUrl', 'http://localhost:2020/__/', { port: 2020 }) + }) + + it('proxyUrl=http://localhost:2020', function () { + return this.defaults('proxyUrl', 'http://localhost:2020', { port: 2020 }) + }) + + it('namespace=__cypress', function () { + return this.defaults('namespace', '__cypress') + }) + + it('baseUrl=http://localhost:8000/app/', function () { + return this.defaults('baseUrl', 'http://localhost:8000/app/', { + baseUrl: 'http://localhost:8000/app///', + }) + }) + + it('baseUrl=http://localhost:8000/app/', function () { + return this.defaults('baseUrl', 'http://localhost:8000/app/', { + baseUrl: 'http://localhost:8000/app//', + }) + }) + + it('baseUrl=http://localhost:8000/app', function () { + return this.defaults('baseUrl', 'http://localhost:8000/app', { + baseUrl: 'http://localhost:8000/app', + }) + }) + + it('baseUrl=http://localhost:8000/', function () { + return this.defaults('baseUrl', 'http://localhost:8000/', { + baseUrl: 'http://localhost:8000//', + }) + }) + + it('baseUrl=http://localhost:8000/', function () { + return this.defaults('baseUrl', 'http://localhost:8000/', { + baseUrl: 'http://localhost:8000/', + }) + }) + + it('baseUrl=http://localhost:8000', function () { + return this.defaults('baseUrl', 'http://localhost:8000', { + baseUrl: 'http://localhost:8000', + }) + }) + + it('viewportWidth=1000', function () { + return this.defaults('viewportWidth', 1000) + }) + + it('viewportHeight=660', function () { + return this.defaults('viewportHeight', 660) + }) + + it('userAgent=null', function () { + return this.defaults('userAgent', null) + }) + + it('baseUrl=null', function () { + return this.defaults('baseUrl', null) + }) + + it('defaultCommandTimeout=4000', function () { + return this.defaults('defaultCommandTimeout', 4000) + }) + + it('pageLoadTimeout=60000', function () { + return this.defaults('pageLoadTimeout', 60000) + }) + + it('requestTimeout=5000', function () { + return this.defaults('requestTimeout', 5000) + }) + + it('responseTimeout=30000', function () { + return this.defaults('responseTimeout', 30000) + }) + + it('execTimeout=60000', function () { + return this.defaults('execTimeout', 60000) + }) + + it('waitForAnimations=true', function () { + return this.defaults('waitForAnimations', true) + }) + + it('scrollBehavior=start', function () { + return this.defaults('scrollBehavior', 'top') + }) + + it('animationDistanceThreshold=5', function () { + return this.defaults('animationDistanceThreshold', 5) + }) + + it('video=true', function () { + return this.defaults('video', true) + }) + + it('videoCompression=32', function () { + return this.defaults('videoCompression', 32) + }) + + it('videoUploadOnPasses=true', function () { + return this.defaults('videoUploadOnPasses', true) + }) + + it('trashAssetsBeforeRuns=32', function () { + return this.defaults('trashAssetsBeforeRuns', true) + }) + + it('morgan=true', function () { + return this.defaults('morgan', true) + }) + + it('isTextTerminal=false', function () { + return this.defaults('isTextTerminal', false) + }) + + it('socketId=null', function () { + return this.defaults('socketId', null) + }) + + it('reporter=spec', function () { + return this.defaults('reporter', 'spec') + }) + + it('watchForFileChanges=true', function () { + return this.defaults('watchForFileChanges', true) + }) + + it('numTestsKeptInMemory=50', function () { + return this.defaults('numTestsKeptInMemory', 50) + }) + + it('modifyObstructiveCode=true', function () { + return this.defaults('modifyObstructiveCode', true) + }) + + it('supportFile=false', function () { + return this.defaults('supportFile', false, { supportFile: false }) + }) + + it('blockHosts=null', function () { + return this.defaults('blockHosts', null) + }) + + it('blockHosts=[a,b]', function () { + return this.defaults('blockHosts', ['a', 'b'], { + blockHosts: ['a', 'b'], + }) + }) + + it('blockHosts=a|b', function () { + return this.defaults('blockHosts', ['a', 'b'], { + blockHosts: ['a', 'b'], + }) + }) + + it('hosts=null', function () { + return this.defaults('hosts', null) + }) + + it('hosts={}', function () { + return this.defaults('hosts', { + foo: 'bar', + baz: 'quux', + }, { + hosts: { + foo: 'bar', + baz: 'quux', + }, + }) + }) + + it('resets numTestsKeptInMemory to 0 when runMode', function () { + return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.numTestsKeptInMemory).to.eq(0) + }) + }) + + it('resets watchForFileChanges to false when runMode', function () { + return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.watchForFileChanges).to.be.false + }) + }) + + it('can override morgan in options', function () { + return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { morgan: false }, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.morgan).to.be.false + }) + }) + + it('can override isTextTerminal in options', function () { + return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.isTextTerminal).to.be.true + }) + }) + + it('can override socketId in options', function () { + return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { socketId: '1234' }, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.socketId).to.eq('1234') + }) + }) + + it('deletes envFile', function () { + const obj = { + projectRoot: '/foo/bar/', + supportFile: false, + env: { + foo: 'bar', + version: '0.5.2', + }, + envFile: { + bar: 'baz', + version: '1.0.1', + }, + } + + return mergeDefaults(obj, {}, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.env).to.deep.eq({ + foo: 'bar', + bar: 'baz', + version: '1.0.1', + }) + + expect(cfg.cypressEnv).to.eq(process.env['CYPRESS_INTERNAL_ENV']) + + expect(cfg).not.to.have.property('envFile') + }) + }) + + it('merges env into @env', function () { + const obj = { + projectRoot: '/foo/bar/', + supportFile: false, + env: { + host: 'localhost', + user: 'brian', + version: '0.12.2', + }, + } + + const options = { + env: { + version: '0.13.1', + foo: 'bar', + }, + } + + return mergeDefaults(obj, options, {}, this.getFilesByGlob) + .then((cfg) => { + expect(cfg.env).to.deep.eq({ + host: 'localhost', + user: 'brian', + version: '0.13.1', + foo: 'bar', + }) + }) + }) + + // @see https://github.com/cypress-io/cypress/issues/6892 + it('warns if experimentalGetCookiesSameSite is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalGetCookiesSameSite', true, { + experimentalGetCookiesSameSite: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_SAMESITE_REMOVED') + }) + + it('warns if experimentalSessionSupport is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalSessionSupport', true, { + experimentalSessionSupport: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_SESSION_SUPPORT_REMOVED') + }) + + it('warns if experimentalShadowDomSupport is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalShadowDomSupport', true, { + experimentalShadowDomSupport: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_SHADOW_DOM_REMOVED') + }) + + it('warns if experimentalRunEvents is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalRunEvents', true, { + experimentalRunEvents: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_RUN_EVENTS_REMOVED') + }) + + it('warns if experimentalStudio is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalStudio', true, { + experimentalStudio: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_STUDIO_REMOVED') + }) + + // @see https://github.com/cypress-io/cypress/pull/9185 + it('warns if experimentalNetworkStubbing is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalNetworkStubbing', true, { + experimentalNetworkStubbing: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_NETWORK_STUBBING_REMOVED') + }) + + it('warns if firefoxGcInterval is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('firefoxGcInterval', true, { + firefoxGcInterval: true, + }) + + expect(warning).to.be.calledWith('FIREFOX_GC_INTERVAL_REMOVED') + }) + + describe('.resolved', () => { + it('sets reporter and port to cli', () => { + const obj = { + projectRoot: '/foo/bar', + supportFile: false, + } + + const options = { + reporter: 'json', + port: 1234, + } + + const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) + + return mergeDefaults(obj, options, {}, getFilesByGlob) + .then((cfg) => { + expect(cfg.resolved).to.deep.eq({ + animationDistanceThreshold: { value: 5, from: 'default' }, + arch: { value: os.arch(), from: 'default' }, + baseUrl: { value: null, from: 'default' }, + blockHosts: { value: null, from: 'default' }, + browsers: { value: [], from: 'default' }, + chromeWebSecurity: { value: true, from: 'default' }, + clientCertificates: { value: [], from: 'default' }, + defaultCommandTimeout: { value: 4000, from: 'default' }, + downloadsFolder: { value: 'cypress/downloads', from: 'default' }, + env: {}, + execTimeout: { value: 60000, from: 'default' }, + experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, + experimentalFetchPolyfill: { value: false, from: 'default' }, + experimentalInteractiveRunEvents: { value: false, from: 'default' }, + experimentalSessionAndOrigin: { value: false, from: 'default' }, + experimentalSingleTabRunMode: { value: false, from: 'default' }, + experimentalSourceRewriting: { value: false, from: 'default' }, + fileServerFolder: { value: '', from: 'default' }, + fixturesFolder: { value: 'cypress/fixtures', from: 'default' }, + hosts: { value: null, from: 'default' }, + excludeSpecPattern: { value: '*.hot-update.js', from: 'default' }, + includeShadowDom: { value: false, from: 'default' }, + isInteractive: { value: true, from: 'default' }, + keystrokeDelay: { value: 0, from: 'default' }, + modifyObstructiveCode: { value: true, from: 'default' }, + nodeVersion: { value: undefined, from: 'default' }, + numTestsKeptInMemory: { value: 50, from: 'default' }, + pageLoadTimeout: { value: 60000, from: 'default' }, + platform: { value: os.platform(), from: 'default' }, + port: { value: 1234, from: 'cli' }, + 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' }, + retries: { value: { runMode: 0, openMode: 0 }, from: 'default' }, + screenshotOnRunFailure: { value: true, from: 'default' }, + screenshotsFolder: { value: 'cypress/screenshots', from: 'default' }, + specPattern: { value: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', from: 'default' }, + slowTestThreshold: { value: 10000, from: 'default' }, + supportFile: { value: false, from: 'config' }, + supportFolder: { value: false, from: 'default' }, + taskTimeout: { value: 60000, from: 'default' }, + testIsolation: { value: 'legacy', from: 'default' }, + trashAssetsBeforeRuns: { value: true, from: 'default' }, + userAgent: { value: null, from: 'default' }, + video: { value: true, from: 'default' }, + videoCompression: { value: 32, from: 'default' }, + videosFolder: { value: 'cypress/videos', from: 'default' }, + videoUploadOnPasses: { value: true, from: 'default' }, + viewportHeight: { value: 660, from: 'default' }, + viewportWidth: { value: 1000, from: 'default' }, + waitForAnimations: { value: true, from: 'default' }, + scrollBehavior: { value: 'top', from: 'default' }, + watchForFileChanges: { value: true, from: 'default' }, + }) + }) + }) + + it('sets config, envFile and env', () => { + sinon.stub(utils, 'getProcessEnvVars').returns({ + quux: 'quux', + RECORD_KEY: 'foobarbazquux', + PROJECT_ID: 'projectId123', + }) + + const obj = { + projectRoot: '/foo/bar', + supportFile: false, + baseUrl: 'http://localhost:8080', + port: 2020, + env: { + foo: 'foo', + }, + envFile: { + bar: 'bar', + }, + } + + const options = { + env: { + baz: 'baz', + }, + } + + const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) + + return mergeDefaults(obj, options, {}, getFilesByGlob) + .then((cfg) => { + expect(cfg.resolved).to.deep.eq({ + arch: { value: os.arch(), from: 'default' }, + animationDistanceThreshold: { value: 5, from: 'default' }, + baseUrl: { value: 'http://localhost:8080', from: 'config' }, + blockHosts: { value: null, from: 'default' }, + browsers: { value: [], from: 'default' }, + chromeWebSecurity: { value: true, from: 'default' }, + clientCertificates: { value: [], from: 'default' }, + defaultCommandTimeout: { value: 4000, from: 'default' }, + downloadsFolder: { value: 'cypress/downloads', from: 'default' }, + execTimeout: { value: 60000, from: 'default' }, + experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, + experimentalFetchPolyfill: { value: false, from: 'default' }, + experimentalInteractiveRunEvents: { value: false, from: 'default' }, + experimentalSessionAndOrigin: { value: false, from: 'default' }, + experimentalSingleTabRunMode: { value: false, from: 'default' }, + experimentalSourceRewriting: { value: false, from: 'default' }, + env: { + foo: { + value: 'foo', + from: 'config', + }, + bar: { + value: 'bar', + from: 'envFile', + }, + baz: { + value: 'baz', + from: 'cli', + }, + quux: { + value: 'quux', + from: 'env', + }, + RECORD_KEY: { + value: 'fooba...zquux', + from: 'env', + }, + }, + fileServerFolder: { value: '', from: 'default' }, + fixturesFolder: { value: 'cypress/fixtures', from: 'default' }, + hosts: { value: null, from: 'default' }, + excludeSpecPattern: { value: '*.hot-update.js', from: 'default' }, + includeShadowDom: { value: false, from: 'default' }, + isInteractive: { value: true, from: 'default' }, + keystrokeDelay: { value: 0, from: 'default' }, + modifyObstructiveCode: { value: true, from: 'default' }, + nodeVersion: { value: undefined, from: 'default' }, + numTestsKeptInMemory: { value: 50, from: 'default' }, + pageLoadTimeout: { value: 60000, from: 'default' }, + platform: { value: os.platform(), from: 'default' }, + port: { value: 2020, from: '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' }, + retries: { value: { runMode: 0, openMode: 0 }, from: 'default' }, + screenshotOnRunFailure: { value: true, from: 'default' }, + screenshotsFolder: { value: 'cypress/screenshots', from: 'default' }, + slowTestThreshold: { value: 10000, from: 'default' }, + specPattern: { value: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', from: 'default' }, + supportFile: { value: false, from: 'config' }, + supportFolder: { value: false, from: 'default' }, + taskTimeout: { value: 60000, from: 'default' }, + testIsolation: { value: 'legacy', from: 'default' }, + trashAssetsBeforeRuns: { value: true, from: 'default' }, + userAgent: { value: null, from: 'default' }, + video: { value: true, from: 'default' }, + videoCompression: { value: 32, from: 'default' }, + videosFolder: { value: 'cypress/videos', from: 'default' }, + videoUploadOnPasses: { value: true, from: 'default' }, + viewportHeight: { value: 660, from: 'default' }, + viewportWidth: { value: 1000, from: 'default' }, + waitForAnimations: { value: true, from: 'default' }, + scrollBehavior: { value: 'top', from: 'default' }, + watchForFileChanges: { value: true, from: 'default' }, + }) + }) + }) + + it('sets testIsolation=strict by default when experimentalSessionAndOrigin=true and e2e testing', () => { + sinon.stub(utils, 'getProcessEnvVars').returns({}) + + const obj = { + projectRoot: '/foo/bar', + supportFile: false, + baseUrl: 'http://localhost:8080', + experimentalSessionAndOrigin: true, + } + + const options = { + testingType: 'e2e', + } + + const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) + + return mergeDefaults(obj, options, {}, getFilesByGlob) + .then((cfg) => { + expect(cfg.resolved).to.have.property('experimentalSessionAndOrigin') + expect(cfg.resolved.experimentalSessionAndOrigin).to.deep.eq({ value: true, from: 'config' }) + expect(cfg.resolved).to.have.property('testIsolation') + expect(cfg.resolved.testIsolation).to.deep.eq({ value: 'strict', from: 'default' }) + }) + }) + + it('honors user config for testIsolation when experimentalSessionAndOrigin=true and e2e testing', () => { + sinon.stub(utils, 'getProcessEnvVars').returns({}) + + const obj = { + projectRoot: '/foo/bar', + supportFile: false, + baseUrl: 'http://localhost:8080', + experimentalSessionAndOrigin: true, + testIsolation: 'legacy', + } + + const options = { + testingType: 'e2e', + } + + const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) + + return mergeDefaults(obj, options, {}, getFilesByGlob) + .then((cfg) => { + expect(cfg.resolved).to.have.property('experimentalSessionAndOrigin') + expect(cfg.resolved.experimentalSessionAndOrigin).to.deep.eq({ value: true, from: 'config' }) + expect(cfg.resolved).to.have.property('testIsolation') + expect(cfg.resolved.testIsolation).to.deep.eq({ value: 'legacy', from: 'config' }) + }) + }) + }) + }) +}) diff --git a/packages/config/test/utils.spec.ts b/packages/config/test/utils.spec.ts new file mode 100644 index 000000000000..3f19939654ab --- /dev/null +++ b/packages/config/test/utils.spec.ts @@ -0,0 +1,166 @@ +import { expect } from 'chai' +import { + hideKeys, + setUrls, + coerce, + isResolvedConfigPropDefault, +} from '../src/utils' +import { + utils as projectUtils, +} from '../src/project/utils' + +describe('config/src/utils', () => { + describe('hideKeys', () => { + it('removes middle part of the string', () => { + const hidden = hideKeys('12345-xxxx-abcde') + + expect(hidden).to.equal('12345...abcde') + }) + + it('returns undefined for missing key', () => { + expect(hideKeys()).to.be.undefined + }) + + // https://github.com/cypress-io/cypress/issues/14571 + it('returns undefined for non-string argument', () => { + expect(hideKeys(true)).to.be.undefined + expect(hideKeys(1234)).to.be.undefined + }) + }) + + context('.setUrls', () => { + it('does not mutate existing obj', () => { + const obj = {} + + expect(setUrls(obj)).not.to.eq(obj) + }) + + it('uses baseUrl when set', () => { + const obj = { + port: 65432, + baseUrl: 'https://www.google.com', + clientRoute: '/__/', + } + + const urls = setUrls(obj) + + expect(urls.browserUrl).to.eq('https://www.google.com/__/') + expect(urls.proxyUrl).to.eq('http://localhost:65432') + }) + + it('strips baseUrl to host when set', () => { + const obj = { + port: 65432, + baseUrl: 'http://localhost:9999/app/?foo=bar#index.html', + clientRoute: '/__/', + } + + const urls = setUrls(obj) + + expect(urls.browserUrl).to.eq('http://localhost:9999/__/') + expect(urls.proxyUrl).to.eq('http://localhost:65432') + }) + }) + + context('coerce', () => { + beforeEach(function () { + this.env = process.env + }) + + afterEach(function () { + process.env = this.env + }) + + it('coerces string', () => { + expect(coerce('foo')).to.eq('foo') + }) + + it('coerces string from process.env', () => { + process.env['CYPRESS_STRING'] = 'bar' + const cypressEnvVar = projectUtils.getProcessEnvVars(process.env) + + expect(coerce(cypressEnvVar)).to.deep.include({ STRING: 'bar' }) + }) + + it('coerces number', () => { + expect(coerce('123')).to.eq(123) + }) + + // NOTE: When exporting shell variables, they are saved in `process.env` as strings, hence why + // all `process.env` variables are assigned as strings in these unit tests + it('coerces number from process.env', () => { + process.env['CYPRESS_NUMBER'] = '8000' + const cypressEnvVar = projectUtils.getProcessEnvVars(process.env) + + expect(coerce(cypressEnvVar)).to.deep.include({ NUMBER: 8000 }) + }) + + it('coerces boolean', () => { + expect(coerce('true')).to.be.true + }) + + it('coerces boolean from process.env', () => { + process.env['CYPRESS_BOOLEAN'] = 'false' + const cypressEnvVar = projectUtils.getProcessEnvVars(process.env) + + expect(coerce(cypressEnvVar)).to.deep.include({ BOOLEAN: false }) + }) + + // https://github.com/cypress-io/cypress/issues/8818 + it('coerces JSON string', () => { + expect(coerce('[{"type": "foo", "value": "bar"}, {"type": "fizz", "value": "buzz"}]')).to.deep.equal( + [{ 'type': 'foo', 'value': 'bar' }, { 'type': 'fizz', 'value': 'buzz' }], + ) + }) + + // https://github.com/cypress-io/cypress/issues/8818 + it('coerces JSON string from process.env', () => { + process.env['CYPRESS_stringified_json'] = '[{"type": "foo", "value": "bar"}, {"type": "fizz", "value": "buzz"}]' + const cypressEnvVar = projectUtils.getProcessEnvVars(process.env) + const coercedCypressEnvVar = coerce(cypressEnvVar) + + expect(coercedCypressEnvVar).to.have.keys('stringified_json') + expect(coercedCypressEnvVar['stringified_json']).to.deep.equal([{ 'type': 'foo', 'value': 'bar' }, { 'type': 'fizz', 'value': 'buzz' }]) + }) + + it('coerces array', () => { + expect(coerce('[foo,bar]')).to.have.members(['foo', 'bar']) + }) + + it('coerces array from process.env', () => { + process.env['CYPRESS_ARRAY'] = '[google.com,yahoo.com]' + const cypressEnvVar = projectUtils.getProcessEnvVars(process.env) + + const coercedCypressEnvVar = coerce(cypressEnvVar) + + expect(coercedCypressEnvVar).to.have.keys('ARRAY') + expect(coercedCypressEnvVar['ARRAY']).to.have.members(['google.com', 'yahoo.com']) + }) + + it('defaults value with multiple types to string', () => { + expect(coerce('123foo456')).to.eq('123foo456') + }) + }) + + context('.isResolvedConfigPropDefault', () => { + it('returns true if value is default value', () => { + const options = { + resolved: { + baseUrl: { from: 'default' }, + }, + } + + expect(isResolvedConfigPropDefault(options, 'baseUrl')).to.be.true + }) + + it('returns false if value is not default value', () => { + const options = { + resolved: { + baseUrl: { from: 'cli' }, + }, + } + + expect(isResolvedConfigPropDefault(options, 'baseUrl')).to.be.false + }) + }) +}) diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts index 1cb4dd4be1f8..301768987922 100644 --- a/packages/data-context/src/DataContext.ts +++ b/packages/data-context/src/DataContext.ts @@ -36,7 +36,7 @@ import type { App as ElectronApp } from 'electron' import { VersionsDataSource } from './sources/VersionsDataSource' import type { SocketIONamespace, SocketIOServer } from '@packages/socket' import { globalPubSub } from '.' -import { InjectedConfigApi, ProjectLifecycleManager } from './data/ProjectLifecycleManager' +import { ProjectLifecycleManager } from './data/ProjectLifecycleManager' import type { CypressError } from '@packages/errors' import { ErrorDataSource } from './sources/ErrorDataSource' import { GraphQLDataSource } from './sources/GraphQLDataSource' @@ -67,7 +67,6 @@ export interface DataContextConfig { appApi: AppApiShape localSettingsApi: LocalSettingsApiShape authApi: AuthApiShape - configApi: InjectedConfigApi projectApi: ProjectApiShape electronApi: ElectronApiShape browserApi: BrowserApiShape @@ -322,7 +321,6 @@ export class DataContext { appApi: this._config.appApi, authApi: this._config.authApi, browserApi: this._config.browserApi, - configApi: this._config.configApi, projectApi: this._config.projectApi, electronApi: this._config.electronApi, localSettingsApi: this._config.localSettingsApi, diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts index 0e51e92fbb70..258ff7209fe9 100644 --- a/packages/data-context/src/data/ProjectConfigManager.ts +++ b/packages/data-context/src/data/ProjectConfigManager.ts @@ -6,7 +6,14 @@ import debugLib from 'debug' import path from 'path' import _ from 'lodash' import chokidar from 'chokidar' -import { validate as validateConfig, validateNoBreakingConfigLaunchpad, validateNoBreakingConfigRoot, validateNoBreakingTestingTypeConfig } from '@packages/config' +import { + validate as validateConfig, + validateNoBreakingConfigLaunchpad, + validateNoBreakingConfigRoot, + validateNoBreakingTestingTypeConfig, + setupFullConfigWithDefaults, + updateWithPluginValues, +} from '@packages/config' import { CypressEnv } from './CypressEnv' import { autoBindDebug } from '../util/autoBindDebug' import type { EventRegistrar } from './EventRegistrar' @@ -310,7 +317,7 @@ export class ProjectConfigManager { const cypressEnv = await this.loadCypressEnvFile() const fullConfig = await this.buildBaseFullConfig(loadConfigReply.initialConfig, cypressEnv, this.options.ctx.modeOptions) - const finalConfig = this._cachedFullConfig = this.options.ctx._apis.configApi.updateWithPluginValues(fullConfig, result.setupConfig ?? {}, this._testingType ?? 'e2e') + const finalConfig = this._cachedFullConfig = updateWithPluginValues(fullConfig, result.setupConfig ?? {}, this._testingType ?? 'e2e') // Check if the config file has a before:browser:launch task, and if it's the case // we should restart the browser if it is open @@ -474,7 +481,7 @@ export class ProjectConfigManager { configFileContents = { ...configFileContents, ...testingTypeOverrides, ...optionsOverrides } // TODO: Convert this to be synchronous, it's just FS checks - let fullConfig = await this.options.ctx._apis.configApi.setupFullConfigWithDefaults({ + let fullConfig = await setupFullConfigWithDefaults({ cliConfig: options.config ?? {}, projectName: path.basename(this.options.projectRoot), projectRoot: this.options.projectRoot, @@ -485,7 +492,8 @@ export class ProjectConfigManager { testingType: this._testingType, configFile: path.basename(this.configFilePath), }, - }) + configFile: this.options.ctx.lifecycleManager.configFile, + }, this.options.ctx.file.getFilesByGlob) if (withBrowsers) { const browsers = await this.options.ctx.browser.machineBrowsers() diff --git a/packages/data-context/test/unit/helper.ts b/packages/data-context/test/unit/helper.ts index aba9be71f5cd..3d3e94cfca46 100644 --- a/packages/data-context/test/unit/helper.ts +++ b/packages/data-context/test/unit/helper.ts @@ -9,7 +9,6 @@ import { graphqlSchema } from '@packages/graphql/src/schema' import { remoteSchemaWrapped as schemaCloud } from '@packages/graphql/src/stitching/remoteSchemaWrapped' import type { BrowserApiShape } from '../../src/sources/BrowserDataSource' import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../src/actions' -import { InjectedConfigApi } from '../../src/data' import sinon from 'sinon' import { execute, parse } from 'graphql' import { getOperationName } from '@urql/core' @@ -50,7 +49,6 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run', logIn: sinon.stub().throws('not stubbed'), resetAuthState: sinon.stub(), } as unknown as AuthApiShape, - configApi: {} as InjectedConfigApi, projectApi: { closeActiveProject: sinon.stub(), insertProjectToCache: sinon.stub().resolves(), diff --git a/packages/driver/package.json b/packages/driver/package.json index e7d222c041e5..8099cb4ebc65 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -33,7 +33,6 @@ "@types/jquery.scrollto": "1.4.29", "@types/mocha": "^8.0.3", "@types/sinonjs__fake-timers": "8.1.1", - "@types/underscore.string": "0.0.38", "angular": "1.8.0", "basic-auth": "2.0.1", "blob-util": "2.0.2", diff --git a/packages/network/lib/uri.ts b/packages/network/lib/uri.ts index e7e8d83f7b18..9e598013c367 100644 --- a/packages/network/lib/uri.ts +++ b/packages/network/lib/uri.ts @@ -13,21 +13,23 @@ import url, { URL } from 'url' const DEFAULT_PROTOCOL_PORTS = { 'https:': '443', 'http:': '80', -} +} as const + +type Protocols = keyof typeof DEFAULT_PROTOCOL_PORTS -const DEFAULT_PORTS = _.values(DEFAULT_PROTOCOL_PORTS) +const DEFAULT_PORTS = _.values(DEFAULT_PROTOCOL_PORTS) as string[] -const portIsDefault = (port) => { +const portIsDefault = (port: string | null) => { return port && DEFAULT_PORTS.includes(port) } -const parseClone = (urlObject) => { +const parseClone = (urlObject: any) => { return url.parse(_.clone(urlObject)) } export const parse = url.parse -export function stripProtocolAndDefaultPorts (urlToCheck) { +export function stripProtocolAndDefaultPorts (urlToCheck: string) { // grab host which is 'hostname:port' only const { host, hostname, port } = url.parse(urlToCheck) @@ -41,7 +43,7 @@ export function stripProtocolAndDefaultPorts (urlToCheck) { return host } -export function removePort (urlObject) { +export function removePort (urlObject: any) { const parsed = parseClone(urlObject) // set host to undefined else url.format(...) will ignore the port property @@ -55,7 +57,7 @@ export function removePort (urlObject) { return parsed } -export function removeDefaultPort (urlToCheck) { +export function removeDefaultPort (urlToCheck: any) { let parsed = parseClone(urlToCheck) if (portIsDefault(parsed.port)) { @@ -65,7 +67,7 @@ export function removeDefaultPort (urlToCheck) { return parsed } -export function addDefaultPort (urlToCheck) { +export function addDefaultPort (urlToCheck: any) { const parsed = parseClone(urlToCheck) if (!parsed.port) { @@ -74,7 +76,7 @@ export function addDefaultPort (urlToCheck) { /* @ts-ignore */ delete parsed.host if (parsed.protocol) { - parsed.port = DEFAULT_PROTOCOL_PORTS[parsed.protocol] + parsed.port = DEFAULT_PROTOCOL_PORTS[parsed.protocol as Protocols] } else { /* @ts-ignore */ delete parsed.port @@ -84,7 +86,7 @@ export function addDefaultPort (urlToCheck) { return parsed } -export function getPath (urlToCheck) { +export function getPath (urlToCheck: string) { return url.parse(urlToCheck).path } @@ -103,3 +105,15 @@ export function isLocalhost (url: URL) { || localhostIPRegex.test(url.hostname) ) } + +export function origin (urlStr: string) { + const parsed = url.parse(urlStr) + + parsed.hash = null + parsed.search = null + parsed.query = null + parsed.path = null + parsed.pathname = null + + return url.format(parsed) +} diff --git a/packages/network/test/unit/uri_spec.ts b/packages/network/test/unit/uri_spec.ts index 3d8fe82c9568..3f648f60fc2e 100644 --- a/packages/network/test/unit/uri_spec.ts +++ b/packages/network/test/unit/uri_spec.ts @@ -37,4 +37,23 @@ describe('lib/uri', () => { expect(uri.isLocalhost(new URL('https:foobar.com'))).to.be.false }) }) + + context('.origin', () => { + it('strips everything but the remote origin', () => { + expect( + uri.origin('http://localhost:9999/foo/bar?baz=quux#/index.html'), + 'http://localhost:9999', + ) + + expect( + uri.origin('https://www.google.com/'), + 'https://www.google.com', + ) + + expect( + uri.origin('https://app.foobar.co.uk:1234/a=b'), + 'https://app.foobar.co.uk:1234', + ) + }) + }) }) diff --git a/packages/server/lib/config.ts b/packages/server/lib/config.ts index 238d51d5dcfd..0a31a6beb673 100644 --- a/packages/server/lib/config.ts +++ b/packages/server/lib/config.ts @@ -1,610 +1,12 @@ -import Bluebird from 'bluebird' -import Debug from 'debug' import _ from 'lodash' -import path from 'path' -import deepDiff from 'return-deep-diff' -import type { ResolvedFromConfig, ResolvedConfigurationOptionSource, TestingType } from '@packages/types' +import type { ResolvedFromConfig } from '@packages/types' import * as configUtils from '@packages/config' -import * as errors from './errors' -import { getProcessEnvVars, CYPRESS_SPECIAL_ENV_VARS } from './util/config' -import { fs } from './util/fs' -import keys from './util/keys' -import origin from './util/origin' -import pathHelpers from './util/path_helpers' -import type { ConfigValidationFailureInfo, CypressError } from '@packages/errors' +export const setupFullConfigWithDefaults = configUtils.setupFullConfigWithDefaults -import { getCtx } from './makeDataContext' +export const updateWithPluginValues = configUtils.updateWithPluginValues -const debug = Debug('cypress:server:config') - -const folders = _(configUtils.options).filter({ isFolder: true }).map('name').value() - -const convertRelativeToAbsolutePaths = (projectRoot, obj) => { - return _.reduce(folders, (memo, folder) => { - const val = obj[folder] - - if ((val != null) && (val !== false)) { - memo[folder] = path.resolve(projectRoot, val) - } - - return memo - } - , {}) -} - -const hideSpecialVals = function (val, key) { - if (_.includes(CYPRESS_SPECIAL_ENV_VARS, key)) { - return keys.hide(val) - } - - return val -} - -// an object with a few utility methods for easy stubbing from unit tests -export const utils = { - resolveModule (name) { - return require.resolve(name) - }, - - // returns: - // false - if the file should not be set - // string - found filename - // null - if there is an error finding the file - discoverModuleFile (options) { - debug('discover module file %o', options) - const { filename } = options - - // they have it explicitly set, so it should be there - return fs.pathExists(filename) - .then((found) => { - if (found) { - debug('file exists, assuming it will load') - - return filename - } - - debug('could not find %o', { filename }) - - return null - }) - }, -} - -export function isValidCypressInternalEnvValue (value) { - // names of config environments, see "config/app.yml" - const names = ['development', 'test', 'staging', 'production'] - - return _.includes(names, value) -} - -export function setupFullConfigWithDefaults (obj: Record = {}) { - debug('setting config object %o', obj) - let { projectRoot, projectName, config, envFile, options, cliConfig } = obj - - // just force config to be an object so we dont have to do as much - // work in our tests - if (config == null) { - config = {} - } - - debug('config is %o', config) - - // flatten the object's properties into the master config object - config.envFile = envFile - config.projectRoot = projectRoot - config.projectName = projectName - - return mergeDefaults(config, options, cliConfig) -} - -export function mergeDefaults ( - config: Record = {}, - options: Record = {}, - cliConfig: Record = {}, -) { - const resolved = {} - const { testingType } = options - - config.rawJson = _.cloneDeep(config) - - _.extend(config, _.pick(options, 'configFile', 'morgan', 'isTextTerminal', 'socketId', 'report', 'browsers')) - debug('merged config with options, got %o', config) - - _ - .chain(configUtils.allowed({ ...cliConfig, ...options })) - .omit('env') - .omit('browsers') - .each((val: any, key) => { - // If users pass in testing-type specific keys (eg, specPattern), - // we want to merge this with what we've read from the config file, - // rather than override it entirely. - if (typeof config[key] === 'object' && typeof val === 'object') { - if (Object.keys(val).length) { - resolved[key] = 'cli' - config[key] = { ...config[key], ...val } - } - } else { - resolved[key] = 'cli' - config[key] = val - } - }).value() - - let url = config.baseUrl - - if (url) { - // replace multiple slashes at the end of string to single slash - // so http://localhost/// will be http://localhost/ - // https://regexr.com/48rvt - config.baseUrl = url.replace(/\/\/+$/, '/') - } - - const defaultsForRuntime = configUtils.getDefaultValues(options) - - _.defaultsDeep(config, defaultsForRuntime) - - let additionalIgnorePattern = config.additionalIgnorePattern - - if (testingType === 'component' && config.e2e && config.e2e.specPattern) { - additionalIgnorePattern = config.e2e.specPattern - } - - config = { - ...config, - ...config[testingType], - additionalIgnorePattern, - } - - // split out our own app wide env from user env variables - // and delete envFile - config.env = parseEnv(config, { ...cliConfig.env, ...options.env }, resolved) - - config.cypressEnv = process.env.CYPRESS_INTERNAL_ENV - debug('using CYPRESS_INTERNAL_ENV %s', config.cypressEnv) - if (!isValidCypressInternalEnvValue(config.cypressEnv)) { - throw errors.throwErr('INVALID_CYPRESS_INTERNAL_ENV', config.cypressEnv) - } - - delete config.envFile - - // when headless - if (config.isTextTerminal && !process.env.CYPRESS_INTERNAL_FORCE_FILEWATCH) { - // dont ever watch for file changes - config.watchForFileChanges = false - - // and forcibly reset numTestsKeptInMemory - // to zero - config.numTestsKeptInMemory = 0 - } - - config = setResolvedConfigValues(config, defaultsForRuntime, resolved) - - if (config.port) { - config = setUrls(config) - } - - // validate config again here so that we catch configuration errors coming - // from the CLI overrides or env var overrides - configUtils.validate(_.omit(config, 'browsers'), (validationResult: ConfigValidationFailureInfo | string) => { - // return errors.throwErr('CONFIG_VALIDATION_ERROR', errMsg) - if (_.isString(validationResult)) { - return errors.throwErr('CONFIG_VALIDATION_MSG_ERROR', null, null, validationResult) - } - - return errors.throwErr('CONFIG_VALIDATION_ERROR', null, null, validationResult) - }) - - config = setAbsolutePaths(config) - - config = setNodeBinary(config, options.userNodePath, options.userNodeVersion) - - debug('validate that there is no breaking config options before setupNodeEvents') - - function makeConfigError (cyError: CypressError) { - cyError.name = `Obsolete option used in config object` - - return cyError - } - - configUtils.validateNoBreakingConfig(config[testingType], errors.warning, (err, options) => { - throw makeConfigError(errors.get(err, { ...options, name: `${testingType}.${options.name}` })) - }, testingType) - - configUtils.validateNoBreakingConfig(config, errors.warning, (err, ...args) => { - throw makeConfigError(errors.get(err, ...args)) - }, testingType) - - // TODO: https://github.com/cypress-io/cypress/issues/23093 - // testIsolation should equal 'strict' by default when experimentalSessionAndOrigin=true - // Once experimentalSessionAndOrigin is made GA, remove this logic and update the defaultValue - // to be be 'strict' - if (testingType === 'e2e' && config.experimentalSessionAndOrigin) { - if (config.rawJson.testIsolation) { - config.resolved.testIsolation.from = 'config' - } else { - config.testIsolation = 'strict' - config.resolved.testIsolation.value = 'strict' - config.resolved.testIsolation.from === 'default' - } - } - - // We need to remove the nested propertied by testing type because it has been - // flattened/compacted based on the current testing type that is selected - // making the config only available with the properties that are valid, - // also, having the correct values that can be used in the setupNodeEvents - delete config['e2e'] - delete config['component'] - delete config['resolved']['e2e'] - delete config['resolved']['component'] - - return setSupportFileAndFolder(config) -} - -export function setResolvedConfigValues (config, defaults, resolved) { - const obj = _.clone(config) - - obj.resolved = resolveConfigValues(config, defaults, resolved) - debug('resolved config is %o', obj.resolved.browsers) - - return obj -} - -// Given an object "resolvedObj" and a list of overrides in "obj" -// marks all properties from "obj" inside "resolvedObj" using -// {value: obj.val, from: "plugin"} -export function setPluginResolvedOn (resolvedObj: Record, obj: Record) { - return _.each(obj, (val, key) => { - if (_.isObject(val) && !_.isArray(val) && resolvedObj[key]) { - // recurse setting overrides - // inside of objected - return setPluginResolvedOn(resolvedObj[key], val) - } - - const valueFrom: ResolvedFromConfig = { - value: val, - from: 'plugin', - } - - resolvedObj[key] = valueFrom - }) -} - -export function updateWithPluginValues (cfg, overrides, testingType: TestingType) { - if (!overrides) { - overrides = {} - } - - debug('updateWithPluginValues %o', { cfg, overrides }) - - // make sure every option returned from the plugins file - // passes our validation functions - configUtils.validate(overrides, (validationResult: ConfigValidationFailureInfo | string) => { - let configFile = getCtx().lifecycleManager.configFile - - if (_.isString(validationResult)) { - return errors.throwErr('CONFIG_VALIDATION_MSG_ERROR', 'configFile', configFile, validationResult) - } - - return errors.throwErr('CONFIG_VALIDATION_ERROR', 'configFile', configFile, validationResult) - }) - - debug('validate that there is no breaking config options added by setupNodeEvents') - - function makeSetupError (cyError: CypressError) { - cyError.name = `Error running ${testingType}.setupNodeEvents()` - - return cyError - } - - configUtils.validateNoBreakingConfig(overrides, errors.warning, (err, options) => { - throw makeSetupError(errors.get(err, options)) - }, testingType) - - configUtils.validateNoBreakingConfig(overrides[testingType], errors.warning, (err, options) => { - throw makeSetupError(errors.get(err, { - ...options, - name: `${testingType}.${options.name}`, - })) - }, testingType) - - const originalResolvedBrowsers = _.cloneDeep(cfg?.resolved?.browsers) ?? { - value: cfg.browsers, - from: 'default', - } as ResolvedFromConfig - - const diffs = deepDiff(cfg, overrides, true) - - debug('config diffs %o', diffs) - - const userBrowserList = diffs && diffs.browsers && _.cloneDeep(diffs.browsers) - - if (userBrowserList) { - debug('user browser list %o', userBrowserList) - } - - // for each override go through - // and change the resolved values of cfg - // to point to the plugin - if (diffs) { - debug('resolved config before diffs %o', cfg.resolved) - setPluginResolvedOn(cfg.resolved, diffs) - debug('resolved config object %o', cfg.resolved) - } - - // merge cfg into overrides - const merged = _.defaultsDeep(diffs, cfg) - - debug('merged config object %o', merged) - - // the above _.defaultsDeep combines arrays, - // if diffs.browsers = [1] and cfg.browsers = [1, 2] - // then the merged result merged.browsers = [1, 2] - // which is NOT what we want - if (Array.isArray(userBrowserList) && userBrowserList.length) { - merged.browsers = userBrowserList - merged.resolved.browsers.value = userBrowserList - } - - if (overrides.browsers === null) { - // null breaks everything when merging lists - debug('replacing null browsers with original list %o', originalResolvedBrowsers) - merged.browsers = cfg.browsers - if (originalResolvedBrowsers) { - merged.resolved.browsers = originalResolvedBrowsers - } - } - - debug('merged plugins config %o', merged) - - return merged -} - -// combines the default configuration object with values specified in the -// configuration file like "cypress.{ts|js}". Values in configuration file -// overwrite the defaults. -export function resolveConfigValues (config, defaults, resolved = {}) { - // pick out only known configuration keys - return _ - .chain(config) - .pick(configUtils.getPublicConfigKeys()) - .mapValues((val, key) => { - let r - const source = (s: ResolvedConfigurationOptionSource): ResolvedFromConfig => { - return { - value: val, - from: s, - } - } - - r = resolved[key] - - if (r) { - if (_.isObject(r)) { - return r - } - - return source(r) - } - - if (!(!_.isEqual(config[key], defaults[key]) && key !== 'browsers')) { - // "browsers" list is special, since it is dynamic by default - // and can only be overwritten via plugins file - return source('default') - } - - return source('config') - }).value() -} - -// instead of the built-in Node process, specify a path to 3rd party Node -export const setNodeBinary = (obj, userNodePath, userNodeVersion) => { - // if execPath isn't found we weren't executed from the CLI and should used the bundled node version. - if (userNodePath && userNodeVersion && obj.nodeVersion !== 'bundled') { - obj.resolvedNodePath = userNodePath - obj.resolvedNodeVersion = userNodeVersion - - return obj - } - - obj.resolvedNodeVersion = process.versions.node - - return obj -} - -export function relativeToProjectRoot (projectRoot: string, file: string) { - if (!file.startsWith(projectRoot)) { - return file - } - - // captures leading slash(es), both forward slash and back slash - const leadingSlashRe = /^[\/|\\]*(?![\/|\\])/ - - return file.replace(projectRoot, '').replace(leadingSlashRe, '') -} - -// async function -export async function setSupportFileAndFolder (obj) { - if (!obj.supportFile) { - return Bluebird.resolve(obj) - } - - obj = _.clone(obj) - - const ctx = getCtx() - - const supportFilesByGlob = await ctx.file.getFilesByGlob(obj.projectRoot, obj.supportFile) - - if (supportFilesByGlob.length > 1) { - return errors.throwErr('MULTIPLE_SUPPORT_FILES_FOUND', obj.supportFile, supportFilesByGlob) - } - - if (supportFilesByGlob.length === 0) { - if (obj.resolved.supportFile.from === 'default') { - return errors.throwErr('DEFAULT_SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, obj.supportFile)) - } - - return errors.throwErr('SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, obj.supportFile)) - } - - // TODO move this logic to find support file into util/path_helpers - const sf = supportFilesByGlob[0] - - debug(`setting support file ${sf}`) - debug(`for project root ${obj.projectRoot}`) - - return Bluebird - .try(() => { - // resolve full path with extension - obj.supportFile = utils.resolveModule(sf) - - return debug('resolved support file %s', obj.supportFile) - }).then(() => { - if (!pathHelpers.checkIfResolveChangedRootFolder(obj.supportFile, sf)) { - return - } - - debug('require.resolve switched support folder from %s to %s', sf, obj.supportFile) - // this means the path was probably symlinked, like - // /tmp/foo -> /private/tmp/foo - // which can confuse the rest of the code - // switch it back to "normal" file - const supportFileName = path.basename(obj.supportFile) - const base = sf.endsWith(supportFileName) ? path.dirname(sf) : sf - - obj.supportFile = path.join(base, supportFileName) - - return fs.pathExists(obj.supportFile) - .then((found) => { - if (!found) { - errors.throwErr('SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, obj.supportFile)) - } - - return debug('switching to found file %s', obj.supportFile) - }) - }).catch({ code: 'MODULE_NOT_FOUND' }, () => { - debug('support JS module %s does not load', sf) - - return utils.discoverModuleFile({ - filename: sf, - projectRoot: obj.projectRoot, - }) - .then((result) => { - if (result === null) { - return errors.throwErr('SUPPORT_FILE_NOT_FOUND', relativeToProjectRoot(obj.projectRoot, sf)) - } - - debug('setting support file to %o', { result }) - obj.supportFile = result - - return obj - }) - }) - .then(() => { - if (obj.supportFile) { - // set config.supportFolder to its directory - obj.supportFolder = path.dirname(obj.supportFile) - debug(`set support folder ${obj.supportFolder}`) - } - - return obj - }) -} - -export function setAbsolutePaths (obj) { - let pr - - obj = _.clone(obj) - - // if we have a projectRoot - pr = obj.projectRoot - - if (pr) { - // reset fileServerFolder to be absolute - // obj.fileServerFolder = path.resolve(pr, obj.fileServerFolder) - - // and do the same for all the rest - _.extend(obj, convertRelativeToAbsolutePaths(pr, obj)) - } - - return obj -} - -export function setUrls (obj) { - obj = _.clone(obj) - - // TODO: rename this to be proxyServer - const proxyUrl = `http://localhost:${obj.port}` - - const rootUrl = obj.baseUrl ? - origin(obj.baseUrl) - : - proxyUrl - - _.extend(obj, { - proxyUrl, - browserUrl: rootUrl + obj.clientRoute, - reporterUrl: rootUrl + obj.reporterRoute, - xhrUrl: obj.namespace + obj.xhrRoute, - }) - - return obj -} - -export function parseEnv (cfg: Record, cliEnvs: Record, resolved: Record = {}) { - const envVars = (resolved.env = {}) - - const resolveFrom = (from, obj = {}) => { - return _.each(obj, (val, key) => { - return envVars[key] = { - value: val, - from, - } - }) - } - - const configEnv = cfg.env != null ? cfg.env : {} - const envFile = cfg.envFile != null ? cfg.envFile : {} - let processEnvs = getProcessEnvVars(process.env) || {} - - cliEnvs = cliEnvs != null ? cliEnvs : {} - - const configFromEnv = _.reduce(processEnvs, (memo: string[], val, key) => { - const cfgKey = configUtils.matchesConfigKey(key) - - if (cfgKey) { - // only change the value if it hasn't been - // set by the CLI. override default + config - if (resolved[cfgKey] !== 'cli') { - cfg[cfgKey] = val - resolved[cfgKey] = { - value: val, - from: 'env', - } as ResolvedFromConfig - } - - memo.push(key) - } - - return memo - } - , []) - - processEnvs = _.chain(processEnvs) - .omit(configFromEnv) - .mapValues(hideSpecialVals) - .value() - - resolveFrom('config', configEnv) - resolveFrom('envFile', envFile) - resolveFrom('env', processEnvs) - resolveFrom('cli', cliEnvs) - - // configEnvs is from cypress.config.{js,ts,mjs,cjs} - // envFile is from cypress.env.json - // processEnvs is from process env vars - // cliEnvs is from CLI arguments - return _.extend(configEnv, envFile, processEnvs, cliEnvs) -} +export const setUrls = configUtils.setUrls export function getResolvedRuntimeConfig (config, runtimeConfig) { const resolvedRuntimeFields = _.mapValues(runtimeConfig, (v): ResolvedFromConfig => ({ value: v, from: 'runtime' })) diff --git a/packages/server/lib/makeDataContext.ts b/packages/server/lib/makeDataContext.ts index ad723c8e46b1..c054682dadfb 100644 --- a/packages/server/lib/makeDataContext.ts +++ b/packages/server/lib/makeDataContext.ts @@ -1,7 +1,5 @@ import { DataContext, getCtx, clearCtx, setCtx } from '@packages/data-context' import electron, { OpenDialogOptions, SaveDialogOptions, BrowserWindow } from 'electron' -import pkg from '@packages/root' -import * as configUtils from '@packages/config' import { isListening } from './util/ensure-url' import { isMainWindowFocused, focusMainWindow } from './gui/windows' @@ -19,7 +17,6 @@ import type { import browserUtils from './browsers/utils' import auth from './gui/auth' import user from './user' -import * as config from './config' import { openProject } from './open_project' import cache from './cache' import { graphqlSchema } from '@packages/graphql/src/schema' @@ -60,13 +57,6 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext { return openProject.relaunchBrowser ? openProject.relaunchBrowser() : null }, }, - configApi: { - allowedConfig: configUtils.allowed, - cypressVersion: pkg.version, - validateConfig: configUtils.validate, - updateWithPluginValues: config.updateWithPluginValues, - setupFullConfigWithDefaults: config.setupFullConfigWithDefaults, - }, appApi: { appData, }, diff --git a/packages/server/lib/modes/record.js b/packages/server/lib/modes/record.js index 48a4681e431b..cffa11894511 100644 --- a/packages/server/lib/modes/record.js +++ b/packages/server/lib/modes/record.js @@ -9,6 +9,8 @@ const Promise = require('bluebird') const isForkPr = require('is-fork-pr') const commitInfo = require('@cypress/commit-info') +const { hideKeys } = require('@packages/config') + const api = require('../api') const exception = require('../exception') const errors = require('../errors') @@ -16,7 +18,6 @@ const capture = require('../capture') const upload = require('../upload') const Config = require('../config') const env = require('../util/env') -const keys = require('../util/keys') const terminal = require('../util/terminal') const ciProvider = require('../util/ci_provider') const testsUtils = require('../util/tests_utils') @@ -387,7 +388,7 @@ const createRun = Promise.method((options = {}) => { switch (err.statusCode) { case 401: - recordKey = keys.hide(recordKey) + recordKey = hideKeys(recordKey) if (!recordKey) { // make sure the key is defined, otherwise the error // printing logic substitutes the default value {} diff --git a/packages/server/lib/remote_states.ts b/packages/server/lib/remote_states.ts index 45ed0cfc4706..e79aa067ef6a 100644 --- a/packages/server/lib/remote_states.ts +++ b/packages/server/lib/remote_states.ts @@ -1,5 +1,4 @@ -import { cors } from '@packages/network' -import origin from './util/origin' +import { cors, uri } from '@packages/network' import Debug from 'debug' import _ from 'lodash' import type EventEmitter from 'events' @@ -99,7 +98,7 @@ export class RemoteStates { let state if (_.isString(urlOrState)) { - const remoteOrigin = origin(urlOrState) + const remoteOrigin = uri.origin(urlOrState) const remoteProps = cors.parseUrlIntoDomainTldPort(remoteOrigin) if ((urlOrState === '') || !fullyQualifiedRe.test(urlOrState)) { diff --git a/packages/server/lib/scaffold.js b/packages/server/lib/scaffold.js index bced0d7f906c..36179af622f7 100644 --- a/packages/server/lib/scaffold.js +++ b/packages/server/lib/scaffold.js @@ -7,7 +7,7 @@ const { fs } = require('./util/fs') const cwd = require('./cwd') const debug = require('debug')('cypress:server:scaffold') const errors = require('./errors') -const { isDefault } = require('./util/config') +const { isResolvedConfigPropDefault } = require('@packages/config') const getExampleSpecsFullPaths = cypressEx.getPathToExamples() const getExampleFolderFullPaths = cypressEx.getPathToExampleFolders() @@ -74,7 +74,7 @@ module.exports = { // skip if user has explicitly set e2eFolder // or if user has set up component testing - if (!isDefault(config, 'e2eFolder') || componentTestingEnabled(config)) { + if (!isResolvedConfigPropDefault(config, 'e2eFolder') || componentTestingEnabled(config)) { return Promise.resolve() } @@ -94,7 +94,7 @@ module.exports = { debug(`fixture folder ${folder}`) // skip if user has explicitly set fixturesFolder - if (!config.fixturesFolder || !isDefault(config, 'fixturesFolder')) { + if (!config.fixturesFolder || !isResolvedConfigPropDefault(config, 'fixturesFolder')) { return Promise.resolve() } @@ -108,7 +108,7 @@ module.exports = { plugins (folder, config) { debug(`plugins folder ${folder}`) // skip if user has explicitly set pluginsFile - if (!config.pluginsFile || !isDefault(config, 'pluginsFile')) { + if (!config.pluginsFile || !isResolvedConfigPropDefault(config, 'pluginsFile')) { return Promise.resolve() } diff --git a/packages/server/lib/util/args.js b/packages/server/lib/util/args.js index db05fe0b5a46..007ff4e7a458 100644 --- a/packages/server/lib/util/args.js +++ b/packages/server/lib/util/args.js @@ -4,9 +4,8 @@ const is = require('check-more-types') const path = require('path') const debug = require('debug')('cypress:server:args') const minimist = require('minimist') -const { getBreakingRootKeys, getPublicConfigKeys } = require('@packages/config') +const { getBreakingRootKeys, getPublicConfigKeys, coerce } = require('@packages/config') -const coerceUtil = require('./coerce') const proxyUtil = require('./proxy') const errors = require('../errors') @@ -157,7 +156,7 @@ const JSONOrCoerce = (str) => { } // nupe :-( - return coerceUtil.coerce(str) + return coerce(str) } const sanitizeAndConvertNestedArgs = (str, argName) => { @@ -389,7 +388,7 @@ module.exports = { // bypassed the cli cwd: process.cwd(), }) - .mapValues(coerceUtil.coerce) + .mapValues(coerce) .value() debug('argv parsed: %o', options) diff --git a/packages/server/lib/util/config.ts b/packages/server/lib/util/config.ts deleted file mode 100644 index 0bb4c8e7eec5..000000000000 --- a/packages/server/lib/util/config.ts +++ /dev/null @@ -1,45 +0,0 @@ -import _ from 'lodash' -import { coerce } from './coerce' - -export const CYPRESS_ENV_PREFIX = 'CYPRESS_' - -export const CYPRESS_ENV_PREFIX_LENGTH = 'CYPRESS_'.length - -export const CYPRESS_RESERVED_ENV_VARS = [ - 'CYPRESS_INTERNAL_ENV', -] - -export const CYPRESS_SPECIAL_ENV_VARS = [ - 'RECORD_KEY', -] - -export const isDefault = (config: Record, prop: string) => { - return config.resolved[prop].from === 'default' -} - -export const getProcessEnvVars = (obj: NodeJS.ProcessEnv) => { - return _.reduce(obj, (memo, value, key) => { - if (!value) { - return memo - } - - if (isCypressEnvLike(key)) { - memo[removeEnvPrefix(key)] = coerce(value) - } - - return memo - } - , {}) -} - -const isCypressEnvLike = (key) => { - return _.chain(key) - .invoke('toUpperCase') - .startsWith(CYPRESS_ENV_PREFIX) - .value() && - !_.includes(CYPRESS_RESERVED_ENV_VARS, key) -} - -const removeEnvPrefix = (key: string) => { - return key.slice(CYPRESS_ENV_PREFIX_LENGTH) -} diff --git a/packages/server/lib/util/keys.js b/packages/server/lib/util/keys.js deleted file mode 100644 index abdeb243b190..000000000000 --- a/packages/server/lib/util/keys.js +++ /dev/null @@ -1,20 +0,0 @@ -const hide = (token) => { - if (!token) { - return - } - - if (typeof token !== 'string') { - // maybe somehow we passes key=true? - // https://github.com/cypress-io/cypress/issues/14571 - return - } - - return [ - token.slice(0, 5), - token.slice(-5), - ].join('...') -} - -module.exports = { - hide, -} diff --git a/packages/server/lib/util/origin.js b/packages/server/lib/util/origin.js deleted file mode 100644 index 465aca61f760..000000000000 --- a/packages/server/lib/util/origin.js +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: move this into lib/util/uri.js -const url = require('url') - -module.exports = function (urlStr) { - const parsed = url.parse(urlStr) - - parsed.hash = null - parsed.search = null - parsed.query = null - parsed.path = null - parsed.pathname = null - - return url.format(parsed) -} diff --git a/packages/server/lib/util/path_helpers.js b/packages/server/lib/util/path_helpers.js deleted file mode 100644 index aa516154c96f..000000000000 --- a/packages/server/lib/util/path_helpers.js +++ /dev/null @@ -1,38 +0,0 @@ -const path = require('path') -const { fs } = require('./fs') - -// require.resolve walks the symlinks, which can really change -// the results. For example -// /tmp/foo is symlink to /private/tmp/foo on Mac -// thus resolving /tmp/foo to find /tmp/foo/index.js -// can return /private/tmp/foo/index.js -// which can really confuse the rest of the code. -// Detect this switch by checking if the resolution of absolute -// paths moved the prefix -// -// Good case: no switcheroo, return false -// /foo/bar -> /foo/bar/index.js -// Bad case: return true -// /tmp/foo/bar -> /private/tmp/foo/bar/index.js -const checkIfResolveChangedRootFolder = (resolved, initial) => { - return path.isAbsolute(resolved) && - path.isAbsolute(initial) && - !resolved.startsWith(initial) -} - -// real folder path found could be different due to symlinks -// For example, folder /tmp/foo on Mac is really /private/tmp/foo -const getRealFolderPath = (folder) => { - // TODO check if folder is a non-empty string - if (!folder) { - throw new Error('Expected folder') - } - - return fs.realpathAsync(folder) -} - -module.exports = { - checkIfResolveChangedRootFolder, - - getRealFolderPath, -} diff --git a/packages/server/package.json b/packages/server/package.json index 6e0dbd0a8431..96e807e6ba97 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -102,7 +102,6 @@ "randomstring": "1.1.5", "recast": "0.20.4", "resolve": "1.17.0", - "return-deep-diff": "0.4.0", "sanitize-filename": "1.6.3", "semver": "7.3.2", "send": "0.17.1", diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 2be0f59575a3..0eafcba81c01 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -107,7 +107,7 @@ describe('Routes', () => { // get all the config defaults // and allow us to override them // for each test - return config.setupFullConfigWithDefaults(obj) + return config.setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob) .then((cfg) => { // use a jar for each test // but reset it automatically diff --git a/packages/server/test/integration/server_spec.js b/packages/server/test/integration/server_spec.js index 10b29cefbeca..9aeeb1fedab2 100644 --- a/packages/server/test/integration/server_spec.js +++ b/packages/server/test/integration/server_spec.js @@ -11,6 +11,7 @@ const { ServerE2E } = require(`../../lib/server-e2e`) const { SocketE2E } = require(`../../lib/socket-e2e`) const Fixtures = require('@tooling/system-tests') const { createRoutes } = require(`../../lib/routes`) +const { getCtx } = require('../../lib/makeDataContext') const s3StaticHtmlUrl = 'https://s3.amazonaws.com/internal-test-runner-assets.cypress.io/index.html' @@ -46,7 +47,7 @@ describe('Server', () => { // get all the config defaults // and allow us to override them // for each test - return config.setupFullConfigWithDefaults(obj) + return config.setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob) .then((cfg) => { // use a jar for each test // but reset it automatically diff --git a/packages/server/test/performance/proxy_performance_spec.js b/packages/server/test/performance/proxy_performance_spec.js index e8e95d20b3f8..b9ad5f6a4bcc 100644 --- a/packages/server/test/performance/proxy_performance_spec.js +++ b/packages/server/test/performance/proxy_performance_spec.js @@ -1,6 +1,6 @@ require('../spec_helper') -const { makeDataContext, setCtx } = require('../../lib/makeDataContext') +const { makeDataContext, setCtx, getCtx } = require('../../lib/makeDataContext') setCtx(makeDataContext({})) @@ -334,6 +334,10 @@ describe('Proxy Performance', function () { }) before(function () { + setCtx(makeDataContext({})) + + const getFilesByGlob = getCtx().file.getFilesByGlob + return CA.create() .then((ca) => { return ca.generateServerCertificateKeys('localhost') @@ -351,7 +355,7 @@ describe('Proxy Performance', function () { config: { supportFile: false, }, - }).then((config) => { + }, getFilesByGlob).then((config) => { config.port = CY_PROXY_PORT // turn off morgan diff --git a/packages/server/test/unit/config_spec.js b/packages/server/test/unit/config_spec.js index 513afd0f03e7..4cdf35762f80 100644 --- a/packages/server/test/unit/config_spec.js +++ b/packages/server/test/unit/config_spec.js @@ -1,18 +1,11 @@ require('../spec_helper') const _ = require('lodash') -const debug = require('debug')('test') const stripAnsi = require('strip-ansi') const { stripIndent } = require('common-tags') const Fixtures = require('@tooling/system-tests') const { getCtx } = require('@packages/data-context') -const config = require(`../../lib/config`) -const errors = require(`../../lib/errors`) -const configUtil = require(`../../lib/util/config`) - -const os = require('node:os') - describe('lib/config', () => { before(function () { this.env = process.env @@ -26,40 +19,6 @@ describe('lib/config', () => { process.env = this.env }) - context('environment name check', () => { - it('throws an error for unknown CYPRESS_INTERNAL_ENV', async () => { - sinon.stub(errors, 'throwErr').withArgs('INVALID_CYPRESS_INTERNAL_ENV', 'foo-bar') - process.env.CYPRESS_INTERNAL_ENV = 'foo-bar' - const cfg = { - projectRoot: '/foo/bar/', - supportFile: false, - } - const options = {} - - try { - await config.mergeDefaults(cfg, options) - } catch { - // - } - - expect(errors.throwErr).have.been.calledOnce - }) - - it('allows production CYPRESS_INTERNAL_ENV', async () => { - sinon.stub(errors, 'throwErr') - process.env.CYPRESS_INTERNAL_ENV = 'production' - const cfg = { - projectRoot: '/foo/bar/', - supportFile: false, - } - const options = {} - - await config.mergeDefaults(cfg, options) - - expect(errors.throwErr).not.to.be.called - }) - }) - context('.get', () => { beforeEach(async function () { this.ctx = getCtx() @@ -1020,1352 +979,4 @@ describe('lib/config', () => { }) }) }) - - context('.resolveConfigValues', () => { - beforeEach(function () { - this.expected = function (obj) { - const merged = config.resolveConfigValues(obj.config, obj.defaults, obj.resolved) - - expect(merged).to.deep.eq(obj.final) - } - }) - - it('sets baseUrl to default', function () { - return this.expected({ - config: { baseUrl: null }, - defaults: { baseUrl: null }, - resolved: {}, - final: { - baseUrl: { - value: null, - from: 'default', - }, - }, - }) - }) - - it('sets baseUrl to config', function () { - return this.expected({ - config: { baseUrl: 'localhost' }, - defaults: { baseUrl: null }, - resolved: {}, - final: { - baseUrl: { - value: 'localhost', - from: 'config', - }, - }, - }) - }) - - it('does not change existing resolved values', function () { - return this.expected({ - config: { baseUrl: 'localhost' }, - defaults: { baseUrl: null }, - resolved: { baseUrl: 'cli' }, - final: { - baseUrl: { - value: 'localhost', - from: 'cli', - }, - }, - }) - }) - - it('ignores values not found in configKeys', function () { - return this.expected({ - config: { baseUrl: 'localhost', foo: 'bar' }, - defaults: { baseUrl: null }, - resolved: { baseUrl: 'cli' }, - final: { - baseUrl: { - value: 'localhost', - from: 'cli', - }, - }, - }) - }) - }) - - context('.mergeDefaults', () => { - beforeEach(function () { - this.defaults = (prop, value, cfg = {}, options = {}) => { - cfg.projectRoot = '/foo/bar/' - - return config.mergeDefaults({ ...cfg, supportFile: cfg.supportFile ?? false }, options) - .then((mergedConfig) => { - expect(mergedConfig[prop]).to.deep.eq(value) - }) - } - }) - - it('slowTestThreshold=10000 for e2e', function () { - return this.defaults('slowTestThreshold', 10000, {}, { testingType: 'e2e' }) - }) - - it('slowTestThreshold=250 for component', function () { - return this.defaults('slowTestThreshold', 250, {}, { testingType: 'component' }) - }) - - it('port=null', function () { - return this.defaults('port', null) - }) - - it('projectId=null', function () { - return this.defaults('projectId', null) - }) - - it('autoOpen=false', function () { - return this.defaults('autoOpen', false) - }) - - it('browserUrl=http://localhost:2020/__/', function () { - return this.defaults('browserUrl', 'http://localhost:2020/__/', { port: 2020 }) - }) - - it('proxyUrl=http://localhost:2020', function () { - return this.defaults('proxyUrl', 'http://localhost:2020', { port: 2020 }) - }) - - it('namespace=__cypress', function () { - return this.defaults('namespace', '__cypress') - }) - - it('baseUrl=http://localhost:8000/app/', function () { - return this.defaults('baseUrl', 'http://localhost:8000/app/', { - baseUrl: 'http://localhost:8000/app///', - }) - }) - - it('baseUrl=http://localhost:8000/app/', function () { - return this.defaults('baseUrl', 'http://localhost:8000/app/', { - baseUrl: 'http://localhost:8000/app//', - }) - }) - - it('baseUrl=http://localhost:8000/app', function () { - return this.defaults('baseUrl', 'http://localhost:8000/app', { - baseUrl: 'http://localhost:8000/app', - }) - }) - - it('baseUrl=http://localhost:8000/', function () { - return this.defaults('baseUrl', 'http://localhost:8000/', { - baseUrl: 'http://localhost:8000//', - }) - }) - - it('baseUrl=http://localhost:8000/', function () { - return this.defaults('baseUrl', 'http://localhost:8000/', { - baseUrl: 'http://localhost:8000/', - }) - }) - - it('baseUrl=http://localhost:8000', function () { - return this.defaults('baseUrl', 'http://localhost:8000', { - baseUrl: 'http://localhost:8000', - }) - }) - - it('viewportWidth=1000', function () { - return this.defaults('viewportWidth', 1000) - }) - - it('viewportHeight=660', function () { - return this.defaults('viewportHeight', 660) - }) - - it('userAgent=null', function () { - return this.defaults('userAgent', null) - }) - - it('baseUrl=null', function () { - return this.defaults('baseUrl', null) - }) - - it('defaultCommandTimeout=4000', function () { - return this.defaults('defaultCommandTimeout', 4000) - }) - - it('pageLoadTimeout=60000', function () { - return this.defaults('pageLoadTimeout', 60000) - }) - - it('requestTimeout=5000', function () { - return this.defaults('requestTimeout', 5000) - }) - - it('responseTimeout=30000', function () { - return this.defaults('responseTimeout', 30000) - }) - - it('execTimeout=60000', function () { - return this.defaults('execTimeout', 60000) - }) - - it('waitForAnimations=true', function () { - return this.defaults('waitForAnimations', true) - }) - - it('scrollBehavior=start', function () { - return this.defaults('scrollBehavior', 'top') - }) - - it('animationDistanceThreshold=5', function () { - return this.defaults('animationDistanceThreshold', 5) - }) - - it('video=true', function () { - return this.defaults('video', true) - }) - - it('videoCompression=32', function () { - return this.defaults('videoCompression', 32) - }) - - it('videoUploadOnPasses=true', function () { - return this.defaults('videoUploadOnPasses', true) - }) - - it('trashAssetsBeforeRuns=32', function () { - return this.defaults('trashAssetsBeforeRuns', true) - }) - - it('morgan=true', function () { - return this.defaults('morgan', true) - }) - - it('isTextTerminal=false', function () { - return this.defaults('isTextTerminal', false) - }) - - it('socketId=null', function () { - return this.defaults('socketId', null) - }) - - it('reporter=spec', function () { - return this.defaults('reporter', 'spec') - }) - - it('watchForFileChanges=true', function () { - return this.defaults('watchForFileChanges', true) - }) - - it('numTestsKeptInMemory=50', function () { - return this.defaults('numTestsKeptInMemory', 50) - }) - - it('modifyObstructiveCode=true', function () { - return this.defaults('modifyObstructiveCode', true) - }) - - it('supportFile=false', function () { - return this.defaults('supportFile', false, { supportFile: false }) - }) - - it('blockHosts=null', function () { - return this.defaults('blockHosts', null) - }) - - it('blockHosts=[a,b]', function () { - return this.defaults('blockHosts', ['a', 'b'], { - blockHosts: ['a', 'b'], - }) - }) - - it('blockHosts=a|b', function () { - return this.defaults('blockHosts', ['a', 'b'], { - blockHosts: ['a', 'b'], - }) - }) - - it('hosts=null', function () { - return this.defaults('hosts', null) - }) - - it('hosts={}', function () { - return this.defaults('hosts', { - foo: 'bar', - baz: 'quux', - }, { - hosts: { - foo: 'bar', - baz: 'quux', - }, - }) - }) - - it('resets numTestsKeptInMemory to 0 when runMode', () => { - return config.mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }) - .then((cfg) => { - expect(cfg.numTestsKeptInMemory).to.eq(0) - }) - }) - - it('resets watchForFileChanges to false when runMode', () => { - return config.mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }) - .then((cfg) => { - expect(cfg.watchForFileChanges).to.be.false - }) - }) - - it('can override morgan in options', () => { - return config.mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { morgan: false }) - .then((cfg) => { - expect(cfg.morgan).to.be.false - }) - }) - - it('can override isTextTerminal in options', () => { - return config.mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }) - .then((cfg) => { - expect(cfg.isTextTerminal).to.be.true - }) - }) - - it('can override socketId in options', () => { - return config.mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { socketId: '1234' }) - .then((cfg) => { - expect(cfg.socketId).to.eq('1234') - }) - }) - - it('deletes envFile', () => { - const obj = { - projectRoot: '/foo/bar/', - supportFile: false, - env: { - foo: 'bar', - version: '0.5.2', - }, - envFile: { - bar: 'baz', - version: '1.0.1', - }, - } - - return config.mergeDefaults(obj) - .then((cfg) => { - expect(cfg.env).to.deep.eq({ - foo: 'bar', - bar: 'baz', - version: '1.0.1', - }) - - expect(cfg.cypressEnv).to.eq(process.env['CYPRESS_INTERNAL_ENV']) - - expect(cfg).not.to.have.property('envFile') - }) - }) - - it('merges env into @config.env', () => { - const obj = { - projectRoot: '/foo/bar/', - supportFile: false, - env: { - host: 'localhost', - user: 'brian', - version: '0.12.2', - }, - } - - const options = { - env: { - version: '0.13.1', - foo: 'bar', - }, - } - - return config.mergeDefaults(obj, options) - .then((cfg) => { - expect(cfg.env).to.deep.eq({ - host: 'localhost', - user: 'brian', - version: '0.13.1', - foo: 'bar', - }) - }) - }) - - // @see https://github.com/cypress-io/cypress/issues/6892 - it('warns if experimentalGetCookiesSameSite is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalGetCookiesSameSite', true, { - experimentalGetCookiesSameSite: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_SAMESITE_REMOVED') - }) - - it('warns if experimentalSessionSupport is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalSessionSupport', true, { - experimentalSessionSupport: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_SESSION_SUPPORT_REMOVED') - }) - - it('warns if experimentalShadowDomSupport is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalShadowDomSupport', true, { - experimentalShadowDomSupport: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_SHADOW_DOM_REMOVED') - }) - - it('warns if experimentalRunEvents is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalRunEvents', true, { - experimentalRunEvents: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_RUN_EVENTS_REMOVED') - }) - - it('warns if experimentalStudio is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalStudio', true, { - experimentalStudio: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_STUDIO_REMOVED') - }) - - // @see https://github.com/cypress-io/cypress/pull/9185 - it('warns if experimentalNetworkStubbing is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalNetworkStubbing', true, { - experimentalNetworkStubbing: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_NETWORK_STUBBING_REMOVED') - }) - - it('warns if firefoxGcInterval is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('firefoxGcInterval', true, { - firefoxGcInterval: true, - }) - - expect(warning).to.be.calledWith('FIREFOX_GC_INTERVAL_REMOVED') - }) - - describe('.resolved', () => { - it('sets reporter and port to cli', () => { - const obj = { - projectRoot: '/foo/bar', - supportFile: false, - } - - const options = { - reporter: 'json', - port: 1234, - } - - return config.mergeDefaults(obj, options) - .then((cfg) => { - expect(cfg.resolved).to.deep.eq({ - animationDistanceThreshold: { value: 5, from: 'default' }, - arch: { value: os.arch(), from: 'default' }, - baseUrl: { value: null, from: 'default' }, - blockHosts: { value: null, from: 'default' }, - browsers: { value: [], from: 'default' }, - chromeWebSecurity: { value: true, from: 'default' }, - clientCertificates: { value: [], from: 'default' }, - defaultCommandTimeout: { value: 4000, from: 'default' }, - downloadsFolder: { value: 'cypress/downloads', from: 'default' }, - env: {}, - execTimeout: { value: 60000, from: 'default' }, - experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, - experimentalFetchPolyfill: { value: false, from: 'default' }, - experimentalInteractiveRunEvents: { value: false, from: 'default' }, - experimentalSingleTabRunMode: { value: false, from: 'default' }, - experimentalSessionAndOrigin: { value: false, from: 'default' }, - experimentalSourceRewriting: { value: false, from: 'default' }, - fileServerFolder: { value: '', from: 'default' }, - fixturesFolder: { value: 'cypress/fixtures', from: 'default' }, - hosts: { value: null, from: 'default' }, - excludeSpecPattern: { value: '*.hot-update.js', from: 'default' }, - includeShadowDom: { value: false, from: 'default' }, - isInteractive: { value: true, from: 'default' }, - keystrokeDelay: { value: 0, from: 'default' }, - modifyObstructiveCode: { value: true, from: 'default' }, - nodeVersion: { value: undefined, from: 'default' }, - numTestsKeptInMemory: { value: 50, from: 'default' }, - pageLoadTimeout: { value: 60000, from: 'default' }, - platform: { value: os.platform(), from: 'default' }, - port: { value: 1234, from: 'cli' }, - 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' }, - retries: { value: { runMode: 0, openMode: 0 }, from: 'default' }, - screenshotOnRunFailure: { value: true, from: 'default' }, - screenshotsFolder: { value: 'cypress/screenshots', from: 'default' }, - specPattern: { value: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', from: 'default' }, - slowTestThreshold: { value: 10000, from: 'default' }, - supportFile: { value: false, from: 'config' }, - supportFolder: { value: false, from: 'default' }, - taskTimeout: { value: 60000, from: 'default' }, - testIsolation: { value: 'legacy', from: 'default' }, - trashAssetsBeforeRuns: { value: true, from: 'default' }, - userAgent: { value: null, from: 'default' }, - video: { value: true, from: 'default' }, - videoCompression: { value: 32, from: 'default' }, - videosFolder: { value: 'cypress/videos', from: 'default' }, - videoUploadOnPasses: { value: true, from: 'default' }, - viewportHeight: { value: 660, from: 'default' }, - viewportWidth: { value: 1000, from: 'default' }, - waitForAnimations: { value: true, from: 'default' }, - scrollBehavior: { value: 'top', from: 'default' }, - watchForFileChanges: { value: true, from: 'default' }, - }) - }) - }) - - it('sets config, envFile and env', () => { - sinon.stub(configUtil, 'getProcessEnvVars').returns({ - quux: 'quux', - RECORD_KEY: 'foobarbazquux', - PROJECT_ID: 'projectId123', - }) - - const obj = { - projectRoot: '/foo/bar', - supportFile: false, - baseUrl: 'http://localhost:8080', - port: 2020, - env: { - foo: 'foo', - }, - envFile: { - bar: 'bar', - }, - } - - const options = { - env: { - baz: 'baz', - }, - } - - return config.mergeDefaults(obj, options) - .then((cfg) => { - expect(cfg.resolved).to.deep.eq({ - arch: { value: os.arch(), from: 'default' }, - animationDistanceThreshold: { value: 5, from: 'default' }, - baseUrl: { value: 'http://localhost:8080', from: 'config' }, - blockHosts: { value: null, from: 'default' }, - browsers: { value: [], from: 'default' }, - chromeWebSecurity: { value: true, from: 'default' }, - clientCertificates: { value: [], from: 'default' }, - defaultCommandTimeout: { value: 4000, from: 'default' }, - downloadsFolder: { value: 'cypress/downloads', from: 'default' }, - execTimeout: { value: 60000, from: 'default' }, - experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, - experimentalFetchPolyfill: { value: false, from: 'default' }, - experimentalInteractiveRunEvents: { value: false, from: 'default' }, - experimentalSingleTabRunMode: { value: false, from: 'default' }, - experimentalSessionAndOrigin: { value: false, from: 'default' }, - experimentalSourceRewriting: { value: false, from: 'default' }, - env: { - foo: { - value: 'foo', - from: 'config', - }, - bar: { - value: 'bar', - from: 'envFile', - }, - baz: { - value: 'baz', - from: 'cli', - }, - quux: { - value: 'quux', - from: 'env', - }, - RECORD_KEY: { - value: 'fooba...zquux', - from: 'env', - }, - }, - fileServerFolder: { value: '', from: 'default' }, - fixturesFolder: { value: 'cypress/fixtures', from: 'default' }, - hosts: { value: null, from: 'default' }, - excludeSpecPattern: { value: '*.hot-update.js', from: 'default' }, - includeShadowDom: { value: false, from: 'default' }, - isInteractive: { value: true, from: 'default' }, - keystrokeDelay: { value: 0, from: 'default' }, - modifyObstructiveCode: { value: true, from: 'default' }, - nodeVersion: { value: undefined, from: 'default' }, - numTestsKeptInMemory: { value: 50, from: 'default' }, - pageLoadTimeout: { value: 60000, from: 'default' }, - platform: { value: os.platform(), from: 'default' }, - port: { value: 2020, from: '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' }, - retries: { value: { runMode: 0, openMode: 0 }, from: 'default' }, - screenshotOnRunFailure: { value: true, from: 'default' }, - screenshotsFolder: { value: 'cypress/screenshots', from: 'default' }, - slowTestThreshold: { value: 10000, from: 'default' }, - specPattern: { value: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', from: 'default' }, - supportFile: { value: false, from: 'config' }, - supportFolder: { value: false, from: 'default' }, - taskTimeout: { value: 60000, from: 'default' }, - testIsolation: { value: 'legacy', from: 'default' }, - trashAssetsBeforeRuns: { value: true, from: 'default' }, - userAgent: { value: null, from: 'default' }, - video: { value: true, from: 'default' }, - videoCompression: { value: 32, from: 'default' }, - videosFolder: { value: 'cypress/videos', from: 'default' }, - videoUploadOnPasses: { value: true, from: 'default' }, - viewportHeight: { value: 660, from: 'default' }, - viewportWidth: { value: 1000, from: 'default' }, - waitForAnimations: { value: true, from: 'default' }, - scrollBehavior: { value: 'top', from: 'default' }, - watchForFileChanges: { value: true, from: 'default' }, - }) - }) - }) - - it('sets testIsolation=strict by default when experimentalSessionAndOrigin=true and e2e testing', () => { - sinon.stub(configUtil, 'getProcessEnvVars').returns({}) - - const obj = { - projectRoot: '/foo/bar', - supportFile: false, - baseUrl: 'http://localhost:8080', - experimentalSessionAndOrigin: true, - } - - const options = { - testingType: 'e2e', - } - - return config.mergeDefaults(obj, options) - .then((cfg) => { - expect(cfg.resolved).to.have.property('experimentalSessionAndOrigin') - expect(cfg.resolved.experimentalSessionAndOrigin).to.deep.eq({ value: true, from: 'config' }) - expect(cfg.resolved).to.have.property('testIsolation') - expect(cfg.resolved.testIsolation).to.deep.eq({ value: 'strict', from: 'default' }) - }) - }) - - it('honors user config for testIsolation when experimentalSessionAndOrigin=true and e2e testing', () => { - sinon.stub(configUtil, 'getProcessEnvVars').returns({}) - - const obj = { - projectRoot: '/foo/bar', - supportFile: false, - baseUrl: 'http://localhost:8080', - experimentalSessionAndOrigin: true, - testIsolation: 'legacy', - } - - const options = { - testingType: 'e2e', - } - - return config.mergeDefaults(obj, options) - .then((cfg) => { - expect(cfg.resolved).to.have.property('experimentalSessionAndOrigin') - expect(cfg.resolved.experimentalSessionAndOrigin).to.deep.eq({ value: true, from: 'config' }) - expect(cfg.resolved).to.have.property('testIsolation') - expect(cfg.resolved.testIsolation).to.deep.eq({ value: 'legacy', from: 'config' }) - }) - }) - }) - }) - - context('.setPluginResolvedOn', () => { - it('resolves an object with single property', () => { - const cfg = {} - const obj = { - foo: 'bar', - } - - config.setPluginResolvedOn(cfg, obj) - - expect(cfg).to.deep.eq({ - foo: { - value: 'bar', - from: 'plugin', - }, - }) - }) - - it('resolves an object with multiple properties', () => { - const cfg = {} - const obj = { - foo: 'bar', - baz: [1, 2, 3], - } - - config.setPluginResolvedOn(cfg, obj) - - expect(cfg).to.deep.eq({ - foo: { - value: 'bar', - from: 'plugin', - }, - baz: { - value: [1, 2, 3], - from: 'plugin', - }, - }) - }) - - it('resolves a nested object', () => { - // we need at least the structure - const cfg = { - foo: { - bar: 1, - }, - } - const obj = { - foo: { - bar: 42, - }, - } - - config.setPluginResolvedOn(cfg, obj) - - expect(cfg, 'foo.bar gets value').to.deep.eq({ - foo: { - bar: { - value: 42, - from: 'plugin', - }, - }, - }) - }) - - // https://github.com/cypress-io/cypress/issues/7959 - it('resolves a single object', () => { - const cfg = { - } - const obj = { - foo: { - bar: { - baz: 42, - }, - }, - } - - config.setPluginResolvedOn(cfg, obj) - - expect(cfg).to.deep.eq({ - foo: { - from: 'plugin', - value: { - bar: { - baz: 42, - }, - }, - }, - }) - }) - }) - - context('_.defaultsDeep', () => { - it('merges arrays', () => { - // sanity checks to confirm how Lodash merges arrays in defaultsDeep - const diffs = { - list: [1], - } - const cfg = { - list: [1, 2], - } - const merged = _.defaultsDeep({}, diffs, cfg) - - expect(merged, 'arrays are combined').to.deep.eq({ - list: [1, 2], - }) - }) - }) - - context('.updateWithPluginValues', () => { - it('is noop when no overrides', () => { - expect(config.updateWithPluginValues({ foo: 'bar' }, null)).to.deep.eq({ - foo: 'bar', - }) - }) - - it('is noop with empty overrides', () => { - expect(config.updateWithPluginValues({ foo: 'bar' }, {})).to.deep.eq({ - foo: 'bar', - }) - }) - - it('updates resolved config values and returns config with overrides', () => { - const cfg = { - foo: 'bar', - baz: 'quux', - quux: 'foo', - lol: 1234, - env: { - a: 'a', - b: 'b', - }, - // previously resolved values - resolved: { - foo: { value: 'bar', from: 'default' }, - baz: { value: 'quux', from: 'cli' }, - quux: { value: 'foo', from: 'default' }, - lol: { value: 1234, from: 'env' }, - env: { - a: { value: 'a', from: 'config' }, - b: { value: 'b', from: 'config' }, - }, - }, - } - - const overrides = { - baz: 'baz', - quux: ['bar', 'quux'], - env: { - b: 'bb', - c: 'c', - }, - } - - expect(config.updateWithPluginValues(cfg, overrides)).to.deep.eq({ - foo: 'bar', - baz: 'baz', - lol: 1234, - quux: ['bar', 'quux'], - env: { - a: 'a', - b: 'bb', - c: 'c', - }, - resolved: { - foo: { value: 'bar', from: 'default' }, - baz: { value: 'baz', from: 'plugin' }, - quux: { value: ['bar', 'quux'], from: 'plugin' }, - lol: { value: 1234, from: 'env' }, - env: { - a: { value: 'a', from: 'config' }, - b: { value: 'bb', from: 'plugin' }, - c: { value: 'c', from: 'plugin' }, - }, - }, - }) - }) - - it('keeps the list of browsers if the plugins returns empty object', () => { - const browser = { - name: 'fake browser name', - family: 'chromium', - displayName: 'My browser', - version: 'x.y.z', - path: '/path/to/browser', - majorVersion: 'x', - } - - const cfg = { - browsers: [browser], - resolved: { - browsers: { - value: [browser], - from: 'default', - }, - }, - } - - const overrides = {} - - expect(config.updateWithPluginValues(cfg, overrides)).to.deep.eq({ - browsers: [browser], - resolved: { - browsers: { - value: [browser], - from: 'default', - }, - }, - }) - }) - - it('catches browsers=null returned from plugins', () => { - const browser = { - name: 'fake browser name', - family: 'chromium', - displayName: 'My browser', - version: 'x.y.z', - path: '/path/to/browser', - majorVersion: 'x', - } - - const cfg = { - projectRoot: '/foo/bar', - browsers: [browser], - resolved: { - browsers: { - value: [browser], - from: 'default', - }, - }, - } - - const overrides = { - browsers: null, - } - - sinon.stub(errors, 'throwErr') - config.updateWithPluginValues(cfg, overrides) - - expect(errors.throwErr).to.have.been.calledWith('CONFIG_VALIDATION_MSG_ERROR') - }) - - it('allows user to filter browsers', () => { - const browserOne = { - name: 'fake browser name', - family: 'chromium', - displayName: 'My browser', - version: 'x.y.z', - path: '/path/to/browser', - majorVersion: 'x', - } - const browserTwo = { - name: 'fake electron', - family: 'chromium', - displayName: 'Electron', - version: 'x.y.z', - // Electron browser is built-in, no external path - path: '', - majorVersion: 'x', - } - - const cfg = { - browsers: [browserOne, browserTwo], - resolved: { - browsers: { - value: [browserOne, browserTwo], - from: 'default', - }, - }, - } - - const overrides = { - browsers: [browserTwo], - } - - const updated = config.updateWithPluginValues(cfg, overrides) - - expect(updated.resolved, 'resolved values').to.deep.eq({ - browsers: { - value: [browserTwo], - from: 'plugin', - }, - }) - - expect(updated, 'all values').to.deep.eq({ - browsers: [browserTwo], - resolved: { - browsers: { - value: [browserTwo], - from: 'plugin', - }, - }, - }) - }) - }) - - context('.parseEnv', () => { - it('merges together env from config, env from file, env from process, and env from CLI', () => { - sinon.stub(configUtil, 'getProcessEnvVars').returns({ - version: '0.12.1', - user: 'bob', - }) - - const obj = { - env: { - version: '0.10.9', - project: 'todos', - host: 'localhost', - baz: 'quux', - }, - - envFile: { - host: 'http://localhost:8888', - user: 'brian', - foo: 'bar', - }, - } - - const envCLI = { - version: '0.14.0', - project: 'pie', - } - - expect(config.parseEnv(obj, envCLI)).to.deep.eq({ - version: '0.14.0', - project: 'pie', - host: 'http://localhost:8888', - user: 'bob', - foo: 'bar', - baz: 'quux', - }) - }) - }) - - context('.getProcessEnvVars', () => { - ['cypress_', 'CYPRESS_'].forEach((key) => { - it(`reduces key: ${key}`, () => { - const obj = { - cypress_host: 'http://localhost:8888', - foo: 'bar', - env: '123', - } - - obj[`${key}version`] = '0.12.0' - - expect(configUtil.getProcessEnvVars(obj)).to.deep.eq({ - host: 'http://localhost:8888', - version: '0.12.0', - }) - }) - }) - - it('does not merge reserved environment variables', () => { - const obj = { - CYPRESS_INTERNAL_ENV: 'production', - CYPRESS_FOO: 'bar', - CYPRESS_CRASH_REPORTS: '0', - CYPRESS_PROJECT_ID: 'abc123', - } - - expect(configUtil.getProcessEnvVars(obj)).to.deep.eq({ - FOO: 'bar', - PROJECT_ID: 'abc123', - CRASH_REPORTS: 0, - }) - }) - }) - - context('.setUrls', () => { - it('does not mutate existing obj', () => { - const obj = {} - - expect(config.setUrls(obj)).not.to.eq(obj) - }) - - it('uses baseUrl when set', () => { - const obj = { - port: 65432, - baseUrl: 'https://www.google.com', - clientRoute: '/__/', - } - - const urls = config.setUrls(obj) - - expect(urls.browserUrl).to.eq('https://www.google.com/__/') - - expect(urls.proxyUrl).to.eq('http://localhost:65432') - }) - - it('strips baseUrl to host when set', () => { - const obj = { - port: 65432, - baseUrl: 'http://localhost:9999/app/?foo=bar#index.html', - clientRoute: '/__/', - } - - const urls = config.setUrls(obj) - - expect(urls.browserUrl).to.eq('http://localhost:9999/__/') - - expect(urls.proxyUrl).to.eq('http://localhost:65432') - }) - }) - - context('.setSupportFileAndFolder', () => { - it('does nothing if supportFile is falsey', () => { - const obj = { - projectRoot: '/_test-output/path/to/project', - } - - return config.setSupportFileAndFolder(obj) - .then((result) => { - expect(result).to.eql(obj) - }) - }) - - it('sets the full path to the supportFile and supportFolder if it exists', () => { - const projectRoot = process.cwd() - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: 'test/unit/config_spec.js', - }) - - return config.setSupportFileAndFolder(obj) - .then((result) => { - expect(result).to.eql({ - projectRoot, - supportFile: `${projectRoot}/test/unit/config_spec.js`, - supportFolder: `${projectRoot}/test/unit`, - }) - }) - }) - - it('sets the supportFile to default e2e.js if it does not exist, support folder does not exist, and supportFile is the default', () => { - const projectRoot = Fixtures.projectPath('no-scaffolding') - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: 'cypress/support/e2e.js', - }) - - return config.setSupportFileAndFolder(obj) - .then((result) => { - expect(result).to.eql({ - projectRoot, - supportFile: `${projectRoot}/cypress/support/e2e.js`, - supportFolder: `${projectRoot}/cypress/support`, - }) - }) - }) - - it('finds support file in project path that contains glob syntax', () => { - const projectRoot = Fixtures.projectPath('project-with-(glob)-[chars]') - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: 'cypress/support/e2e.js', - }) - - return config.setSupportFileAndFolder(obj) - .then((result) => { - expect(result).to.eql({ - projectRoot, - supportFile: `${projectRoot}/cypress/support/e2e.js`, - supportFolder: `${projectRoot}/cypress/support`, - }) - }) - }) - - it('sets the supportFile to false if it does not exist, support folder exists, and supportFile is the default', () => { - const projectRoot = Fixtures.projectPath('empty-folders') - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: false, - }) - - return config.setSupportFileAndFolder(obj) - .then((result) => { - expect(result).to.eql({ - projectRoot, - supportFile: false, - }) - }) - }) - - it('throws error if supportFile is not default and does not exist', () => { - const projectRoot = process.cwd() - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: 'does/not/exist', - resolved: { - supportFile: { - value: 'does/not/exist', - from: 'default', - }, - }, - }) - - return config.setSupportFileAndFolder(obj) - .catch((err) => { - expect(stripAnsi(err.message)).to.include('Your project does not contain a default supportFile') - }) - }) - - it('sets the supportFile to index.ts if it exists (without ts require hook)', () => { - const projectRoot = Fixtures.projectPath('ts-proj') - const supportFolder = `${projectRoot}/cypress/support` - const supportFilename = `${supportFolder}/index.ts` - - const e = new Error('Cannot resolve TS file by default') - - e.code = 'MODULE_NOT_FOUND' - sinon.stub(config.utils, 'resolveModule').withArgs(supportFilename).throws(e) - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: 'cypress/support/index.ts', - }) - - return config.setSupportFileAndFolder(obj) - .then((result) => { - debug('result is', result) - - expect(result).to.eql({ - projectRoot, - supportFolder, - supportFile: supportFilename, - }) - }) - }) - - it('uses custom TS supportFile if it exists (without ts require hook)', () => { - const projectRoot = Fixtures.projectPath('ts-proj-custom-names') - const supportFolder = `${projectRoot}/cypress` - const supportFilename = `${supportFolder}/support.ts` - - const e = new Error('Cannot resolve TS file by default') - - e.code = 'MODULE_NOT_FOUND' - sinon.stub(config.utils, 'resolveModule').withArgs(supportFilename).throws(e) - - const obj = config.setAbsolutePaths({ - projectRoot, - supportFile: 'cypress/support.ts', - }) - - return config.setSupportFileAndFolder(obj) - .then((result) => { - debug('result is', result) - - expect(result).to.eql({ - projectRoot, - supportFolder, - supportFile: supportFilename, - }) - }) - }) - }) - - context('.setAbsolutePaths', () => { - it('is noop without projectRoot', () => { - expect(config.setAbsolutePaths({})).to.deep.eq({}) - }) - - it('does not mutate existing obj', () => { - const obj = {} - - expect(config.setAbsolutePaths(obj)).not.to.eq(obj) - }) - - it('ignores non special *folder properties', () => { - const obj = { - projectRoot: '/_test-output/path/to/project', - blehFolder: 'some/rando/path', - foo: 'bar', - baz: 'quux', - } - - expect(config.setAbsolutePaths(obj)).to.deep.eq(obj) - }) - - return ['fileServerFolder', 'fixturesFolder'].forEach((folder) => { - it(`converts relative ${folder} to absolute path`, () => { - const obj = { - projectRoot: '/_test-output/path/to/project', - } - - obj[folder] = 'foo/bar' - - const expected = { - projectRoot: '/_test-output/path/to/project', - } - - expected[folder] = '/_test-output/path/to/project/foo/bar' - - expect(config.setAbsolutePaths(obj)).to.deep.eq(expected) - }) - }) - }) - - context('.setNodeBinary', () => { - beforeEach(function () { - this.nodeVersion = process.versions.node - }) - - it('sets bundled Node ver if nodeVersion != system', function () { - const obj = config.setNodeBinary({ - nodeVersion: 'bundled', - }) - - expect(obj).to.deep.eq({ - nodeVersion: 'bundled', - resolvedNodeVersion: this.nodeVersion, - }) - }) - - it('sets cli Node ver if nodeVersion = system', function () { - const obj = config.setNodeBinary({ - nodeVersion: 'system', - }, '/foo/bar/node', '1.2.3') - - expect(obj).to.deep.eq({ - nodeVersion: 'system', - resolvedNodeVersion: '1.2.3', - resolvedNodePath: '/foo/bar/node', - }) - }) - - it('sets bundled Node ver and if nodeVersion = system and userNodePath undefined', function () { - const obj = config.setNodeBinary({ - nodeVersion: 'system', - }, undefined, '1.2.3') - - expect(obj).to.deep.eq({ - nodeVersion: 'system', - resolvedNodeVersion: this.nodeVersion, - }) - }) - - it('sets bundled Node ver and if nodeVersion = system and userNodeVersion undefined', function () { - const obj = config.setNodeBinary({ - nodeVersion: 'system', - }, '/foo/bar/node') - - expect(obj).to.deep.eq({ - nodeVersion: 'system', - resolvedNodeVersion: this.nodeVersion, - }) - }) - }) - - describe('relativeToProjectRoot', () => { - context('posix', () => { - it('returns path of file relative to projectRoot', () => { - const projectRoot = '/root/projects' - const supportFile = '/root/projects/cypress/support/e2e.js' - - expect(config.relativeToProjectRoot(projectRoot, supportFile)).to.eq('cypress/support/e2e.js') - }) - }) - - context('windows', () => { - it('returns path of file relative to projectRoot', () => { - const projectRoot = `\\root\\projects` - const supportFile = `\\root\\projects\\cypress\\support\\e2e.js` - - expect(config.relativeToProjectRoot(projectRoot, supportFile)).to.eq(`cypress\\support\\e2e.js`) - }) - }) - }) }) diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js index b6f750ad9b30..ae77d612909c 100644 --- a/packages/server/test/unit/project_spec.js +++ b/packages/server/test/unit/project_spec.js @@ -45,7 +45,7 @@ describe.skip('lib/project-base', () => { .then((obj = {}) => { ({ projectId: this.projectId } = obj) - return config.setupFullConfigWithDefaults({ projectName: 'project', projectRoot: '/foo/bar' }) + return config.setupFullConfigWithDefaults({ projectName: 'project', projectRoot: '/foo/bar' }, getCtx().file.getFilesByGlob) .then((config1) => { this.config = config1 this.project = new ProjectBase({ projectRoot: this.todosPath, testingType: 'e2e' }) diff --git a/packages/server/test/unit/server_spec.js b/packages/server/test/unit/server_spec.js index 5dc8fe48eeb9..4cffec331cb6 100644 --- a/packages/server/test/unit/server_spec.js +++ b/packages/server/test/unit/server_spec.js @@ -10,6 +10,7 @@ const { ServerE2E } = require(`../../lib/server-e2e`) const { SocketE2E } = require(`../../lib/socket-e2e`) const fileServer = require(`../../lib/file_server`) const ensureUrl = require(`../../lib/util/ensure-url`) +const { getCtx } = require('@packages/data-context') const morganFn = function () {} @@ -21,7 +22,7 @@ describe('lib/server', () => { beforeEach(function () { this.server = new ServerE2E() - return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }) + return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }, getCtx().file.getFilesByGlob) .then((cfg) => { this.config = cfg }) @@ -50,7 +51,7 @@ describe.skip('lib/server', () => { sinon.stub(fileServer, 'create').returns(this.fileServer) - return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }) + return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }, getCtx().file.getFilesByGlob) .then((cfg) => { this.config = cfg this.server = new ServerE2E() diff --git a/packages/server/test/unit/util/coerce_spec.js b/packages/server/test/unit/util/coerce_spec.js deleted file mode 100644 index 46115718358c..000000000000 --- a/packages/server/test/unit/util/coerce_spec.js +++ /dev/null @@ -1,86 +0,0 @@ -require('../../spec_helper') - -const { coerce } = require(`../../../lib/util/coerce`) -const { getProcessEnvVars } = require(`../../../lib/util/config`) - -describe('lib/util/coerce', () => { - beforeEach(function () { - this.env = process.env - }) - - afterEach(function () { - process.env = this.env - }) - - context('coerce', () => { - it('coerces string', () => { - expect(coerce('foo')).to.eq('foo') - }) - - it('coerces string from process.env', () => { - process.env['CYPRESS_STRING'] = 'bar' - const cypressEnvVar = getProcessEnvVars(process.env) - - expect(coerce(cypressEnvVar)).to.deep.include({ STRING: 'bar' }) - }) - - it('coerces number', () => { - expect(coerce('123')).to.eq(123) - }) - - // NOTE: When exporting shell variables, they are saved in `process.env` as strings, hence why - // all `process.env` variables are assigned as strings in these unit tests - it('coerces number from process.env', () => { - process.env['CYPRESS_NUMBER'] = '8000' - const cypressEnvVar = getProcessEnvVars(process.env) - - expect(coerce(cypressEnvVar)).to.deep.include({ NUMBER: 8000 }) - }) - - it('coerces boolean', () => { - expect(coerce('true')).to.be.true - }) - - it('coerces boolean from process.env', () => { - process.env['CYPRESS_BOOLEAN'] = 'false' - const cypressEnvVar = getProcessEnvVars(process.env) - - expect(coerce(cypressEnvVar)).to.deep.include({ BOOLEAN: false }) - }) - - // https://github.com/cypress-io/cypress/issues/8818 - it('coerces JSON string', () => { - expect(coerce('[{"type": "foo", "value": "bar"}, {"type": "fizz", "value": "buzz"}]')).to.deep.equal( - [{ 'type': 'foo', 'value': 'bar' }, { 'type': 'fizz', 'value': 'buzz' }], - ) - }) - - // https://github.com/cypress-io/cypress/issues/8818 - it('coerces JSON string from process.env', () => { - process.env['CYPRESS_stringified_json'] = '[{"type": "foo", "value": "bar"}, {"type": "fizz", "value": "buzz"}]' - const cypressEnvVar = getProcessEnvVars(process.env) - const coercedCypressEnvVar = coerce(cypressEnvVar) - - expect(coercedCypressEnvVar).to.have.keys('stringified_json') - expect(coercedCypressEnvVar['stringified_json']).to.deep.equal([{ 'type': 'foo', 'value': 'bar' }, { 'type': 'fizz', 'value': 'buzz' }]) - }) - - it('coerces array', () => { - expect(coerce('[foo,bar]')).to.have.members(['foo', 'bar']) - }) - - it('coerces array from process.env', () => { - process.env['CYPRESS_ARRAY'] = '[google.com,yahoo.com]' - const cypressEnvVar = getProcessEnvVars(process.env) - - const coercedCypressEnvVar = coerce(cypressEnvVar) - - expect(coercedCypressEnvVar).to.have.keys('ARRAY') - expect(coercedCypressEnvVar['ARRAY']).to.have.members(['google.com', 'yahoo.com']) - }) - - it('defaults value with multiple types to string', () => { - expect(coerce('123foo456')).to.eq('123foo456') - }) - }) -}) diff --git a/packages/server/test/unit/util/config_spec.js b/packages/server/test/unit/util/config_spec.js deleted file mode 100644 index 589d67238901..000000000000 --- a/packages/server/test/unit/util/config_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -require('../../spec_helper') - -const configUtil = require(`../../../lib/util/config`) - -describe('lib/util/config', () => { - context('.isDefault', () => { - it('returns true if value is default value', () => { - const options = { - resolved: { - baseUrl: { from: 'default' }, - }, - } - - expect(configUtil.isDefault(options, 'baseUrl')).to.be.true - }) - - it('returns false if value is not default value', () => { - const options = { - resolved: { - baseUrl: { from: 'cli' }, - }, - } - - expect(configUtil.isDefault(options, 'baseUrl')).to.be.false - }) - }) - - context('.getProcessEnvVars', () => { - it('returns process envs prefixed with cypress', () => { - const envs = { - CYPRESS_BASE_URL: 'value', - RANDOM_ENV: 'ignored', - } - - expect(configUtil.getProcessEnvVars(envs)).to.deep.eq({ - BASE_URL: 'value', - }) - }) - - it('does not return CYPRESS_RESERVED_ENV_VARS', () => { - const envs = { - CYPRESS_INTERNAL_ENV: 'value', - } - - expect(configUtil.getProcessEnvVars(envs)).to.deep.eq({}) - }) - }) -}) diff --git a/packages/server/test/unit/util/keys_spec.ts b/packages/server/test/unit/util/keys_spec.ts deleted file mode 100644 index 7851be19b9e6..000000000000 --- a/packages/server/test/unit/util/keys_spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { hide } from '../../../lib/util/keys' -import { expect } from 'chai' - -describe('util/keys', () => { - it('removes middle part of the string', () => { - const hidden = hide('12345-xxxx-abcde') - - expect(hidden).to.equal('12345...abcde') - }) - - it('returns undefined for missing key', () => { - expect(hide()).to.be.undefined - }) - - // https://github.com/cypress-io/cypress/issues/14571 - it('returns undefined for non-string argument', () => { - expect(hide(true)).to.be.undefined - expect(hide(1234)).to.be.undefined - }) -}) diff --git a/packages/server/test/unit/util/origin_spec.js b/packages/server/test/unit/util/origin_spec.js deleted file mode 100644 index 3084434f9752..000000000000 --- a/packages/server/test/unit/util/origin_spec.js +++ /dev/null @@ -1,18 +0,0 @@ -require('../../spec_helper') - -const origin = require(`../../../lib/util/origin`) - -describe('lib/util/origin', () => { - beforeEach(function () { - this.expects = (url, expected) => { - expect(origin(url)).to.eq(expected) - } - }) - - it('strips everything but the remote origin', function () { - this.expects('http://localhost:9999/foo/bar?baz=quux#/index.html', 'http://localhost:9999') - this.expects('https://www.google.com/', 'https://www.google.com') - - return this.expects('https://app.foobar.co.uk:1234/a=b', 'https://app.foobar.co.uk:1234') - }) -}) diff --git a/packages/server/test/unit/util/path_helpers_spec.js b/packages/server/test/unit/util/path_helpers_spec.js deleted file mode 100644 index 86ec2485c3de..000000000000 --- a/packages/server/test/unit/util/path_helpers_spec.js +++ /dev/null @@ -1,21 +0,0 @@ -require('../../spec_helper') - -const path_helpers = require(`../../../lib/util/path_helpers`) - -describe('lib/util/path_helpers', () => { - context('checkIfResolveChangedRootFolder', () => { - const check = path_helpers.checkIfResolveChangedRootFolder - - it('ignores non-absolute paths', () => { - expect(check('foo/index.js', 'foo')).to.be.false - }) - - it('handles paths that do not switch', () => { - expect(check('/foo/index.js', '/foo')).to.be.false - }) - - it('detects path switch', () => { - expect(check('/private/foo/index.js', '/foo')).to.be.true - }) - }) -}) diff --git a/packages/ts/tsconfig.json b/packages/ts/tsconfig.json index 0c982dae1855..7e7e3baee408 100644 --- a/packages/ts/tsconfig.json +++ b/packages/ts/tsconfig.json @@ -3,7 +3,7 @@ /* Basic Options */ "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ - "lib": ["es2018", "ES2020.Promise"], /* Specify library files to be included in the compilation: */ + "lib": ["es2018", "ES2020.Promise", "ES2021.String"], /* Specify library files to be included in the compilation: */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ diff --git a/patches/return-deep-diff+0.4.0.patch b/patches/return-deep-diff+0.4.0.patch new file mode 100644 index 000000000000..cb614a6939b3 --- /dev/null +++ b/patches/return-deep-diff+0.4.0.patch @@ -0,0 +1,7 @@ +diff --git a/node_modules/return-deep-diff/index.d.ts b/node_modules/return-deep-diff/index.d.ts +new file mode 100644 +index 0000000..53711dd +--- /dev/null ++++ b/node_modules/return-deep-diff/index.d.ts +@@ -0,0 +1 @@ ++export default function deepDiff(obj1: Record, obj2: Record, keepNewKeys?: boolean): Record