Merge branch 'develop' into feature-multidomain

This commit is contained in:
Chris Breiding
2022-03-16 11:23:30 -04:00
137 changed files with 5031 additions and 2996 deletions
-15
View File
@@ -1,15 +0,0 @@
exports['list of all projects'] = [
{
"repo": "cypress-io/cypress-test-module-api",
"provider": "circle",
"platform": "linux"
}
]
exports['should have just circle and linux projects'] = [
{
"repo": "cypress-io/cypress-test-module-api",
"provider": "circle",
"platform": "linux"
}
]
@@ -0,0 +1,8 @@
exports['prepare-release-artifacts runs expected commands 1'] = `
$ node ./scripts/prepare-release-artifacts.js --dry-run --sha 57d0a85108fad6f77b39db88b8a7d8a3bfdb51a2 --version 1.2.3
🏗 Running \`move-binaries\`...
🏗 Dry run, not executing: node ./scripts/binary.js move-binaries --sha 57d0a85108fad6f77b39db88b8a7d8a3bfdb51a2 --version 1.2.3
🏗 Running \`create-stable-npm-package\`...
🏗 Dry run, not executing: ./scripts/create-stable-npm-package.sh https://cdn.cypress.io/beta/npm/1.2.3/linux-x64/develop-57d0a85108fad6f77b39db88b8a7d8a3bfdb51a2/cypress.tgz
`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
{
"chrome:beta": "99.0.4844.27",
"chrome:stable": "98.0.4758.102"
"chrome:beta": "100.0.4896.30",
"chrome:stable": "99.0.4844.51"
}
+19 -64
View File
@@ -29,7 +29,7 @@ mainBuildFilters: &mainBuildFilters
only:
- develop
- 10.0-release
- fix-darwin-win32-node-modules-install
- fix-beta-build-caching
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -38,7 +38,7 @@ macWorkflowFilters: &mac-workflow-filters
when:
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ fix-darwin-win32-node-modules-install, << pipeline.git.branch >> ]
- equal: [ fix-beta-build-caching, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -48,7 +48,7 @@ windowsWorkflowFilters: &windows-workflow-filters
or:
- equal: [ master, << pipeline.git.branch >> ]
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ fix-darwin-win32-node-modules-install, << pipeline.git.branch >> ]
- equal: [ fix-beta-build-caching, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -81,6 +81,7 @@ executors:
macos:
# Executor should have Node >= required version
xcode: "13.0.0"
resource_class: macos.x86.medium.gen2
environment:
PLATFORM: mac
@@ -265,7 +266,7 @@ commands:
- run:
name: Bail if cache exists
command: |
if [[ -f "/tmp/node_modules_installed" ]]; then
if [[ -f "node_modules_installed" ]]; then
echo "Node modules already cached for dependencies, exiting"
circleci-agent step halt
fi
@@ -300,12 +301,12 @@ commands:
key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }}
paths:
- /tmp/node_modules_cache
- run: touch /tmp/node_modules_installed
- run: touch node_modules_installed
- save_cache:
name: Saving node-modules cache state key
key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-state-{{ checksum "circle_cache_key" }}
paths:
- /tmp/node_modules_installed
- node_modules_installed
- save_cache:
name: Save weekly yarn cache
key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }}
@@ -586,10 +587,16 @@ commands:
steps:
- restore_cached_binary
- run:
name: "Cloning test project: <<parameters.repo>>"
name: "Cloning test project and checking out release branch: <<parameters.repo>>"
working_directory: ~/
command: |
git clone --depth 1 --no-single-branch https://github.com/cypress-io/<<parameters.repo>>.git /tmp/<<parameters.repo>>
cd /tmp/<<parameters.repo>> && (git checkout $(node ./scripts/get-next-version.js) || true)
# install some deps for get-next-version
npm i semver@7.3.2 conventional-recommended-bump@6.1.0 conventional-changelog-angular@5.0.12
NEXT_VERSION=$(node ./cypress/scripts/get-next-version.js)
cd /tmp/<<parameters.repo>> && (git checkout $NEXT_VERSION || true)
test-binary-against-rwa:
description: |
@@ -1626,7 +1633,7 @@ jobs:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "fix-darwin-win32-node-modules-install" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "fix-beta-build-caching" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
@@ -1699,39 +1706,6 @@ jobs:
yarn cypress:run --project /tmp/cypress-test-tiny --record
- store-npm-logs
test-binary-and-npm-against-other-projects:
<<: *defaults
parameters:
<<: *defaultsParameters
resource_class:
type: string
default: medium
resource_class: << parameters.resource_class >>
steps:
# needs uploaded NPM and test binary
- restore_cached_workspace
- run: ls -la
# make sure JSON files with uploaded urls are present
- run: ls -la binary-url.json npm-package-url.json
- run: cat binary-url.json
- run: cat npm-package-url.json
- run:
# install NPM from unique urls
name: Install Cypress Binary in Dummy Package
command: |
node scripts/test-unique-npm-and-binary.js \
--npm npm-package-url.json \
--binary binary-url.json \
--cwd /tmp/testing
- run:
name: Running other test projects with new NPM package and binary
command: |
node scripts/test-other-projects.js \
--npm npm-package-url.json \
--binary binary-url.json \
--provider circle
- store-npm-logs
test-npm-module-and-verify-binary:
<<: *defaults
steps:
@@ -2272,11 +2246,6 @@ linux-workflow: &linux-workflow
- test-binary-against-kitchensink:
requires:
- create-build-artifacts
- test-binary-and-npm-against-other-projects:
context: test-runner:trigger-test-jobs
<<: *mainBuildFilters
requires:
- create-build-artifacts
- test-npm-module-and-verify-binary:
<<: *mainBuildFilters
requires:
@@ -2333,12 +2302,13 @@ mac-workflow: &mac-workflow
- node_modules_install:
name: darwin-node-modules-install
executor: mac
resource_class: macos.x86.medium.gen2
only-cache-for-root-user: true
- build:
name: darwin-build
executor: mac
resource_class: medium
resource_class: macos.x86.medium.gen2
requires:
- darwin-node-modules-install
@@ -2357,7 +2327,7 @@ mac-workflow: &mac-workflow
- test-runner:upload
- test-runner:commit-status-checks
executor: mac
resource_class: medium
resource_class: macos.x86.medium.gen2
requires:
- darwin-build
@@ -2367,13 +2337,6 @@ mac-workflow: &mac-workflow
requires:
- darwin-build
- test-binary-and-npm-against-other-projects:
context: test-runner:trigger-test-jobs
name: darwin-test-binary-and-npm-against-other-projects
executor: mac
requires:
- darwin-create-build-artifacts
windows-workflow: &windows-workflow
jobs:
- node_modules_install:
@@ -2413,14 +2376,6 @@ windows-workflow: &windows-workflow
requires:
- windows-build
- test-binary-and-npm-against-other-projects:
context: test-runner:trigger-test-jobs
name: windows-test-binary-and-npm-against-other-projects
executor: windows
resource_class: windows.medium
requires:
- windows-create-build-artifacts
workflows:
linux:
<<: *linux-workflow
+1
View File
@@ -2,6 +2,7 @@ exports['package.json build outputs expected properties 1'] = {
"name": "test",
"engines": "test engines",
"version": "x.y.z",
"buildInfo": "replaced by normalizePackageJson",
"description": "Cypress.io end to end testing tool",
"homepage": "https://github.com/cypress-io/cypress",
"license": "MIT",
+1 -1
View File
@@ -43,4 +43,4 @@ https://download.cypress.io/desktop/0.20.2?platform=OS&arch=ARCH
exports['desktop url from template'] = `
https://download.cypress.io/desktop/0.20.2/darwin-x64/cypress.zip
`
`
+41 -3
View File
@@ -7,7 +7,7 @@ Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development
Cypress Version: 0.0.0-development (stable)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
@@ -29,7 +29,7 @@ Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development
Cypress Version: 0.0.0-development (stable)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
@@ -48,8 +48,46 @@ Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development
Cypress Version: 0.0.0-development (stable)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
`
exports['logs additional info about pre-releases'] = `
Proxy Settings: none detected
Environment Variables: none detected
Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (pre-release)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
This is a pre-release build of Cypress.
Build info:
Commit SHA: abc123
Commit Branch: someBranchName
Commit Date: 2022-02-02Txx:xx:xx.000Z
`
exports['logs additional info about development'] = `
Proxy Settings: none detected
Environment Variables: none detected
Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (pre-release)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
This is the development (un-built) Cypress CLI.
`
+36
View File
@@ -256,4 +256,40 @@ https://on.cypress.io/guides/getting-started/installing-cypress#system-requireme
Platform: win32-ia32
`
exports['/lib/tasks/install .start non-stable builds logs a warning about installing a pre-release 1'] = `
⚠ Warning: You are installing a pre-release build of Cypress.
Bugs may be present which do not exist in production builds.
This build was created from:
* Commit SHA: abc123
* Commit Branch: aBranchName
* Commit Timestamp: 1996-11-27Txx:xx:xx.000Z
Installing Cypress (version: https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-abc123/cypress.zip)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running: node_modules/.bin/cypress open
https://on.cypress.io/installing-cypress
`
+54 -43
View File
@@ -11,6 +11,7 @@ const _ = require('lodash')
const g = chalk.green
// color for paths
const p = chalk.cyan
const red = chalk.red
// urls
const link = chalk.blue.underline
@@ -43,56 +44,66 @@ const formatCypressVariables = () => {
return maskSensitiveVariables(vars)
}
methods.start = (options = {}) => {
methods.start = async (options = {}) => {
const args = ['--mode=info']
return spawn.start(args, {
await spawn.start(args, {
dev: options.dev,
})
.then(() => {
console.log()
const proxyVars = methods.findProxyEnvironmentVariables()
if (_.isEmpty(proxyVars)) {
console.log('Proxy Settings: none detected')
} else {
console.log('Proxy Settings:')
_.forEach(proxyVars, (value, key) => {
console.log('%s: %s', key, g(value))
})
console.log()
const proxyVars = methods.findProxyEnvironmentVariables()
console.log()
console.log('Learn More: %s', link('https://on.cypress.io/proxy-configuration'))
console.log()
}
})
.then(() => {
const cyVars = formatCypressVariables()
if (_.isEmpty(cyVars)) {
console.log('Environment Variables: none detected')
} else {
console.log('Environment Variables:')
_.forEach(cyVars, (value, key) => {
console.log('%s: %s', key, g(value))
})
}
})
.then(() => {
console.log()
console.log('Application Data:', p(util.getApplicationDataFolder()))
console.log('Browser Profiles:', p(util.getApplicationDataFolder('browsers')))
console.log('Binary Caches: %s', p(state.getCacheDir()))
})
.then(() => {
console.log()
return util.getOsVersionAsync().then((osVersion) => {
console.log('Cypress Version: %s', g(util.pkgVersion()))
console.log('System Platform: %s (%s)', g(os.platform()), g(osVersion))
console.log('System Memory: %s free %s', g(prettyBytes(os.totalmem())), g(prettyBytes(os.freemem())))
if (_.isEmpty(proxyVars)) {
console.log('Proxy Settings: none detected')
} else {
console.log('Proxy Settings:')
_.forEach(proxyVars, (value, key) => {
console.log('%s: %s', key, g(value))
})
})
console.log()
console.log('Learn More: %s', link('https://on.cypress.io/proxy-configuration'))
console.log()
}
const cyVars = formatCypressVariables()
if (_.isEmpty(cyVars)) {
console.log('Environment Variables: none detected')
} else {
console.log('Environment Variables:')
_.forEach(cyVars, (value, key) => {
console.log('%s: %s', key, g(value))
})
}
console.log()
console.log('Application Data:', p(util.getApplicationDataFolder()))
console.log('Browser Profiles:', p(util.getApplicationDataFolder('browsers')))
console.log('Binary Caches: %s', p(state.getCacheDir()))
console.log()
const osVersion = await util.getOsVersionAsync()
const buildInfo = util.pkgBuildInfo()
const isStable = buildInfo && buildInfo.stable
console.log('Cypress Version: %s', g(util.pkgVersion()), isStable ? g('(stable)') : red('(pre-release)'))
console.log('System Platform: %s (%s)', g(os.platform()), g(osVersion))
console.log('System Memory: %s free %s', g(prettyBytes(os.totalmem())), g(prettyBytes(os.freemem())))
if (!buildInfo) {
console.log()
console.log('This is the', red('development'), '(un-built) Cypress CLI.')
} else if (!isStable) {
console.log()
console.log('This is a', red('pre-release'), 'build of Cypress.')
console.log('Build info:')
console.log(' Commit SHA:', g(buildInfo.commitSha))
console.log(' Commit Branch:', g(buildInfo.commitBranch))
console.log(' Commit Date:', g(buildInfo.commitDate))
}
}
module.exports = methods
+8 -1
View File
@@ -41,8 +41,15 @@ const getVersions = () => {
return versions
})
.then((binaryVersions) => {
const buildInfo = util.pkgBuildInfo()
let packageVersion = util.pkgVersion()
if (!buildInfo) packageVersion += ' (development)'
else if (!buildInfo.stable) packageVersion += ' (pre-release)'
const versions = {
package: util.pkgVersion(),
package: packageVersion,
binary: binaryVersions.binary || 'not installed',
electronVersion: binaryVersions.electronVersion || 'not found',
electronNodeVersion: binaryVersions.electronNodeVersion || 'not found',
+139 -217
View File
@@ -1,6 +1,6 @@
const _ = require('lodash')
const arch = require('arch')
const os = require('os')
const url = require('url')
const path = require('path')
const chalk = require('chalk')
const debug = require('debug')('cypress:cli')
@@ -17,97 +17,10 @@ const logger = require('../logger')
const { throwFormErrorText, errors } = require('../errors')
const verbose = require('../VerboseRenderer')
const getNpmArgv = () => {
const json = process.env.npm_config_argv
const { buildInfo, version } = require('../../package.json')
if (!json) {
return
}
debug('found npm argv json %o', json)
try {
return JSON.parse(json).original || []
} catch (e) {
return []
}
}
// attempt to discover the version specifier used to install Cypress
// for example: "^5.0.0", "https://cdn.cypress.io/...", ...
const getVersionSpecifier = (startDir = path.resolve(__dirname, '../..')) => {
const argv = getNpmArgv()
if ((process.env.npm_package_resolved || '').endsWith('cypress.tgz')) {
return process.env.npm_package_resolved
}
if (argv) {
const tgz = _.find(argv, (t) => t.endsWith('cypress.tgz'))
if (tgz) {
return tgz
}
}
const getVersionSpecifierFromPkg = (dir) => {
debug('looking for versionSpecifier %o', { dir })
const tryParent = () => {
const parentPath = path.resolve(dir, '..')
if (parentPath === dir) {
debug('reached FS root with no versionSpecifier found')
return
}
return getVersionSpecifierFromPkg(parentPath)
}
return fs.readJSON(path.join(dir, 'package.json'))
.catch(() => ({}))
.then((pkg) => {
const specifier = _.chain(['dependencies', 'devDependencies', 'optionalDependencies'])
.map((prop) => _.get(pkg, `${prop}.cypress`))
.compact().first().value()
return specifier || tryParent()
})
}
// recurse through parent directories until package.json with `cypress` is found
return getVersionSpecifierFromPkg(startDir)
.then((versionSpecifier) => {
debug('finished looking for versionSpecifier', { versionSpecifier })
return versionSpecifier
})
}
const betaNpmUrlRe = /^\/beta\/npm\/(?<version>[0-9.]+)\/(?<platformSlug>.+?)\/(?<artifactSlug>.+?)\/cypress\.tgz$/
// convert a prerelease NPM package .tgz URL to the corresponding binary .zip URL
const getBinaryUrlFromPrereleaseNpmUrl = (npmUrl) => {
let parsed
try {
parsed = url.parse(npmUrl)
} catch (e) {
return
}
const matches = betaNpmUrlRe.exec(parsed.pathname)
if (parsed.hostname !== 'cdn.cypress.io' || !matches) {
return
}
const { version, artifactSlug } = matches.groups
parsed.pathname = `/beta/binary/${version}/${os.platform()}-${os.arch()}/${artifactSlug}/cypress.zip`
return parsed.format()
function _getBinaryUrlFromBuildInfo ({ commitSha, commitBranch }) {
return `https://cdn.cypress.io/beta/binary/${version}/${os.platform()}-${arch()}/${commitBranch}-${commitSha}/cypress.zip`
}
const alreadyInstalledMsg = () => {
@@ -226,43 +139,71 @@ const validateOS = () => {
})
}
const start = (options = {}) => {
/**
* Returns the version to install - either a string like `1.2.3` to be fetched
* from the download server or a file path or HTTP URL.
*/
function getVersionOverride ({ envVarVersion, buildInfo }) {
// let this environment variable reset the binary version we need
if (envVarVersion) {
return envVarVersion
}
if (buildInfo && !buildInfo.stable) {
logger.log(
chalk.yellow(stripIndent`
${logSymbols.warning} Warning: You are installing a pre-release build of Cypress.
Bugs may be present which do not exist in production builds.
This build was created from:
* Commit SHA: ${buildInfo.commitSha}
* Commit Branch: ${buildInfo.commitBranch}
* Commit Timestamp: ${buildInfo.commitDate}
`),
)
logger.log()
return _getBinaryUrlFromBuildInfo(buildInfo)
}
}
function getEnvVarVersion () {
if (!util.getEnv('CYPRESS_INSTALL_BINARY')) return
// because passed file paths are often double quoted
// and might have extra whitespace around, be robust and trim the string
const trimAndRemoveDoubleQuotes = true
const envVarVersion = util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes)
debug('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion)
return envVarVersion
}
const start = async (options = {}) => {
debug('installing with options %j', options)
const envVarVersion = getEnvVarVersion()
if (envVarVersion === '0') {
debug('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install')
logger.log(
stripIndent`
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`,
)
logger.log()
return
}
_.defaults(options, {
force: false,
buildInfo,
})
const pkgVersion = util.pkgVersion()
let needVersion = pkgVersion
let binaryUrlOverride
debug('version in package.json is', needVersion)
// let this environment variable reset the binary version we need
if (util.getEnv('CYPRESS_INSTALL_BINARY')) {
// because passed file paths are often double quoted
// and might have extra whitespace around, be robust and trim the string
const trimAndRemoveDoubleQuotes = true
const envVarVersion = util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes)
debug('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion)
if (envVarVersion === '0') {
debug('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install')
logger.log(
stripIndent`
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`,
)
logger.log()
return Promise.resolve()
}
binaryUrlOverride = envVarVersion
}
if (util.getEnv('CYPRESS_CACHE_FOLDER')) {
const envCache = util.getEnv('CYPRESS_CACHE_FOLDER')
@@ -277,18 +218,21 @@ const start = (options = {}) => {
logger.log()
}
const installDir = state.getVersionDir(pkgVersion)
const pkgVersion = util.pkgVersion()
const versionOverride = getVersionOverride({ envVarVersion, buildInfo: options.buildInfo })
const versionToInstall = versionOverride || pkgVersion
debug('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall)
const installDir = state.getVersionDir(pkgVersion, options.buildInfo)
const cacheDir = state.getCacheDir()
const binaryDir = state.getBinaryDir(pkgVersion)
return validateOS().then((isValid) => {
if (!isValid) {
return throwFormErrorText(errors.invalidOS)()
}
})
.then(() => {
return fs.ensureDirAsync(cacheDir)
})
if (!(await validateOS())) {
return throwFormErrorText(errors.invalidOS)()
}
await fs.ensureDirAsync(cacheDir)
.catch({ code: 'EACCES' }, (err) => {
return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent`
Failed to access ${chalk.cyan(cacheDir)}:
@@ -296,26 +240,11 @@ const start = (options = {}) => {
${err.message}
`)
})
.then(() => {
return Promise.all([
state.getBinaryPkgAsync(binaryDir).then(state.getBinaryPkgVersion),
getVersionSpecifier(),
])
})
.then(([binaryVersion, versionSpecifier]) => {
if (!binaryUrlOverride && versionSpecifier) {
const computedBinaryUrl = getBinaryUrlFromPrereleaseNpmUrl(versionSpecifier)
if (computedBinaryUrl) {
debug('computed binary url from version specifier %o', { computedBinaryUrl, needVersion })
binaryUrlOverride = computedBinaryUrl
}
}
needVersion = binaryUrlOverride || needVersion
debug('installed version is', binaryVersion, 'version needed is', needVersion)
const binaryPkg = await state.getBinaryPkgAsync(binaryDir)
const binaryVersion = await state.getBinaryPkgVersion(binaryPkg)
const shouldInstall = () => {
if (!binaryVersion) {
debug('no binary installed under cli version')
@@ -335,7 +264,7 @@ const start = (options = {}) => {
return true
}
if ((binaryVersion === needVersion) || !util.isSemver(needVersion)) {
if ((binaryVersion === versionToInstall) || !util.isSemver(versionToInstall)) {
// our version matches, tell the user this is a noop
alreadyInstalledMsg()
@@ -343,96 +272,89 @@ const start = (options = {}) => {
}
return true
})
.then((shouldInstall) => {
// noop if we've been told not to download
if (!shouldInstall) {
debug('Not downloading or installing binary')
}
return
}
// noop if we've been told not to download
if (!shouldInstall()) {
return debug('Not downloading or installing binary')
}
if (needVersion !== pkgVersion) {
logger.log(
chalk.yellow(stripIndent`
${logSymbols.warning} Warning: Forcing a binary version different than the default.
if (envVarVersion) {
logger.log(
chalk.yellow(stripIndent`
${logSymbols.warning} Warning: Forcing a binary version different than the default.
The CLI expected to install version: ${chalk.green(pkgVersion)}
The CLI expected to install version: ${chalk.green(pkgVersion)}
Instead we will install version: ${chalk.green(needVersion)}
Instead we will install version: ${chalk.green(versionToInstall)}
These versions may not work properly together.
`),
)
These versions may not work properly together.
`),
)
logger.log()
}
logger.log()
}
const getLocalFilePath = async () => {
// see if version supplied is a path to a binary
return fs.pathExistsAsync(needVersion)
.then((exists) => {
if (exists) {
return path.extname(needVersion) === '.zip' ? needVersion : false
}
if (await fs.pathExistsAsync(versionToInstall)) {
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false
}
const possibleFile = util.formAbsolutePath(needVersion)
const possibleFile = util.formAbsolutePath(versionToInstall)
debug('checking local file', possibleFile, 'cwd', process.cwd())
debug('checking local file', possibleFile, 'cwd', process.cwd())
return fs.pathExistsAsync(possibleFile)
.then((exists) => {
// if this exists return the path to it
// else false
if (exists && path.extname(possibleFile) === '.zip') {
return possibleFile
}
// if this exists return the path to it
// else false
if ((await fs.pathExistsAsync(possibleFile)) && path.extname(possibleFile) === '.zip') {
return possibleFile
}
return false
})
})
.then((pathToLocalFile) => {
if (pathToLocalFile) {
const absolutePath = path.resolve(needVersion)
return false
}
debug('found local file at', absolutePath)
debug('skipping download')
const pathToLocalFile = await getLocalFilePath()
const rendererOptions = getRendererOptions()
if (pathToLocalFile) {
const absolutePath = path.resolve(versionToInstall)
return new Listr([unzipTask({
progress: {
throttle: 100,
onProgress: null,
},
zipFilePath: absolutePath,
installDir,
rendererOptions,
})], { rendererOptions }).run()
}
debug('found local file at', absolutePath)
debug('skipping download')
if (options.force) {
debug('Cypress already installed at', installDir)
debug('but the installation was forced')
}
const rendererOptions = getRendererOptions()
debug('preparing to download and unzip version ', needVersion, 'to path', installDir)
return new Listr([unzipTask({
progress: {
throttle: 100,
onProgress: null,
},
zipFilePath: absolutePath,
installDir,
rendererOptions,
})], { rendererOptions }).run()
}
const downloadDir = os.tmpdir()
if (options.force) {
debug('Cypress already installed at', installDir)
debug('but the installation was forced')
}
return downloadAndUnzip({ version: needVersion, installDir, downloadDir })
})
// delay 1 sec for UX, unless we are testing
.then(() => {
return Promise.delay(1000)
})
.then(displayCompletionMsg)
})
debug('preparing to download and unzip version ', versionToInstall, 'to path', installDir)
const downloadDir = os.tmpdir()
await downloadAndUnzip({ version: versionToInstall, installDir, downloadDir })
// delay 1 sec for UX, unless we are testing
await Promise.delay(1000)
displayCompletionMsg()
}
module.exports = {
start,
_getVersionSpecifier: getVersionSpecifier,
_getBinaryUrlFromPrereleaseNpmUrl: getBinaryUrlFromPrereleaseNpmUrl,
_getBinaryUrlFromBuildInfo,
}
const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }) => {
+5 -1
View File
@@ -50,7 +50,11 @@ const getBinaryDir = (version = util.pkgVersion()) => {
return path.join(getVersionDir(version), getPlatFormBinaryFolder())
}
const getVersionDir = (version = util.pkgVersion()) => {
const getVersionDir = (version = util.pkgVersion(), buildInfo = util.pkgBuildInfo()) => {
if (buildInfo && !buildInfo.stable) {
version = ['beta', version, buildInfo.commitBranch, buildInfo.commitSha].join('-')
}
return path.join(getCacheDir(), version)
}
+4
View File
@@ -356,6 +356,10 @@ const util = {
return process.cwd()
},
pkgBuildInfo () {
return pkg.buildInfo
},
pkgVersion () {
return pkg.version
},
-2
View File
@@ -9,8 +9,6 @@
"build": "node ./scripts/build.js",
"dtslint": "dtslint types",
"postinstall": "node ./scripts/post-install.js",
"prerelease": "yarn build",
"release": "cd build && releaser --no-node --no-changelog",
"size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"test": "yarn test-unit",
"test-debug": "node --inspect-brk $(yarn bin mocha)",
+11
View File
@@ -1,5 +1,6 @@
const _ = require('lodash')
const path = require('path')
const shell = require('shelljs')
const fs = require('../lib/fs')
@@ -19,6 +20,10 @@ const {
const packageJsonSrc = path.join('package.json')
const packageJsonDest = path.join('build', 'package.json')
function getStdout (cmd) {
return shell.exec(cmd).trim()
}
function preparePackageForNpmRelease (json) {
// modify the existing package.json
// to prepare it for releasing to npm
@@ -29,6 +34,12 @@ function preparePackageForNpmRelease (json) {
_.extend(json, {
version,
buildInfo: {
commitBranch: process.env.CIRCLE_BRANCH || getStdout('git branch --show-current'),
commitSha: getStdout('git rev-parse HEAD'),
commitDate: new Date(getStdout('git show -s --format=%ci')).toISOString(),
stable: false,
},
description,
homepage,
license,
+12 -2
View File
@@ -10,7 +10,17 @@ const hasVersion = (json) => {
return la(is.semver(json.version), 'cannot find version', json)
}
const changeVersion = (o) => ({ ...o, version: 'x.y.z' })
const normalizePackageJson = (o) => {
expect(o.buildInfo).to.include({ stable: false })
expect(o.buildInfo.commitBranch).to.match(/.+/)
expect(o.buildInfo.commitSha).to.match(/[a-f0-9]+/)
return {
...o,
version: 'x.y.z',
buildInfo: 'replaced by normalizePackageJson',
}
}
describe('package.json build', () => {
beforeEach(function () {
@@ -32,7 +42,7 @@ describe('package.json build', () => {
it('outputs expected properties', () => {
return makeUserPackageFile()
.then(changeVersion)
.then(normalizePackageJson)
.then(snapshot)
})
})
+1
View File
@@ -28,6 +28,7 @@ describe('cli', () => {
os.platform.returns('darwin')
// sinon.stub(util, 'exit')
sinon.stub(util, 'logErrorExit1')
sinon.stub(util, 'pkgBuildInfo').returns({ stable: true })
this.exec = (args) => {
const cliArgs = `node test ${args}`.split(' ')
+21
View File
@@ -25,6 +25,10 @@ describe('exec info', function () {
.withArgs('browsers').returns('/user/app/data/path/to/browsers')
.withArgs().returns('/user/app/data/path')
sinon.stub(util, 'pkgBuildInfo').returns({
stable: true,
})
sinon.stub(state, 'getCacheDir').returns('/user/path/to/binary/cache')
})
@@ -68,4 +72,21 @@ describe('exec info', function () {
await startInfoAndSnapshot('cypress redacts sensitive vars')
})
it('logs additional info about pre-releases', async () => {
util.pkgBuildInfo.returns({
stable: false,
commitSha: 'abc123',
commitBranch: 'someBranchName',
commitDate: new Date('02-02-2022').toISOString(),
})
await startInfoAndSnapshot('logs additional info about pre-releases')
})
it('logs if unbuilt development', async () => {
util.pkgBuildInfo.returns(undefined)
await startInfoAndSnapshot('logs additional info about development')
})
})
+17
View File
@@ -18,6 +18,7 @@ describe('lib/exec/versions', function () {
})
sinon.stub(util, 'pkgVersion').returns('4.5.6')
sinon.stub(util, 'pkgBuildInfo').returns({ stable: true })
})
describe('.getVersions', function () {
@@ -50,6 +51,22 @@ describe('lib/exec/versions', function () {
})
})
it('appends pre-release if not stable', async function () {
util.pkgBuildInfo.returns({ stable: false })
const v = await versions.getVersions()
expect(v.package).to.eql('4.5.6 (pre-release)')
})
it('appends development if missing buildInfo', async function () {
util.pkgBuildInfo.returns(undefined)
const v = await versions.getVersions()
expect(v.package).to.eql('4.5.6 (development)')
})
it('reports default versions if not found', function () {
// imagine package.json only has version there
state.getBinaryPkgAsync.withArgs(binaryDir).resolves({
+35 -138
View File
@@ -1,11 +1,9 @@
require('../../spec_helper')
const _ = require('lodash')
const os = require('os')
const path = require('path')
const chalk = require('chalk')
const Promise = require('bluebird')
const mockfs = require('mock-fs')
const mockedEnv = require('mocked-env')
const snapshot = require('../../support/snapshot')
const stdout = require('../../support/stdout')
@@ -75,6 +73,32 @@ describe('/lib/tasks/install', function () {
})
})
describe('non-stable builds', () => {
function runInstall () {
return install.start({
buildInfo: {
stable: false,
commitSha: 'abc123',
commitBranch: 'aBranchName',
commitDate: new Date('11-27-1996').toISOString(),
},
})
}
it('install from a constructed CDN URL', async function () {
await runInstall()
expect(download.start).to.be.calledWithMatch({
version: 'https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-abc123/cypress.zip',
})
})
it('logs a warning about installing a pre-release', async function () {
await runInstall()
snapshot(normalize(this.stdout.toString()))
})
})
describe('override version', function () {
it('warns when specifying cypress version in env', function () {
const version = '0.12.1'
@@ -460,145 +484,18 @@ describe('/lib/tasks/install', function () {
})
})
context('._getBinaryUrlFromPrereleaseNpmUrl', function () {
beforeEach(() => {
context('._getBinaryUrlFromBuildInfo', function () {
const buildInfo = {
commitSha: 'abc123',
commitBranch: 'aBranchName',
}
it('generates the expected URL', () => {
os.platform.returns('linux')
sinon.stub(os, 'arch').returns('x64')
})
it('returns binary url for prerelease npm url', function () {
expect(install._getBinaryUrlFromPrereleaseNpmUrl('https://cdn.cypress.io/beta/npm/5.1.1/linux-x64/ciprovider-branchname-sha/cypress.tgz'))
.to.eq('https://cdn.cypress.io/beta/binary/5.1.1/linux-x64/ciprovider-branchname-sha/cypress.zip')
expect(install._getBinaryUrlFromPrereleaseNpmUrl('https://cdn.cypress.io/beta/npm/5.1.1/inux-x64/circle-develop-3fdfc3b453eb38ad3c0b079531e4dde6668e3dd0-436710/cypress.tgz'))
.to.eq('https://cdn.cypress.io/beta/binary/5.1.1/linux-x64/circle-develop-3fdfc3b453eb38ad3c0b079531e4dde6668e3dd0-436710/cypress.zip')
expect(install._getBinaryUrlFromPrereleaseNpmUrl('https://cdn.cypress.io/beta/npm/5.1.1/inux-x64/circle-develop/some/branch-3fdfc3b453eb38ad3c0b079531e4dde6668e3dd0-436710/cypress.tgz'))
.to.eq('https://cdn.cypress.io/beta/binary/5.1.1/linux-x64/circle-develop/some/branch-3fdfc3b453eb38ad3c0b079531e4dde6668e3dd0-436710/cypress.zip')
})
it('returns nothing for an invalid url', function () {
expect(install._getBinaryUrlFromPrereleaseNpmUrl('1.2.3')).to.be.undefined
expect(install._getBinaryUrlFromPrereleaseNpmUrl(null)).to.be.undefined
})
})
context('._getVersionSpecifier', function () {
let restoreEnv
beforeEach(function () {
sinon.stub(fs, 'readJSON').rejects()
restoreEnv && restoreEnv()
})
it('resolves undefined if no versionSpecifier found', async function () {
expect(await install._getVersionSpecifier('/foo/bar/baz')).to.be.undefined
})
it('resolves with cypress.tgz URL if specified in npm argv', async function () {
restoreEnv = mockedEnv({
npm_config_argv: JSON.stringify({
original: ['npm', 'i', 'https://foo.com/cypress.tgz'],
}),
})
expect(await install._getVersionSpecifier('/foo/bar/baz')).to.eq('https://foo.com/cypress.tgz')
})
it('resolves with cypress.tgz URL if specified in npm env npm_package_resolved', async function () {
restoreEnv = mockedEnv({
npm_package_resolved: 'https://foo.com/cypress.tgz',
})
expect(await install._getVersionSpecifier('/foo/bar/baz')).to.eq('https://foo.com/cypress.tgz')
})
it('resolves with versionSpecifier from parent pkg.json', async function () {
fs.readJSON.withArgs('/foo/bar/baz/package.json').resolves({
dependencies: {
'cypress': '1.2.3',
},
})
fs.readJSON.withArgs('/foo/bar/package.json').resolves({
dependencies: {
'cypress': 'wrong',
},
})
expect(await install._getVersionSpecifier('/foo/bar/baz')).to.eq('1.2.3')
})
it('resolves with devDependencies too', async function () {
fs.readJSON.withArgs('/foo/bar/baz/package.json').resolves({
devDependencies: {
'cypress': '4.5.6',
},
})
expect(await install._getVersionSpecifier('/foo/bar/baz')).to.eq('4.5.6')
})
it('resolves with optionalDependencies too', async function () {
fs.readJSON.withArgs('/foo/bar/baz/package.json').resolves({
optionalDependencies: {
'cypress': '6.7.8',
},
})
expect(await install._getVersionSpecifier('/foo/bar/baz')).to.eq('6.7.8')
})
context('with win32 path functions and paths', async function () {
const oldPath = _.clone(path)
beforeEach(() => {
_.assign(path, path.win32)
})
afterEach(() => {
_.assign(path, oldPath)
})
it('resolves undefined if no versionSpecifier found', async function () {
expect(await install._getVersionSpecifier('C:\\foo\\bar\\baz')).to.be.undefined
})
it('resolves with versionSpecifier from parent pkg.json', async function () {
fs.readJSON.withArgs('C:\\foo\\bar\\baz\\package.json').resolves({
dependencies: {
'cypress': '1.2.3',
},
})
fs.readJSON.withArgs('C:\\foo\\bar\\package.json').resolves({
dependencies: {
'cypress': 'wrong',
},
})
expect(await install._getVersionSpecifier('C:\\foo\\bar\\baz')).to.eq('1.2.3')
})
it('resolves with devDependencies too', async function () {
fs.readJSON.withArgs('C:\\foo\\bar\\baz\\package.json').resolves({
devDependencies: {
'cypress': '4.5.6',
},
})
expect(await install._getVersionSpecifier('C:\\foo\\bar\\baz')).to.eq('4.5.6')
})
it('resolves with optionalDependencies too', async function () {
fs.readJSON.withArgs('C:\\foo\\bar\\baz\\package.json').resolves({
optionalDependencies: {
'cypress': '6.7.8',
},
})
expect(await install._getVersionSpecifier('C:\\foo\\bar\\baz')).to.eq('6.7.8')
})
expect(install._getBinaryUrlFromBuildInfo(buildInfo))
.to.eq(`https://cdn.cypress.io/beta/binary/0.0.0-development/linux-x64/aBranchName-abc123/cypress.zip`)
})
})
})
+9 -9
View File
@@ -6,15 +6,15 @@ const pluginConfig: Cypress.PluginConfig = (on, config) => {}
// allows synchronous returns
const pluginConfig2: Cypress.PluginConfig = (on, config) => {
config // $ExpectType PluginConfigOptions
config.baseUrl // $ExpectType: string
config.configFile // $ExpectType: string | false
config.fixturesFolder // $ExpectType: string | false
config.pluginsFile // $ExpectType: string | false
config.screenshotsFolder // $ExpectType: string | false
config.videoCompression // $ExpectType: number | false
config.projectRoot // $ExpectType: string
config.version // $ExpectType: string
config.testingType // $ExpectType: TestingType
config.baseUrl // $ExpectType string | null
config.configFile // $ExpectType string | false
config.fixturesFolder // $ExpectType string | false
config.pluginsFile // $ExpectType string | false
config.screenshotsFolder // $ExpectType string | false
config.videoCompression // $ExpectType number | false
config.projectRoot // $ExpectType string
config.version // $ExpectType string
config.testingType // $ExpectType TestingType
on('before:browser:launch', (browser, options) => {
browser.displayName // $ExpectType string
+2
View File
@@ -13,5 +13,7 @@ For general contributor information, check out [`CONTRIBUTING.md`](../CONTRIBUTI
* [Building release artifacts](./building-release-artifacts.md)
* [Code signing](./code-signing.md)
* [Determining the next version of Cypress to be released](./next-version.md)
* [Error handling](./error-handling.md)
* [Patching packages](./patch-package.md)
* [Release process](./release-process.md)
* [Testing other projects](./testing-other-projects.md)
+38
View File
@@ -0,0 +1,38 @@
# Patching packages
Sometimes we need to patch `node_modules` that are not in our control in order to fix bugs or add features. There are a few ways to do this:
1. Fork the package to the `cypress-io` org and install via Git hash
2. Re-publish a patched version under the `@cypress` org on NPM
3. Patch the package using the [`patch-package`](https://github.com/ds300/patch-package#readme) utility on install/build
In *most cases*, it is best to use `patch-package`. Using `patch-package` has a number of advantages over #1 and #2:
* `patch-package` avoids the need for maintaining yet another repo or `npm/` package
* `patch-package` avoids the need for keeping version numbers/Git hashes synced in `package.json`/`yarn.lock` in the monorepo
* `patch-package` makes it easy to review changes in the context of a single PR to the `cypress` repo, as opposed to having to review changes in 2+ repos
* `patch-package` side-steps [a bug in Yarn](https://github.com/yarnpkg/yarn/issues/4722) that causes extremely confusing behavior when installing/caching Git dependencies
The *only* times where we cannot use `patch-package` is if we need to make a patch that is not included in the binary. The `cli` and `npm/` packages have their transitive dependencies installed by the user's package manager, so we cannot use `patch-package` to patch them.
For example: [`@cypress/request`](https://github.com/cypress-io/request) is used in the CLI, so we maintain a separate NPM package.
Also, we cannot include Git dependencies (#1) in any NPM packages, because not all users can install Git dependencies: [#6752](https://github.com/cypress-io/cypress/issues/6752)
## Upstreaming patches
If your patch is general purpose, you should submit a PR to the dependency's repo and create an issue in the `cypress` repo that tracks your upstream PR.
Once your upstream PR is merged, we can bump the version of the patched module in the monorepo and remove the patch, along with associated maintenance burden.
## Testing patches
*All patches require tests.*
Along with regular unit/integration/etc. tests against unbuilt Cypress, there should be at least one test for the patch that uses the built version of Cypress. This prevents regressions from a patch not being applied as expected when we build Cypress.
You can add a test for your patch against the built binary in a couple of ways:
1. Create a [`binary-system-test`](../system-tests/README.md) that tests that the patched behavior is correct in the built binary.
2. Add an expectation to [`scripts/binary/util/testStaticAssets.js`](../scripts/binary/util/testStaticAssets.js) that asserts the patch is applied.
3. Add some other test that runs against the built binary in CI.
+59 -81
View File
@@ -11,45 +11,30 @@ The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) di
### Prerequisites
- Ensure you have the following permissions set up:
- An AWS account with permission to create AWS access keys for the Cypress CDN.
- An AWS account with permission to access and write to the AWS S3, i.e. the Cypress CDN.
- Permissions for your npm account to publish the `cypress` package.
- Permissions to update releases in ZenHub.
- Set up the following environment variables:
- Cypress AWS access key and secret in `aws_credentials_json`, which looks like this:
- For the `release-automations` steps, you will need setup the following envs:
- GitHub token - generated yourself in github.
- [ZenHub API token](https://app.zenhub.com/dashboard/tokens) to interact with Zenhub. Found in 1Password.
- The `cypress-bot` GitHub app credentials. Found in 1Password.
```text
aws_credentials_json='{"bucket":"cdn.cypress.io","folder":"desktop","key":"...","secret":"..."}'
GITHUB_TOKEN="..."
ZENHUB_API_TOKEN="..."
GITHUB_APP_CYPRESS_INSTALLATION_ID=
GITHUB_APP_ID=
GITHUB_PRIVATE_KEY=
```
- A [GitHub token](https://github.com/settings/tokens) and a [CircleCI token](https://circleci.com/account/api) in `ci_json`:
```text
ci_json='{"githubToken":"...","circleToken":"..."}'
```
- You'll also need to put the GitHub token under its own variable and get a [ZenHub API token](https://app.zenhub.com/dashboard/tokens) for the `release-automations` step.
```text
GITHUB_TOKEN="..."
ZENHUB_API_TOKEN="..."
```
- The `cypress-bot` GitHub app credentials are also needed. Ask another team member who has done a deploy for those.
```text
GITHUB_APP_CYPRESS_INSTALLATION_ID=
GITHUB_APP_ID=
GITHUB_PRIVATE_KEY=
```
- For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. If you don't have access, ask a team member who has done a deploy.
- For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password.
```text
CF_ZONEID="..."
CF_TOKEN="..."
```
- If you don't have access to 1Password, ask a team member who has done a deploy.
- Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations.
### Before Publishing a New Version
@@ -67,96 +52,89 @@ of Cypress. You can see the progress of the test projects by opening the status
![Screenshot of status checks](https://i.imgur.com/AsQwzgO.png)
Once the `develop` branch for all test projects are reliably passing with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/<sha>/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/<sha>/cypress.tgz`, publishing can proceed.
### Steps to Publish a New Version
In the following instructions, "X.Y.Z" is used to denote the [next version of Cypress being published](./next-version.md).
1. `develop` should contain all of the changes made in `master`. However, this occasionally may not be the case. Ensure that `master` does not have any additional commits that are not on `develop` and all auto-generated pull requests designed to merge master into develop have been successfully merged.
1. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog.
2. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version.
2. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one.
- Use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub:
```shell
cd packages/issues-in-release
yarn do:changelog --release <release label>
```
- Ensure the changelog is up-to-date and has the correct date.
- Merge any release-specific documentation changes into the main release PR.
- You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check.
3. Use the `move-binaries` script to move the binaries for `<commit sha>` from `beta` to the `desktop` folder for `<new target version>`. This also purges the cloudflare cache for this version.
3. `develop` should contain all of the changes made in `master`. However, this occasionally may not be the case.
- Ensure that `master` does not have any additional commits that are not on `develop`.
- Ensure all auto-generated pull requests designed to merge master into develop have been successfully merged.
4. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version.
5. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/<sha>/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/<sha>/cypress.tgz`, publishing can proceed.
6. Log into AWS SSO with `aws sso login --profile <name_of_profile>`. The release scripts assumes you are using the `production` profile. If you have setup your credentials under a different profile, be sure to set the `AWS_PROFILE` environment variable.
7. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens:
* the binaries for `<commit sha>` are moved from `beta` to the `desktop` folder for `<new target version>` in S3
* the Cloudflare cache for this version is purged
* the pre-prod `cypress.tgz` NPM package is converted to a stable NPM package ready for release
```shell
yarn move-binaries --sha <commit sha> --version <new target version>
yarn prepare-release-artifacts --sha <commit sha> --version <new target version>
```
4. Publish the new npm package under the `dev` tag, using your personal npm account.
- To find the link to the package file `cypress.tgz`:
1. In GitHub, go to the latest commit (the one whose sha you used in the last step).
![commit-link](https://user-images.githubusercontent.com/1157043/80608728-33fe6100-8a05-11ea-8b53-375303757b67.png)
2. Scroll down past the changes to the comments. The first comment should be a `cypress-bot` comment that includes a line beginning `npm install ...`. Grab the `https://cdn.../npm/X.Y.Z/<platform>/<long sha>/cypress.tgz` link.
![commit-bot-comment](../assets/cypress-bot-pre-release-comment.png)
- Make sure the `linux-x64` binary and npm package are present at the commented locations. See [Before Publishing a New Version](#before-publishing-a-new-version).
- Publish the `linux-x64` distribution to the npm registry straight from the URL:
You can pass `--dry-run` to see the commands this would run under the hood.
```shell
npm publish https://cdn.cypress.io/beta/npm/X.Y.Z/<long sha>/cypress.tgz --tag dev
```
:bangbang: Important :bangbang: Be sure to release the `linux-x64` distribution.
8. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`.
5. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output:
9. Publish the generated npm package under the `dev` tag, using your personal npm account.
```shell
npm publish /tmp/cypress-prod.tgz --tag dev
```
10. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output:
```shell
dist-tags:
dev: 3.4.0 latest: 3.3.2
```
6. Test `cypress@X.Y.Z` to make sure everything is working.
11. Test `cypress@X.Y.Z` to make sure everything is working.
- Install the new version: `npm install -g cypress@X.Y.Z`
- Run a quick, manual smoke test:
- `cypress open`
- Go into a project, run a quick test, make sure things look right
- Install the new version into an established project and run the tests there
- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation.
- Optionally, do more thorough tests:
- Trigger test projects from the command line (if you have the appropriate permissions)
- Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress dashboard repo.
```shell
node scripts/test-other-projects.js --npm cypress@X.Y.Z --binary X.Y.Z
```
- Test the new version of Cypress against the Cypress dashboard repo.
7. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog.
8. Deploy the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation).
- If there is not already a release-specific PR open, create one. You can use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub:
```shell
cd packages/issues-in-release
yarn do:changelog --release <release label>
```
- Ensure the changelog is up-to-date and has the correct date.
- Merge any release-specific documentation changes into the main release PR.
- You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check.
- Merge this PR into `master` to deploy it to production.
9. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
12. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
```shell
npm dist-tag add cypress@X.Y.Z
```
10. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system.
13. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system.
```shell
yarn binary-release --version X.Y.Z
```
11. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on).
14. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on).
12. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md).
15. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md).
13. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release):
16. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release):
- Close the current release in ZenHub.
- Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date.
- Move all issues that are still open from the current release to the appropriate future release.
14. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up:
17. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up:
```shell
git commit -am "release X.Y.Z [skip ci]"
@@ -166,7 +144,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
git push origin vX.Y.Z
```
15. Merge `develop` into `master` and push both branches up. Note: pushing to `master` will automatically publish any independent npm packages that have not yet been published.
18. Merge `develop` into `master` and push both branches up. Note: pushing to `master` will automatically publish any independent npm packages that have not yet been published.
```shell
git push origin develop
@@ -175,7 +153,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
git push origin master
```
16. Inside of [cypress-io/release-automations][release-automations]:
19. Inside of [cypress-io/release-automations][release-automations]:
- Publish GitHub release to [cypress-io/cypress/releases](https://github.com/cypress-io/cypress/releases) using package `set-releases`:
```shell
@@ -190,9 +168,9 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left
17. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version. Instructions for updating `cypress-docker-images` can be found [here](https://github.com/cypress-io/cypress-docker-images/blob/master/CONTRIBUTING.md#add-new-included-image).
20. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version. Instructions for updating `cypress-docker-images` can be found [here](https://github.com/cypress-io/cypress-docker-images/blob/master/CONTRIBUTING.md#add-new-included-image).
18. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
21. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
- [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99)
- [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1)
- [cypress-example-realworld](https://github.com/cypress-io/cypress-example-realworld/issues/2)
@@ -202,7 +180,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- [cypress-documentation](https://github.com/cypress-io/cypress-documentation/issues/1313)
- [cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose) - Doesn't have a Renovate issue, but will auto-create and auto-merge non-major Cypress updates as long as the tests pass.
19. Check if any test or example repositories have a branch for testing the features or fixes from the newly published version `x.y.z`. The branch should also be named `x.y.z`. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z`, merge it into `master`.
22. Check if any test or example repositories have a branch for testing the features or fixes from the newly published version `x.y.z`. The branch should also be named `x.y.z`. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z`, merge it into `master`.
**Test Repos**
+4 -28
View File
@@ -4,10 +4,10 @@ In `develop`, `master`, and any other branch configured in [`circle.yml`](../cir
Two main strategies are used to spawn these test projects:
1. Local CI
1. `test-binary-against-repo` jobs
2. Remote CI
## Local CI
## `test-binary-against-repo` jobs
A number of CI jobs in `circle.yml` clone test projects and run tests as part of `cypress-io/cypress`'s CI pipeline.
@@ -17,30 +17,6 @@ Similarly to "Remote CI" test projects, Local CI test projects will attempt to c
One advantage to local CI is that it does not require creating commits to another repo.
## Remote CI
## `binary-system-tests`
After the production binary and NPM package are build and uploaded in CI, [`/scripts/test-other-projects.js`](../scripts/test-other-projects.js) is run as part of the `test-other-projects` `circle.yml` step.
This script creates commits inside of several test projects (hence "Remote CI") in order to trigger a realistic, continous-integration test of Cypress.
For a list of the projects, see the definition of `_PROVIDERS` in [`/scripts/binary/bump.js`](../scripts/binary/bump.js).
For each project and operating system combo in `_PROVIDERS`, the script:
1. Creates a commit to the test project's GitHub repo using the API. [An example of such a commit.](https://github.com/cypress-io/cypress-test-tiny/commit/5b39f3f43f6b7598f0d57cffcba71a7048d1d809)
* Note the commit is specifically for `linux`, and only the `linux-tests` job runs to completion.
* If a branch exists that is named after the [next version](./next-version.md) (`X.Y.Z`), the commit will be made to that branch.
* This is useful to test a release's breaking changes or new features against an example project without having to have the project's main branch in a broken state.
* Otherwise, the default branch is used for the commit.
2. Creates a status check in this GitHub repo (`cypress-io/cypress`) and marks it `pending`.
3. Waits for the test project's CI workflow to finish running.
* Each test project is configured to use [`@cypress/commit-message-install`](https://github.com/cypress-io/commit-message-install) to configure the exact test required via the information in the commit message.
* Each test project is configured to update the `pending` CI job in `cypress-io/cypress` to a `success` when the CI workflow successfully finishes.
These tests add coverage to the Cypress code base by:
* Providing a super-close-to-real-world usage of Cypress (i.e. installing fresh from an NPM package and running in a bare repo using the repo's CI setup)
* Testing in a variety of environments
* Different Node.js versions
* Different operating systems
* A multitude of CI providers
System tests in `/system-tests/test-binary` are run against the built Cypress App in CI. For more details, see the [README](../system-tests/README.md).
+7
View File
@@ -1,3 +1,10 @@
# [@cypress/react-v5.12.4](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.12.3...@cypress/react-v5.12.4) (2022-03-03)
### Bug Fixes
* avoid nextjs unsafeCache and watchOptions ([#20440](https://github.com/cypress-io/cypress/issues/20440)) ([9f60901](https://github.com/cypress-io/cypress/commit/9f6090170b0675d25b26b98cd0f987a5e395ab78))
# [@cypress/react-v5.12.3](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.12.2...@cypress/react-v5.12.3) (2022-02-10)
@@ -41,6 +41,15 @@ async function getNextWebpackConfig (config) {
checkSWC(nextWebpackConfig, config)
if (nextWebpackConfig.watchOptions && Array.isArray(nextWebpackConfig.watchOptions.ignored)) {
nextWebpackConfig.watchOptions = {
...nextWebpackConfig.watchOptions,
ignored: [...nextWebpackConfig.watchOptions.ignored.filter((pattern) => !/node_modules/.test(pattern)), '**/node_modules/!(@cypress/webpack-dev-server/dist/browser.js)**'],
}
debug('found options next.js watchOptions.ignored %O', nextWebpackConfig.watchOptions.ignored)
}
return nextWebpackConfig
}
+8
View File
@@ -1,3 +1,11 @@
# [@cypress/webpack-dev-server-v1.8.2](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.8.1...@cypress/webpack-dev-server-v1.8.2) (2022-03-03)
### Bug Fixes
* avoid nextjs unsafeCache and watchOptions ([#20440](https://github.com/cypress-io/cypress/issues/20440)) ([9f60901](https://github.com/cypress-io/cypress/commit/9f6090170b0675d25b26b98cd0f987a5e395ab78))
* error regression - strip ansi colors out of cy.fixture() error message ([#20335](https://github.com/cypress-io/cypress/issues/20335)) ([e0bd6ac](https://github.com/cypress-io/cypress/commit/e0bd6ac2aaf8d00b9233fffefed8f6ed2484bf45))
# [@cypress/webpack-dev-server-v1.8.1](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.8.0...@cypress/webpack-dev-server-v1.8.1) (2022-02-08)
@@ -78,6 +78,14 @@ export async function makeWebpackConfig (userWebpackConfig: webpack.Configuratio
})
}
if (typeof userWebpackConfig?.module?.unsafeCache === 'function') {
const originalCachePredicate = userWebpackConfig.module.unsafeCache
userWebpackConfig.module.unsafeCache = (module: any) => {
return originalCachePredicate(module) && !/[\\/]webpack-dev-server[\\/]dist[\\/]browser\.js/.test(module.resource)
}
}
const mergedConfig = merge<webpack.Configuration>(
userWebpackConfig,
makeDefaultWebpackConfig(template),
+1
View File
@@ -187,3 +187,4 @@ describe('#startDevServer', () => {
})
})
})
.timeout(5000)
+5 -8
View File
@@ -1,6 +1,6 @@
{
"name": "cypress",
"version": "9.5.0",
"version": "9.5.2",
"description": "Cypress.io end to end testing tool",
"private": true,
"scripts": {
@@ -14,7 +14,6 @@
"binary-zip": "node ./scripts/binary.js zip",
"build": "lerna run build --stream --ignore create-cypress-tests && lerna run build --stream --scope create-cypress-tests",
"build-prod": "lerna run build-prod --stream --ignore create-cypress-tests && lerna run build-prod --stream --scope create-cypress-tests",
"bump": "node ./scripts/binary.js bump",
"check-node-version": "node scripts/check-node-version.js",
"check-terminal": "node scripts/check-terminal.js",
"clean": "lerna run clean --parallel --no-bail || echo 'ok, errors while cleaning'",
@@ -41,7 +40,7 @@
"jscodeshift": "jscodeshift -t ./node_modules/js-codemod/transforms/arrow-function-arguments.js",
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.json .",
"lint-changed": "lint-changed",
"move-binaries": "node ./scripts/binary.js move-binaries",
"prepare-release-artifacts": "node ./scripts/prepare-release-artifacts.js",
"npm-release": "node scripts/npm-release.js",
"prestart": "yarn ensure-deps",
"start": "cypress open --dev --global",
@@ -63,7 +62,7 @@
"test-unit": "lerna exec yarn test-unit --ignore \"'{@packages/{desktop-gui,driver,root,static,web-config,net-stubbing,rewriter,ui-components},@cypress/{webpack-dev-server,eslint-plugin-dev}}'\"",
"pretest-watch": "yarn ensure-deps",
"test-watch": "lerna exec yarn test-watch --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",
"type-check": "node scripts/type_check",
"type-check": "yarn lerna exec yarn type-check --scope @tooling/system-tests && node scripts/type_check",
"verify:mocha:results": "node ./scripts/verify_mocha_results",
"prewatch": "yarn ensure-deps",
"watch": "lerna exec yarn watch --parallel --stream",
@@ -73,7 +72,7 @@
"nvm": "0.0.4"
},
"devDependencies": {
"@cypress/bumpercar": "2.0.12",
"@aws-sdk/credential-providers": "3.53.0",
"@cypress/commit-message-install": "3.1.3",
"@cypress/env-or-json-file": "2.0.0",
"@cypress/github-commit-status-check": "1.5.0",
@@ -157,16 +156,14 @@
"lint-staged": "11.1.2",
"listr2": "3.8.3",
"lodash": "^4.17.21",
"make-empty-github-commit": "cypress-io/make-empty-github-commit#4a592aedb776ba2f4cc88979055315a53eec42ee",
"minimist": "1.2.5",
"mocha": "3.5.3",
"mocha-banner": "1.1.2",
"mocha-junit-reporter": "2.0.0",
"mocha-multi-reporters": "1.1.7",
"mock-fs": "5.1.1",
"parse-github-repo-url": "1.4.1",
"patch-package": "6.4.7",
"plist": "3.0.1",
"plist": "3.0.4",
"pluralize": "8.0.0",
"postinstall-postinstall": "2.0.0",
"prefixed-list": "1.0.1",
@@ -821,6 +821,7 @@ describe('Settings', () => {
it('loads preferred editor, available editors and shows spinner', () => {
cy.get('.loading-editors').then(function () {
expect(this.ipc.getUserEditor).to.be.called
cy.contains('File Opener Preference').click()
})
})
@@ -6,14 +6,33 @@
</head>
<body>
<button id="reset">clear log</button>
<button id="target-button-tag">button tag</button>
<input id="target-input-button" type="button" value="input button" />
<input id="target-input-image" type="image" value="input image" />
<input id="target-input-reset" type="reset" value="input reset" />
<input id="target-input-submit" type="submit" value="input submit" />
<input id="target-input-checkbox" type="checkbox" value="input checkbox" />
<input id="target-input-radio" type="radio" value="input radio" />
<div>
<button id="reset">clear log</button>
</div>
<div>
<input id="input-text" type="text" />
<select id="focus-options">
<option value="clear">clear</option>
<option value="button-tag">button tag</option>
<option value="input-button">input button</option>
<option value="input-image">input image</option>
<option value="input-reset">input reset</option>
<option value="input-submit">input submit</option>
<option value="input-checkbox">input checkbox</option>
<option value="input-radio">input radio</option>
</select>
</div>
<div>
<button id="target-button-tag">button tag</button>
<input id="target-input-button" type="button" value="input button" />
<input id="target-input-image" type="image" value="input image" />
<input id="target-input-reset" type="reset" value="input reset" />
<input id="target-input-submit" type="submit" value="input submit" />
<input id="target-input-checkbox" type="checkbox" value="input checkbox" />
<input id="target-input-radio" type="radio" value="input radio" />
</div>
<div id="log"></div>
@@ -70,6 +89,32 @@
updateLog("keyup");
});
});
let handler = null
const focusOptions = document.getElementById("focus-options");
focusOptions.addEventListener('change', (event) => {
const val = event.target.value;
const target = document.getElementById('input-text');
if (handler) {
target.removeEventListener('keydown', handler);
}
if (val === 'clear') {
handler = null
return
}
handler = (e) => {
const focusEl = document.getElementById(`target-${val}`);
focusEl.focus()
}
target.addEventListener('keydown', handler);
})
</script>
</body>
</html>
@@ -0,0 +1,824 @@
const { _, $ } = Cypress
describe('src/cy/commands/actions/type - #type events', () => {
beforeEach(() => {
cy.visit('fixtures/dom.html')
})
describe('keyboard events', () => {
it('receives keydown event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('keydown', (e) => {
expect(_.toPlainObject(e.originalEvent)).to.include({
altKey: false,
bubbles: true,
cancelable: true,
charCode: 0, // deprecated
ctrlKey: false,
detail: 0,
key: 'a',
// has code property https://github.com/cypress-io/cypress/issues/3722
code: 'KeyA',
keyCode: 65, // deprecated but fired by chrome always uppercase in the ASCII table
location: 0,
metaKey: false,
repeat: false,
shiftKey: false,
type: 'keydown',
which: 65, // deprecated but fired by chrome
})
done()
})
cy.get(':text:first').type('a')
})
it('receives keypress event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('keypress', (e) => {
expect(_.toPlainObject(e.originalEvent)).to.include({
altKey: false,
bubbles: true,
cancelable: true,
charCode: 97, // deprecated
ctrlKey: false,
detail: 0,
key: 'a',
code: 'KeyA',
keyCode: 97, // deprecated
location: 0,
metaKey: false,
repeat: false,
shiftKey: false,
type: 'keypress',
which: 97, // deprecated
})
done()
})
cy.get(':text:first').type('a')
})
it('receives keyup event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('keyup', (e) => {
expect(_.toPlainObject(e.originalEvent)).to.include({
altKey: false,
bubbles: true,
cancelable: true,
charCode: 0, // deprecated
ctrlKey: false,
detail: 0,
key: 'a',
code: 'KeyA',
keyCode: 65, // deprecated but fired by chrome always uppercase in the ASCII table
location: 0,
metaKey: false,
repeat: false,
shiftKey: false,
type: 'keyup',
view: cy.state('window'),
which: 65, // deprecated but fired by chrome
})
.not.have.property('inputType')
done()
})
cy.get(':text:first').type('a')
})
it('receives textInput event', (done) => {
const $txt = cy.$$(':text:first')
$txt[0].addEventListener('textInput', (e) => {
// FIXME: (firefox) firefox cannot access window objects else throw cross-origin error
expect(Object.prototype.toString.call(e.view)).eq('[object Window]')
e.view = null
expect(_.toPlainObject(e)).to.include({
bubbles: true,
cancelable: true,
data: 'a',
detail: 0,
type: 'textInput',
// view: cy.state('window'),
which: 0,
})
done()
})
cy.get(':text:first').type('a')
})
it('receives input event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('input', (e) => {
const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'type')
expect(obj).to.deep.eq({
bubbles: true,
cancelable: false,
type: 'input',
})
done()
})
cy.get(':text:first').type('a')
})
// https://github.com/cypress-io/cypress/issues/20283
// TODO: Implement tests below.
it('fires events in the correct order')
it('fires events for each key stroke')
it('does fire input event when value changes', () => {
const onInput = cy.stub()
cy.$$(':text:first').on('input', onInput)
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{rightarrow}{backspace}')
.then(() => {
expect(onInput).to.be.calledOnce
})
.then(() => {
onInput.resetHistory()
})
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{leftarrow}{del}')
.then(() => {
expect(onInput).to.be.calledOnce
})
.then(() => {
onInput.resetHistory()
})
cy.$$('[contenteditable]:first').on('input', onInput)
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{rightarrow}{backspace}')
.then(() => {
expect(onInput).to.be.calledOnce
})
.then(() => {
onInput.resetHistory()
})
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{leftarrow}{del}')
.then(() => {
expect(onInput).to.be.calledOnce
})
})
it('does not fire input event when value does not change', () => {
let fired = false
cy.$$(':text:first').on('input', () => {
fired = true
})
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{rightarrow}{del}')
.then(() => {
expect(fired).to.eq(false)
})
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{leftarrow}{backspace}')
.then(() => {
expect(fired).to.eq(false)
})
cy.$$('textarea:first').on('input', () => {
fired = true
})
cy.get('textarea:first')
.invoke('val', 'bar')
.type('{selectAll}{rightarrow}{del}')
.then(() => {
expect(fired).to.eq(false)
})
cy.get('textarea:first')
.invoke('val', 'bar')
.type('{selectAll}{leftarrow}{backspace}')
.then(() => {
expect(fired).to.eq(false)
})
cy.$$('[contenteditable]:first').on('input', () => {
fired = true
})
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{movetoend}')
.then(($el) => {
expect(fired).to.eq(false)
})
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{leftarrow}{backspace}')
.then(() => {
expect(fired).to.eq(false)
})
})
})
describe('click events', () => {
it('passes timeout and interval down to click', (done) => {
const input = $('<input />').attr('id', 'input-covered-in-span').prependTo(cy.$$('body'))
$('<span>span on input</span>')
.css({
position: 'absolute',
left: input.offset().left,
top: input.offset().top,
padding: 5,
display: 'inline-block',
backgroundColor: 'yellow',
})
.prependTo(cy.$$('body'))
cy.on('command:retry', (options) => {
expect(options.timeout).to.eq(1000)
expect(options.interval).to.eq(60)
done()
})
cy.get('#input-covered-in-span').type('foobar', { timeout: 1000, interval: 60 })
})
it('does not issue another click event between type/type', () => {
const clicked = cy.stub()
cy.$$(':text:first').click(clicked)
cy.get(':text:first').type('f').type('o').then(() => {
expect(clicked).to.be.calledOnce
})
})
it('does not issue another click event if element is already in focus from click', () => {
const clicked = cy.stub()
cy.$$(':text:first').click(clicked)
cy.get(':text:first').click().type('o').then(() => {
expect(clicked).to.be.calledOnce
})
})
})
describe('change events', () => {
it('fires when enter is pressed and value has changed', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('bar{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires twice when enter is pressed and then again after losing focus', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('bar{enter}baz').blur().then(() => {
expect(changed).to.be.calledTwice
})
})
it('fires when element loses focus due to another action (click)', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').then(() => {
expect(changed).not.to.be.called
})
.get('button:first').click().then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires when element loses focus due to another action (type)', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').then(() => {
expect(changed).not.to.be.called
})
.get('textarea:first').type('bar').then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires when element is directly blurred', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').blur().then(() => {
expect(changed).to.be.calledOnce
})
})
// https://github.com/cypress-io/cypress/issues/20283
// TODO: implement this test
it('fires when element is tabbed away from')//, ->
// changed = 0
// cy.$$(":text:first").change ->
// changed += 1
// cy.get(":text:first").invoke("val", "foo").type("b{tab}").then ->
// expect(changed).to.eq 1
it('does not fire twice if element is already in focus between type/type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('f').type('o{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire twice if element is already in focus between clear/type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').clear().type('o{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire twice if element is already in focus between click/type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').click().type('o{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire twice if element is already in focus between type/click', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('d{enter}').click().then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire at all between clear/type/click', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').clear().type('o').click().then(($el) => {
expect(changed).not.to.be.called
return $el
}).blur()
.then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire if {enter} is preventedDefault', () => {
const changed = cy.stub()
cy.$$(':text:first').keypress((e) => {
if (e.which === 13) {
e.preventDefault()
}
})
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('b{enter}').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire when enter is pressed and value hasnt changed', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('b{backspace}{enter}').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire at the end of the type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire change event if value hasnt actually changed', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').invoke('val', 'foo').type('{backspace}{backspace}oo{enter}').blur().then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire if mousedown is preventedDefault which prevents element from losing focus', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.$$('textarea:first').mousedown(() => {
return false
})
cy
.get(':text:first').invoke('val', 'foo').type('bar')
.get('textarea:first').click().then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire hitting {enter} inside of a textarea', () => {
const changed = cy.stub()
cy.$$('textarea:first').change(changed)
cy
.get('textarea:first').type('foo{enter}bar').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire hitting {enter} inside of [contenteditable]', () => {
const changed = cy.stub()
cy.$$('[contenteditable]:first').change(changed)
cy
.get('[contenteditable]:first').type('foo{enter}bar').then(() => {
expect(changed).not.to.be.called
})
})
// [contenteditable] does not fire ANY change events ever.
it('does not fire at ALL for [contenteditable]', () => {
const changed = cy.stub()
cy.$$('[contenteditable]:first').change(changed)
cy
.get('[contenteditable]:first').type('foo')
.get('button:first').click().then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire on .clear() without blur', () => {
const changed = cy.stub()
cy.$$('input:first').change(changed)
cy.get('input:first').invoke('val', 'foo')
.clear()
.then(($el) => {
expect(changed).not.to.be.called
return $el
}).type('foo')
.blur()
.then(() => {
expect(changed).not.to.be.called
})
})
it('fires change for single value change inputs', () => {
const changed = cy.stub()
cy.$$('input[type="date"]:first').change(changed)
cy.get('input[type="date"]:first')
.type('1959-09-13')
.blur()
.then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire change for non-change single value input', () => {
const changed = cy.stub()
cy.$$('input[type="date"]:first').change(changed)
cy.get('input[type="date"]:first')
.invoke('val', '1959-09-13')
.type('1959-09-13')
.blur()
.then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire change for type\'d change that restores value', () => {
const changed = cy.stub()
cy.$$('input:first').change(changed)
cy.get('input:first')
.invoke('val', 'foo')
.type('{backspace}o')
.invoke('val', 'bar')
.type('{backspace}r')
.blur()
.then(() => {
expect(changed).not.to.be.called
})
})
})
// https://github.com/cypress-io/cypress/issues/19541
describe(`type('{enter}') and click event on button-like elements`, () => {
beforeEach(() => {
cy.visit('fixtures/click-event-by-type.html')
})
describe('triggers', () => {
const targets = [
'button-tag',
'input-button',
'input-image',
'input-reset',
'input-submit',
]
targets.forEach((target) => {
it(target, () => {
cy.get(`#target-${target}`).focus().type('{enter}')
cy.get('li').should('have.length', 4)
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'click')
cy.get('li').eq(3).should('have.text', 'keyup')
})
})
describe('keydown triggered on another element', () => {
targets.forEach((target) => {
it(target, () => {
cy.get('#focus-options').select(target)
cy.get('#input-text').focus().type('{enter}')
cy.get('li').should('have.length', 3)
cy.get('li').eq(0).should('have.text', 'keypress')
cy.get('li').eq(1).should('have.text', 'click')
cy.get('li').eq(2).should('have.text', 'keyup')
})
})
})
})
describe('does not trigger', () => {
const targets = [
'input-checkbox',
'input-radio',
]
targets.forEach((target) => {
it(target, () => {
cy.get(`#target-${target}`).focus().type('{enter}')
cy.get('li').should('have.length', 3)
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
})
})
describe('keydown triggered on another element', () => {
targets.forEach((target) => {
it(target, () => {
cy.get('#focus-options').select(target)
cy.get('#input-text').focus().type('{enter}')
cy.get('li').should('have.length', 2)
cy.get('li').eq(0).should('have.text', 'keypress')
cy.get('li').eq(1).should('have.text', 'keyup')
})
})
})
})
})
describe(`type(' ') fires click event on button-like elements`, () => {
beforeEach(() => {
cy.visit('fixtures/click-event-by-type.html')
})
const targets = [
'#target-button-tag',
'#target-input-button',
'#target-input-image',
'#target-input-reset',
'#target-input-submit',
]
describe(`triggers with single space`, () => {
targets.forEach((target) => {
it(target, () => {
const events = []
$(target).on('keydown keypress keyup click', (evt) => {
events.push(evt.type)
})
cy.get(target).focus().type(' ').then(() => {
expect(events).to.deep.eq([
'keydown',
'keypress',
'keyup',
'click',
])
})
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
cy.get('li').eq(3).should('have.text', 'click')
})
})
})
describe(`does not trigger if keyup prevented`, () => {
targets.forEach((target) => {
it(`${target} does not fire click event`, () => {
const events = []
$(target)
.on('keydown keypress keyup click', (evt) => {
events.push(evt.type)
})
.on('keyup', (evt) => {
evt.preventDefault()
})
cy.get(target).focus().type(' ').then(() => {
expect(events).to.deep.eq([
'keydown',
'keypress',
'keyup',
])
})
cy.get('li').should('have.length', 3)
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
})
})
})
describe('triggers after other characters', () => {
targets.forEach((target) => {
it(target, () => {
const events = []
$(target).on('keydown keypress keyup click', (evt) => {
events.push(evt.type)
})
cy.get(target).focus().type('asd ').then(() => {
expect(events).to.deep.eq([
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'click',
])
})
cy.get('li').eq(12).should('have.text', 'click')
})
})
})
describe('checkbox', () => {
it('checkbox is checked/unchecked', () => {
cy.get(`#target-input-checkbox`).focus().type(' ')
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
cy.get('li').eq(3).should('have.text', 'click')
cy.get('#target-input-checkbox').should('be.checked')
cy.get(`#target-input-checkbox`).type(' ')
cy.get('li').eq(4).should('have.text', 'keydown')
cy.get('li').eq(5).should('have.text', 'keypress')
cy.get('li').eq(6).should('have.text', 'keyup')
cy.get('li').eq(7).should('have.text', 'click')
cy.get('#target-input-checkbox').should('not.be.checked')
})
})
describe('radio', () => {
it('radio fires click event when it is not checked', () => {
cy.get(`#target-input-radio`).focus().type(' ')
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
cy.get('li').eq(3).should('have.text', 'click')
cy.get('#target-input-radio').should('be.checked')
})
it('radio does not fire click event when it is checked', () => {
// We're clicking here first to make the radio element checked.
cy.get(`#target-input-radio`).click().type(' ')
// item 0 is click event. It's fired because we want to make sure our radio button is checked.
cy.get('li').eq(1).should('have.text', 'keydown')
cy.get('li').eq(2).should('have.text', 'keypress')
cy.get('li').eq(3).should('have.text', 'keyup')
cy.get('#target-input-radio').should('be.checked')
})
})
describe('keydown on another element does not trigger click', () => {
const targets = [
'button-tag',
'input-button',
'input-image',
'input-reset',
'input-submit',
'input-checkbox',
'input-radio',
]
targets.forEach((target) => {
it(target, () => {
cy.get('#focus-options').select('button-tag')
cy.get('#input-text').focus().type(' ')
cy.get('li').should('have.length', 2)
cy.get('li').eq(0).should('have.text', 'keypress')
cy.get('li').eq(1).should('have.text', 'keyup')
})
})
})
})
})
@@ -557,199 +557,6 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})
// https://github.com/cypress-io/cypress/issues/19541
describe(`type('{enter}') and click event on button-like elements`, () => {
beforeEach(() => {
cy.visit('fixtures/click-event-by-type.html')
})
describe('triggers', () => {
const targets = [
'button-tag',
'input-button',
'input-image',
'input-reset',
'input-submit',
]
targets.forEach((targetId) => {
it(`${targetId}`, () => {
cy.get(`#target-${targetId}`).focus().type('{enter}')
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'click')
cy.get('li').eq(3).should('have.text', 'keyup')
})
})
})
describe('does not trigger', () => {
const targets = [
'input-checkbox',
'input-radio',
]
targets.forEach((targetId) => {
it(`${targetId}`, () => {
cy.get(`#target-${targetId}`).focus().type('{enter}')
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
})
})
})
})
describe(`type(' ') fires click event on button-like elements`, () => {
beforeEach(() => {
cy.visit('fixtures/click-event-by-type.html')
})
const targets = [
'#target-button-tag',
'#target-input-button',
'#target-input-image',
'#target-input-reset',
'#target-input-submit',
]
describe(`triggers with single space`, () => {
targets.forEach((target) => {
it(target, () => {
const events = []
$(target).on('keydown keypress keyup click', (evt) => {
events.push(evt.type)
})
cy.get(target).focus().type(' ').then(() => {
expect(events).to.deep.eq([
'keydown',
'keypress',
'keyup',
'click',
])
})
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
cy.get('li').eq(3).should('have.text', 'click')
})
})
})
describe(`does not trigger if keyup prevented`, () => {
targets.forEach((target) => {
it(`${target} does not fire click event`, () => {
const events = []
$(target)
.on('keydown keypress keyup click', (evt) => {
events.push(evt.type)
})
.on('keyup', (evt) => {
evt.preventDefault()
})
cy.get(target).focus().type(' ').then(() => {
expect(events).to.deep.eq([
'keydown',
'keypress',
'keyup',
])
})
cy.get('li').should('have.length', 3)
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
})
})
})
describe('triggers after other characters', () => {
targets.forEach((target) => {
it(target, () => {
const events = []
$(target).on('keydown keypress keyup click', (evt) => {
events.push(evt.type)
})
cy.get(target).focus().type('asd ').then(() => {
expect(events).to.deep.eq([
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'click',
])
})
cy.get('li').eq(12).should('have.text', 'click')
})
})
})
describe('checkbox', () => {
it('checkbox is checked/unchecked', () => {
cy.get(`#target-input-checkbox`).focus().type(' ')
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
cy.get('li').eq(3).should('have.text', 'click')
cy.get('#target-input-checkbox').should('be.checked')
cy.get(`#target-input-checkbox`).type(' ')
cy.get('li').eq(4).should('have.text', 'keydown')
cy.get('li').eq(5).should('have.text', 'keypress')
cy.get('li').eq(6).should('have.text', 'keyup')
cy.get('li').eq(7).should('have.text', 'click')
cy.get('#target-input-checkbox').should('not.be.checked')
})
})
describe('radio', () => {
it('radio fires click event when it is not checked', () => {
cy.get(`#target-input-radio`).focus().type(' ')
cy.get('li').eq(0).should('have.text', 'keydown')
cy.get('li').eq(1).should('have.text', 'keypress')
cy.get('li').eq(2).should('have.text', 'keyup')
cy.get('li').eq(3).should('have.text', 'click')
cy.get('#target-input-radio').should('be.checked')
})
it('radio does not fire click event when it is checked', () => {
// We're clicking here first to make the radio element checked.
cy.get(`#target-input-radio`).click().type(' ')
// item 0 is click event. It's fired because we want to make sure our radio button is checked.
cy.get('li').eq(1).should('have.text', 'keydown')
cy.get('li').eq(2).should('have.text', 'keypress')
cy.get('li').eq(3).should('have.text', 'keyup')
cy.get('#target-input-radio').should('be.checked')
})
})
})
describe('tabindex', () => {
beforeEach(function () {
this.$div = cy.$$('#tabindex')
@@ -979,243 +786,6 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})
describe('events', () => {
it('receives keydown event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('keydown', (e) => {
expect(_.toPlainObject(e.originalEvent)).to.include({
altKey: false,
bubbles: true,
cancelable: true,
charCode: 0, // deprecated
ctrlKey: false,
detail: 0,
key: 'a',
// has code property https://github.com/cypress-io/cypress/issues/3722
code: 'KeyA',
keyCode: 65, // deprecated but fired by chrome always uppercase in the ASCII table
location: 0,
metaKey: false,
repeat: false,
shiftKey: false,
type: 'keydown',
which: 65, // deprecated but fired by chrome
})
done()
})
cy.get(':text:first').type('a')
})
it('receives keypress event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('keypress', (e) => {
expect(_.toPlainObject(e.originalEvent)).to.include({
altKey: false,
bubbles: true,
cancelable: true,
charCode: 97, // deprecated
ctrlKey: false,
detail: 0,
key: 'a',
code: 'KeyA',
keyCode: 97, // deprecated
location: 0,
metaKey: false,
repeat: false,
shiftKey: false,
type: 'keypress',
which: 97, // deprecated
})
done()
})
cy.get(':text:first').type('a')
})
it('receives keyup event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('keyup', (e) => {
expect(_.toPlainObject(e.originalEvent)).to.include({
altKey: false,
bubbles: true,
cancelable: true,
charCode: 0, // deprecated
ctrlKey: false,
detail: 0,
key: 'a',
code: 'KeyA',
keyCode: 65, // deprecated but fired by chrome always uppercase in the ASCII table
location: 0,
metaKey: false,
repeat: false,
shiftKey: false,
type: 'keyup',
view: cy.state('window'),
which: 65, // deprecated but fired by chrome
})
.not.have.property('inputType')
done()
})
cy.get(':text:first').type('a')
})
it('receives textInput event', (done) => {
const $txt = cy.$$(':text:first')
$txt[0].addEventListener('textInput', (e) => {
// FIXME: (firefox) firefox cannot access window objects else throw cross-origin error
expect(Object.prototype.toString.call(e.view)).eq('[object Window]')
e.view = null
expect(_.toPlainObject(e)).to.include({
bubbles: true,
cancelable: true,
data: 'a',
detail: 0,
type: 'textInput',
// view: cy.state('window'),
which: 0,
})
done()
})
cy.get(':text:first').type('a')
})
it('receives input event', (done) => {
const $txt = cy.$$(':text:first')
$txt.on('input', (e) => {
const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'type')
expect(obj).to.deep.eq({
bubbles: true,
cancelable: false,
type: 'input',
})
done()
})
cy.get(':text:first').type('a')
})
it('fires events in the correct order')
it('fires events for each key stroke')
it('does fire input event when value changes', () => {
const onInput = cy.stub()
cy.$$(':text:first').on('input', onInput)
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{rightarrow}{backspace}')
.then(() => {
expect(onInput).to.be.calledOnce
})
.then(() => {
onInput.resetHistory()
})
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{leftarrow}{del}')
.then(() => {
expect(onInput).to.be.calledOnce
})
.then(() => {
onInput.resetHistory()
})
cy.$$('[contenteditable]:first').on('input', onInput)
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{rightarrow}{backspace}')
.then(() => {
expect(onInput).to.be.calledOnce
})
.then(() => {
onInput.resetHistory()
})
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{leftarrow}{del}')
.then(() => {
expect(onInput).to.be.calledOnce
})
})
it('does not fire input event when value does not change', () => {
let fired = false
cy.$$(':text:first').on('input', () => {
fired = true
})
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{rightarrow}{del}')
.then(() => {
expect(fired).to.eq(false)
})
cy.get(':text:first')
.invoke('val', 'bar')
.type('{selectAll}{leftarrow}{backspace}')
.then(() => {
expect(fired).to.eq(false)
})
cy.$$('textarea:first').on('input', () => {
fired = true
})
cy.get('textarea:first')
.invoke('val', 'bar')
.type('{selectAll}{rightarrow}{del}')
.then(() => {
expect(fired).to.eq(false)
})
cy.get('textarea:first')
.invoke('val', 'bar')
.type('{selectAll}{leftarrow}{backspace}')
.then(() => {
expect(fired).to.eq(false)
})
cy.$$('[contenteditable]:first').on('input', () => {
fired = true
})
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{movetoend}')
.then(($el) => {
expect(fired).to.eq(false)
})
cy.get('[contenteditable]:first')
.invoke('html', 'foobar')
.type('{selectAll}{leftarrow}{backspace}')
.then(() => {
expect(fired).to.eq(false)
})
})
})
describe('maxlength', () => {
it('limits text entered to the maxlength attribute of a text input', () => {
const $input = cy.$$(':text:first')
@@ -2876,337 +2446,6 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})
describe('click events', () => {
it('passes timeout and interval down to click', (done) => {
const input = $('<input />').attr('id', 'input-covered-in-span').prependTo(cy.$$('body'))
$('<span>span on input</span>')
.css({
position: 'absolute',
left: input.offset().left,
top: input.offset().top,
padding: 5,
display: 'inline-block',
backgroundColor: 'yellow',
})
.prependTo(cy.$$('body'))
cy.on('command:retry', (options) => {
expect(options.timeout).to.eq(1000)
expect(options.interval).to.eq(60)
done()
})
cy.get('#input-covered-in-span').type('foobar', { timeout: 1000, interval: 60 })
})
it('does not issue another click event between type/type', () => {
const clicked = cy.stub()
cy.$$(':text:first').click(clicked)
cy.get(':text:first').type('f').type('o').then(() => {
expect(clicked).to.be.calledOnce
})
})
it('does not issue another click event if element is already in focus from click', () => {
const clicked = cy.stub()
cy.$$(':text:first').click(clicked)
cy.get(':text:first').click().type('o').then(() => {
expect(clicked).to.be.calledOnce
})
})
})
describe('change events', () => {
it('fires when enter is pressed and value has changed', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('bar{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires twice when enter is pressed and then again after losing focus', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('bar{enter}baz').blur().then(() => {
expect(changed).to.be.calledTwice
})
})
it('fires when element loses focus due to another action (click)', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').then(() => {
expect(changed).not.to.be.called
})
.get('button:first').click().then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires when element loses focus due to another action (type)', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').then(() => {
expect(changed).not.to.be.called
})
.get('textarea:first').type('bar').then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires when element is directly blurred', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').blur().then(() => {
expect(changed).to.be.calledOnce
})
})
it('fires when element is tabbed away from')//, ->
// changed = 0
// cy.$$(":text:first").change ->
// changed += 1
// cy.get(":text:first").invoke("val", "foo").type("b{tab}").then ->
// expect(changed).to.eq 1
it('does not fire twice if element is already in focus between type/type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('f').type('o{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire twice if element is already in focus between clear/type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').clear().type('o{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire twice if element is already in focus between click/type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').click().type('o{enter}').then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire twice if element is already in focus between type/click', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('d{enter}').click().then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire at all between clear/type/click', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').clear().type('o').click().then(($el) => {
expect(changed).not.to.be.called
return $el
}).blur()
.then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire if {enter} is preventedDefault', () => {
const changed = cy.stub()
cy.$$(':text:first').keypress((e) => {
if (e.which === 13) {
e.preventDefault()
}
})
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('b{enter}').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire when enter is pressed and value hasnt changed', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.get(':text:first').invoke('val', 'foo').type('b{backspace}{enter}').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire at the end of the type', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').type('foo').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire change event if value hasnt actually changed', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy
.get(':text:first').invoke('val', 'foo').type('{backspace}{backspace}oo{enter}').blur().then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire if mousedown is preventedDefault which prevents element from losing focus', () => {
const changed = cy.stub()
cy.$$(':text:first').change(changed)
cy.$$('textarea:first').mousedown(() => {
return false
})
cy
.get(':text:first').invoke('val', 'foo').type('bar')
.get('textarea:first').click().then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire hitting {enter} inside of a textarea', () => {
const changed = cy.stub()
cy.$$('textarea:first').change(changed)
cy
.get('textarea:first').type('foo{enter}bar').then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire hitting {enter} inside of [contenteditable]', () => {
const changed = cy.stub()
cy.$$('[contenteditable]:first').change(changed)
cy
.get('[contenteditable]:first').type('foo{enter}bar').then(() => {
expect(changed).not.to.be.called
})
})
// [contenteditable] does not fire ANY change events ever.
it('does not fire at ALL for [contenteditable]', () => {
const changed = cy.stub()
cy.$$('[contenteditable]:first').change(changed)
cy
.get('[contenteditable]:first').type('foo')
.get('button:first').click().then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire on .clear() without blur', () => {
const changed = cy.stub()
cy.$$('input:first').change(changed)
cy.get('input:first').invoke('val', 'foo')
.clear()
.then(($el) => {
expect(changed).not.to.be.called
return $el
}).type('foo')
.blur()
.then(() => {
expect(changed).not.to.be.called
})
})
it('fires change for single value change inputs', () => {
const changed = cy.stub()
cy.$$('input[type="date"]:first').change(changed)
cy.get('input[type="date"]:first')
.type('1959-09-13')
.blur()
.then(() => {
expect(changed).to.be.calledOnce
})
})
it('does not fire change for non-change single value input', () => {
const changed = cy.stub()
cy.$$('input[type="date"]:first').change(changed)
cy.get('input[type="date"]:first')
.invoke('val', '1959-09-13')
.type('1959-09-13')
.blur()
.then(() => {
expect(changed).not.to.be.called
})
})
it('does not fire change for type\'d change that restores value', () => {
const changed = cy.stub()
cy.$$('input:first').change(changed)
cy.get('input:first')
.invoke('val', 'foo')
.type('{backspace}o')
.invoke('val', 'bar')
.type('{backspace}r')
.blur()
.then(() => {
expect(changed).not.to.be.called
})
})
})
describe('single value change inputs', () => {
// https://github.com/cypress-io/cypress/issues/5476
it('fires all keyboard events', () => {
@@ -1,3 +1,5 @@
const stripAnsi = require('strip-ansi')
const { assertLogLength } = require('../../support/utils')
const { Promise } = Cypress
@@ -130,6 +132,10 @@ describe('src/cy/commands/fixtures', () => {
expect(err.message).to.include('A fixture file could not be found')
expect(err.message).to.include('cypress/fixtures/err')
// ensure ansi color codes are not embedded in the error msg
// https://github.com/cypress-io/cypress/issues/20208
expect(err.message).to.eq(stripAnsi(err.message))
done()
})
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,9 @@
// @ts-nocheck
import { allowTsModuleStubbing } from '../../support/helpers'
allowTsModuleStubbing()
import $stackUtils from '@packages/driver/src/cypress/stack_utils'
import $errUtils from '@packages/driver/src/cypress/error_utils'
import $errUtils, { CypressError } from '@packages/driver/src/cypress/error_utils'
import $errorMessages from '@packages/driver/src/cypress/error_messages'
describe('driver/src/cypress/error_utils', () => {
@@ -76,6 +74,7 @@ describe('driver/src/cypress/error_utils', () => {
it('throws error when it is an error', () => {
const err = new Error('Something unexpected')
// @ts-ignore
err.extraProp = 'extra prop'
const fn = () => {
$errUtils.throwErr(err)
@@ -117,6 +116,7 @@ describe('driver/src/cypress/error_utils', () => {
context('.errByPath', () => {
beforeEach(() => {
// @ts-ignore
$errorMessages.__test_errors = {
obj: {
message: 'This is a simple error message',
@@ -186,7 +186,7 @@ describe('driver/src/cypress/error_utils', () => {
describe('when message value is an object', () => {
it('has correct name, message, and docs url when path exists', () => {
const err = $errUtils.errByPath('__test_errors.obj')
const err = $errUtils.errByPath('__test_errors.obj') as CypressError
expect(err.name).to.eq('CypressError')
expect(err.message).to.include('This is a simple error message')
@@ -196,7 +196,7 @@ describe('driver/src/cypress/error_utils', () => {
it('uses args provided for the error', () => {
const err = $errUtils.errByPath('__test_errors.obj_with_args', {
foo: 'foo', bar: ['bar', 'qux'],
})
}) as CypressError
expect(err.message).to.include('This has args like \'foo\' and bar,qux')
expect(err.docsUrl).to.include('https://on.link.io')
@@ -205,7 +205,7 @@ describe('driver/src/cypress/error_utils', () => {
it('handles args being used multiple times in message', () => {
const err = $errUtils.errByPath('__test_errors.obj_with_multi_args', {
foo: 'foo', bar: ['bar', 'qux'],
})
}) as CypressError
expect(err.message).to.include('This has args like \'foo\' and bar,qux, and \'foo\' is used twice')
expect(err.docsUrl).to.include('https://on.link.io')
@@ -214,7 +214,7 @@ describe('driver/src/cypress/error_utils', () => {
it('formats markdown in the error message', () => {
const err = $errUtils.errByPath('__test_errors.obj_with_markdown', {
foo: 'foo', bar: ['bar', 'qux'],
})
}) as CypressError
expect(err.message).to.include('This has markdown like `foo`, *bar,qux*, **foo**, and _bar,qux_')
expect(err.docsUrl).to.include('https://on.link.io')
@@ -223,7 +223,7 @@ describe('driver/src/cypress/error_utils', () => {
describe('when message value is a string', () => {
it('has correct name, message, and docs url', () => {
const err = $errUtils.errByPath('__test_errors.str')
const err = $errUtils.errByPath('__test_errors.str') as CypressError
expect(err.name).to.eq('CypressError')
expect(err.message).to.include('This is a simple error message')
@@ -299,7 +299,7 @@ describe('driver/src/cypress/error_utils', () => {
})
it('has the right message and docs url', () => {
const err = $errUtils.errByPath('__test_errors.fn_returns_obj')
const err = $errUtils.errByPath('__test_errors.fn_returns_obj') as CypressError
expect(err.message).to.include('This is a simple error message')
expect(err.docsUrl).to.include('https://on.link.io')
@@ -310,7 +310,7 @@ describe('driver/src/cypress/error_utils', () => {
it('uses them in the error message', () => {
const err = $errUtils.errByPath('__test_errors.fn_returns_obj_with_args', {
foo: 'foo', bar: ['bar', 'qux'],
})
}) as CypressError
expect(err.message).to.include('This has args like \'foo\' and bar,qux')
expect(err.docsUrl).to.include('https://on.link.io')
@@ -321,7 +321,7 @@ describe('driver/src/cypress/error_utils', () => {
it('uses them in the error message', () => {
const err = $errUtils.errByPath('__test_errors.fn_returns_obj_with_multi_args', {
foo: 'foo', bar: ['bar', 'qux'],
})
}) as CypressError
expect(err.message).to.include('This has args like \'foo\' and bar,qux, and \'foo\' is used twice')
expect(err.docsUrl).to.include('https://on.link.io')
@@ -334,6 +334,7 @@ describe('driver/src/cypress/error_utils', () => {
let fn
beforeEach(() => {
// @ts-ignore
$errorMessages.__test_errors = {
test: 'Simple error {{message}}',
}
@@ -370,6 +371,7 @@ describe('driver/src/cypress/error_utils', () => {
context('.throwErrByPath', () => {
it('looks up error and throws it', () => {
// @ts-ignore
$errorMessages.__test_error = 'simple error message'
const fn = () => $errUtils.throwErrByPath('__test_error')
@@ -598,7 +600,7 @@ describe('driver/src/cypress/error_utils', () => {
context('Error.captureStackTrace', () => {
it('works - even where not natively support', () => {
function removeMe2 () {
const err = {}
const err: Record<string, any> = {}
Error.captureStackTrace(err, removeMeAndAbove)
@@ -1,4 +1,4 @@
const $Log = require('@packages/driver/src/cypress/log').default
const { create } = require('@packages/driver/src/cypress/log')
describe('src/cypress/log', function () {
context('#snapshot', function () {
@@ -11,7 +11,7 @@ describe('src/cypress/log', function () {
this.config = cy.stub()
this.config.withArgs('isInteractive').returns(true)
this.config.withArgs('numTestsKeptInMemory').returns(50)
this.log = $Log.create(Cypress, this.cy, this.state, this.config)
this.log = create(Cypress, this.cy, this.state, this.config)
})
it('creates a snapshot and returns the log', function () {
@@ -102,8 +102,8 @@ describe('Proxy Logging', () => {
})
})
// @see https://github.com/cypress-io/cypress/issues/17656
it('xhr log has response body/status code', (done) => {
// @see https://github.com/cypress-io/cypress/issues/18757 and https://github.com/cypress-io/cypress/issues/17656
it('xhr log has response body/status code when xhr response is logged first', (done) => {
cy.window()
.then({ timeout: 10000 }, (win) => {
cy.on('log:changed', (log) => {
@@ -132,6 +132,59 @@ describe('Proxy Logging', () => {
}
})
const oldUpdateRequestWithResponse = Cypress.ProxyLogging.updateRequestWithResponse
cy.stub(Cypress.ProxyLogging, 'updateRequestWithResponse').log(false).callsFake(function (...args) {
setTimeout(() => {
oldUpdateRequestWithResponse.call(this, ...args)
}, 500)
})
const xhr = new win.XMLHttpRequest()
xhr.open('GET', '/some-url')
xhr.send()
})
})
// @see https://github.com/cypress-io/cypress/issues/18757 and https://github.com/cypress-io/cypress/issues/17656
it('xhr log has response body/status code when xhr response is logged second', (done) => {
cy.window()
.then({ timeout: 10000 }, (win) => {
cy.on('log:changed', (log) => {
try {
expect(log.snapshots.map((v) => v.name)).to.deep.eq(['request', 'response'])
expect(log.consoleProps['Response Headers']).to.include({
'x-powered-by': 'Express',
})
expect(log.consoleProps['Response Body']).to.include('Cannot GET /some-url')
expect(log.consoleProps['Response Status Code']).to.eq(404)
expect(log.renderProps).to.include({
indicator: 'bad',
message: 'GET 404 /some-url',
})
expect(Object.keys(log.consoleProps)).to.deep.eq(
['Event', 'Resource Type', 'Method', 'URL', 'Request went to origin?', 'XHR', 'groups', 'Request Headers', 'Response Status Code', 'Response Headers', 'Response Body'],
)
done()
} catch (err) {
// eslint-disable-next-line no-console
console.log('assertion error, retrying', err)
}
})
const oldOnload = cy.state('server').options.onLoad
cy.stub(cy.state('server').options, 'onLoad').log(false).callsFake(function (...args) {
setTimeout(() => {
oldOnload.call(this, ...args)
}, 500)
})
const xhr = new win.XMLHttpRequest()
xhr.open('GET', '/some-url')
@@ -243,7 +243,7 @@ describe('per-test config', () => {
})
})
describe('in mulitple nested suites', {
describe('in multiple nested suites', {
foo: false,
}, () => {
describe('config in suite', {
+1 -1
View File
@@ -82,7 +82,7 @@
"text-mask-addons": "3.8.0",
"underscore.string": "3.3.5",
"unfetch": "4.1.0",
"url-parse": "1.5.6",
"url-parse": "1.5.9",
"vanilla-text-mask": "5.1.1",
"vite": "^2.4.4",
"webpack": "4.41.2",
@@ -275,11 +275,6 @@ export default function (Commands, Cypress, cy, state, config) {
const isContentEditable = $elements.isContentEditable(options.$el.get(0))
const isTextarea = $elements.isTextarea(options.$el.get(0))
// click event is only fired on button, image, submit, reset elements.
// That's why we cannot use $elements.isButtonLike() here.
const type = (type) => $elements.isInputType(options.$el.get(0), type)
const sendClickEvent = type('button') || type('image') || type('submit') || type('reset')
const fireClickEvent = (el) => {
const ctor = $dom.getDocumentFromElement(el).defaultView!.PointerEvent
const event = new ctor('click')
@@ -287,6 +282,8 @@ export default function (Commands, Cypress, cy, state, config) {
el.dispatchEvent(event)
}
let keydownEvents: any[] = []
return keyboard.type({
$el: options.$el,
chars,
@@ -332,21 +329,29 @@ export default function (Commands, Cypress, cy, state, config) {
updateTable(id, key, event, value)
}
if (event.type === 'keydown') {
keydownEvents.push(event.target)
}
if (
// Firefox sends a click event when the Space key is pressed.
// We don't want send it twice.
// We don't want to send it twice.
!Cypress.isBrowser('firefox') &&
// Click event is sent after keyup event with space key.
event.type === 'keyup' && event.code === 'Space' &&
// When event is prevented, the click event should not be emitted
!event.defaultPrevented &&
// Click events should be only sent to button-like elements.
// event.target is null when used with shadow DOM.
(event.target && $elements.isButtonLike(event.target)) &&
// When a space key is pressed for input radio elements, the click event is only fired when it's not checked.
!(event.target.tagName === 'INPUT' && event.target.type === 'radio' && event.target.checked === true) &&
// When event is prevented, the click event should not be emitted
!event.defaultPrevented
// When a space key is pressed on another element, the click event should not be fired.
keydownEvents.includes(event.target)
) {
fireClickEvent(event.target)
keydownEvents = []
}
},
@@ -391,6 +396,11 @@ export default function (Commands, Cypress, cy, state, config) {
return
}
// click event is only fired on button, image, submit, reset elements.
// That's why we cannot use $elements.isButtonLike() here.
const type = (type) => $elements.isInputType(el, type)
const sendClickEvent = type('button') || type('image') || type('submit') || type('reset')
// https://github.com/cypress-io/cypress/issues/19541
// Send click event on type('{enter}')
if (sendClickEvent) {
@@ -5,7 +5,7 @@ import Promise from 'bluebird'
import $utils from '../../cypress/utils'
import $errUtils from '../../cypress/error_utils'
import $Log from '../../cypress/log'
import { LogUtils } from '../../cypress/log'
import { bothUrlsMatchAndOneHasHash } from '../navigation'
import { $Location } from '../../cypress/location'
@@ -1105,7 +1105,7 @@ export default (Commands, Cypress, cy, state, config) => {
s.passed = Cypress.runner.countByTestState(s.tests, 'passed')
s.failed = Cypress.runner.countByTestState(s.tests, 'failed')
s.pending = Cypress.runner.countByTestState(s.tests, 'pending')
s.numLogs = $Log.countLogsByTests(s.tests)
s.numLogs = LogUtils.countLogsByTests(s.tests)
return Cypress.action('cy:collect:run:state')
.then((a = []) => {
+102 -48
View File
@@ -1,5 +1,3 @@
// @ts-nocheck
import { validate, validateNoReadOnlyConfig } from '@packages/config'
import _ from 'lodash'
import $ from 'jquery'
@@ -19,7 +17,7 @@ import $dom from './dom'
import $Downloads from './cypress/downloads'
import $errorMessages from './cypress/error_messages'
import $errUtils from './cypress/error_utils'
import $Log from './cypress/log'
import { create as createLogFn, LogUtils } from './cypress/log'
import $LocalStorage from './cypress/local_storage'
import $Mocha from './cypress/mocha'
import { create as createMouse } from './cy/mouse'
@@ -43,6 +41,15 @@ import { PrimaryDomainCommunicator, SpecBridgeDomainCommunicator } from './multi
const debug = debugFn('cypress:driver:cypress')
declare global {
interface Window {
__cySkipValidateConfig: boolean
Cypress: Cypress.Cypress
Runner: any
cy: Cypress.cy
}
}
const jqueryProxyFn = function (...args) {
if (!this.cy) {
$errUtils.throwErrByPath('miscellaneous.no_cy')
@@ -57,7 +64,83 @@ const throwPrivateCommandInterface = (method) => {
})
}
interface BackendError extends Error {
__stackCleaned__: boolean
backend: boolean
}
interface AutomationError extends Error {
automation: boolean
}
class $Cypress {
cy: any
chai: any
mocha: any
runner: any
downloads: any
Commands: any
$autIframe: any
onSpecReady: any
events: any
$: any
arch: any
spec: any
version: any
browser: any
platform: any
testingType: any
state: any
originalConfig: any
config: any
env: any
getTestRetries: any
Cookies: any
ProxyLogging: any
_onInitialize: any
isCy: any
log: any
isBrowser: any
emit: any
emitThen: any
emitMap: any
// attach to $Cypress to access
// all of the constructors
// to enable users to monkeypatch
$Cypress = $Cypress
Cy = $Cy
Chainer = $Chainer
Command = $Command
dom = $dom
errorMessages = $errorMessages
Keyboard = $Keyboard
Location = $Location
Log = LogUtils
LocalStorage = $LocalStorage
Mocha = $Mocha
resolveWindowReference = resolvers.resolveWindowReference
resolveLocationReference = resolvers.resolveLocationReference
Mouse = {
create: createMouse,
}
Runner = $Runner
Server = $Server
Screenshot = $Screenshot
SelectorPlayground = $SelectorPlayground
utils = $utils
_ = _
Blob = blobUtil
Buffer = Buffer
Promise = Promise
minimatch = minimatch
sinon = sinon
lolex = fakeTimers
static $: any
static utils: any
constructor () {
this.cy = null
this.chai = null
@@ -148,7 +231,7 @@ class $Cypress {
this.state = $SetterGetter.create({})
this.originalConfig = _.cloneDeep(config)
this.config = $SetterGetter.create(config, (config) => {
if (this.isMultiDomain ? !window.__cySkipValidateConfig : !window.top.__cySkipValidateConfig) {
if (this.isMultiDomain ? !window.__cySkipValidateConfig : !window.top!.__cySkipValidateConfig) {
validateNoReadOnlyConfig(config, (errProperty) => {
const errPath = this.state('runnable')
? 'config.invalid_cypress_config_override'
@@ -172,7 +255,7 @@ class $Cypress {
// and those leak out into the stdout formatting.
const errMsg = _.isString(errResult)
? errResult
: `Expected ${format(errResult.key)} to be ${errResult.type}.\n\nInstead the value was: ${stringify(errResult.value)}\``
: `Expected ${format(errResult.key)} to be ${errResult.type}.\n\nInstead the value was: ${stringify(errResult.value)}`
throw new this.state('specWindow').Error(errMsg)
})
@@ -195,6 +278,8 @@ class $Cypress {
this.Cookies = $Cookies.create(config.namespace, d)
// TODO: Remove this after $Events functions are added to $Cypress.
// @ts-ignore
this.ProxyLogging = new ProxyLogging(this)
return this.action('cypress:config', config)
@@ -220,15 +305,15 @@ class $Cypress {
// Method to manually re-execute Runner (usually within $autIframe)
// used mainly by Component Testing
restartRunner () {
if (!window.top.Cypress) {
if (!window.top!.Cypress) {
throw Error('Cannot re-run spec without Cypress')
}
// MobX state is only available on the Runner instance
// which is attached to the top level `window`
// We avoid infinite restart loop by checking if not in a loading state.
if (!window.top.Runner.state.isLoading) {
window.top.Runner.emit('restart')
if (!window.top!.Runner.state.isLoading) {
window.top!.Runner.emit('restart')
}
}
@@ -242,7 +327,7 @@ class $Cypress {
this.cy = new $Cy(specWindow, this, this.Cookies, this.state, this.config)
window.cy = this.cy
this.isCy = this.cy.isCy
this.log = $Log.create(this, this.cy, this.state, this.config)
this.log = createLogFn(this, this.cy, this.state, this.config)
this.mocha = $Mocha.create(specWindow, this, this.config)
this.runner = $Runner.create(specWindow, this.mocha, this, this.cy, this.state)
this.downloads = $Downloads.create(this)
@@ -253,6 +338,8 @@ class $Cypress {
this.events.proxyTo(this.cy)
$scriptUtils.runScripts(specWindow, scripts)
// TODO: remove this after making the type of `runScripts` more specific.
// @ts-ignore
.catch((error) => {
this.runner.onSpecError('error')({ error })
})
@@ -279,6 +366,8 @@ class $Cypress {
return this.backend('firefox:window:focus')
}
}
return
})
.then(() => {
this.cy.initialize(this.$autIframe)
@@ -604,7 +693,7 @@ class $Cypress {
// attaching long stace traces
// which otherwise make this err
// unusably long
const err = $errUtils.makeErrFromObj(e)
const err = $errUtils.makeErrFromObj(e) as BackendError
err.__stackCleaned__ = true
err.backend = true
@@ -626,7 +715,7 @@ class $Cypress {
const e = reply.error
if (e) {
const err = $errUtils.makeErrFromObj(e)
const err = $errUtils.makeErrFromObj(e) as AutomationError
err.automation = true
@@ -689,43 +778,8 @@ class $Cypress {
}
}
// // attach to $Cypress to access
// // all of the constructors
// // to enable users to monkeypatch
$Cypress.prototype.$Cypress = $Cypress
$Cypress.prototype.Cy = $Cy
$Cypress.prototype.Chainer = $Chainer
$Cypress.prototype.Cookies = $Cookies
$Cypress.prototype.Command = $Command
$Cypress.prototype.Commands = $Commands
$Cypress.prototype.dom = $dom
$Cypress.prototype.errorMessages = $errorMessages
$Cypress.prototype.Keyboard = $Keyboard
$Cypress.prototype.Location = $Location
$Cypress.prototype.Log = $Log
$Cypress.prototype.LocalStorage = $LocalStorage
$Cypress.prototype.Mocha = $Mocha
$Cypress.prototype.resolveWindowReference = resolvers.resolveWindowReference
$Cypress.prototype.resolveLocationReference = resolvers.resolveLocationReference
$Cypress.prototype.Mouse = {
create: createMouse,
}
$Cypress.prototype.Runner = $Runner
$Cypress.prototype.Server = $Server
$Cypress.prototype.Screenshot = $Screenshot
$Cypress.prototype.SelectorPlayground = $SelectorPlayground
$Cypress.prototype.utils = $utils
$Cypress.prototype._ = _
$Cypress.prototype.Blob = blobUtil
$Cypress.prototype.Buffer = Buffer
$Cypress.prototype.Promise = Promise
$Cypress.prototype.minimatch = minimatch
$Cypress.prototype.sinon = sinon
$Cypress.prototype.lolex = fakeTimers
// // attaching these so they are accessible
// // via the runner + integration spec helper
// attaching these so they are accessible
// via the runner + integration spec helper
$Cypress.$ = $
$Cypress.utils = $utils
export default $Cypress
+5 -1
View File
@@ -422,7 +422,11 @@ const preferredStackAndCodeFrameIndex = (err, userInvocationStack) => {
return { stack, index }
}
const enhanceStack = ({ err, userInvocationStack, projectRoot }) => {
const enhanceStack = ({ err, userInvocationStack, projectRoot }: {
err: any
userInvocationStack?: any
projectRoot?: any
}) => {
const { stack, index } = preferredStackAndCodeFrameIndex(err, userInvocationStack)
const { sourceMapped, parsed } = $stackUtils.getSourceStack(stack, projectRoot)
+417 -407
View File
@@ -1,11 +1,9 @@
// @ts-nocheck
import _ from 'lodash'
import $ from 'jquery'
import clone from 'clone'
import { HIGHLIGHT_ATTR } from '../cy/snapshots'
import * as $Events from './events'
import { extend as extendEvents } from './events'
import $dom from '../dom'
import $utils from './utils'
import $errUtils from './error_utils'
@@ -20,94 +18,96 @@ const BLACKLIST_PROPS = 'snapshots'.split(' ')
let counter = 0
// mutate attrs by nulling out
// object properties
const reduceMemory = (attrs) => {
return _.each(attrs, (value, key) => {
if (_.isObject(value)) {
attrs[key] = null
}
})
}
export const LogUtils = {
// mutate attrs by nulling out
// object properties
reduceMemory: (attrs) => {
return _.each(attrs, (value, key) => {
if (_.isObject(value)) {
attrs[key] = null
}
})
},
const toSerializedJSON = function (attrs) {
const { isDom } = $dom
toSerializedJSON (attrs) {
const { isDom } = $dom
const stringify = function (value, key) {
if (BLACKLIST_PROPS.includes(key)) {
return null
}
if (_.isArray(value)) {
return _.map(value, stringify)
}
if (isDom(value)) {
return $dom.stringify(value, 'short')
}
if (!(!_.isFunction(value) || !groupsOrTableRe.test(key))) {
return value()
}
if (_.isFunction(value) || _.isSymbol(value)) {
return value.toString()
}
if (_.isObject(value)) {
// clone to nuke circular references
// and blow away anything that throws
try {
return _.mapValues(clone(value), stringify)
} catch (err) {
const stringify = function (value, key) {
if (BLACKLIST_PROPS.includes(key)) {
return null
}
if (_.isArray(value)) {
return _.map(value, stringify)
}
if (isDom(value)) {
return $dom.stringify(value, 'short')
}
if (_.isFunction(value) && groupsOrTableRe.test(key)) {
return value()
}
if (_.isFunction(value) || _.isSymbol(value)) {
return value.toString()
}
if (_.isObject(value)) {
// clone to nuke circular references
// and blow away anything that throws
try {
return _.mapValues(clone(value), stringify)
} catch (err) {
return null
}
}
return value
}
return value
}
return _.mapValues(attrs, stringify)
},
return _.mapValues(attrs, stringify)
}
getDisplayProps: (attrs) => {
return {
..._.pick(attrs, DISPLAY_PROPS),
hasSnapshot: !!attrs.snapshots,
hasConsoleProps: !!attrs.consoleProps,
}
},
const getDisplayProps = (attrs) => {
return {
..._.pick(attrs, DISPLAY_PROPS),
hasSnapshot: !!attrs.snapshots,
hasConsoleProps: !!attrs.consoleProps,
}
}
getConsoleProps: (attrs) => {
return attrs.consoleProps
},
const getConsoleProps = (attrs) => {
return attrs.consoleProps
}
getSnapshotProps: (attrs) => {
return _.pick(attrs, SNAPSHOT_PROPS)
},
const getSnapshotProps = (attrs) => {
return _.pick(attrs, SNAPSHOT_PROPS)
}
countLogsByTests (tests: Record<string, any> = {}) {
if (_.isEmpty(tests)) {
return 0
}
const countLogsByTests = function (tests = {}) {
if (_.isEmpty(tests)) {
return 0
}
return _
.chain(tests)
.flatMap((test) => {
return [test, test.prevAttempts]
})
.flatMap<{id: number}>((tests) => {
return [].concat(tests.agents, tests.routes, tests.commands)
}).compact()
.union([{ counter: 0 }])
.map('counter')
.max()
.value()
},
// Counts the number of logs by determining the greatest counter among all test attempts and logs.
return _
.chain(tests)
.flatMap((test) => {
return [test, test.prevAttempts]
})
.flatMap((tests) => {
return [].concat(tests.agents, tests.routes, tests.commands)
}).compact()
.union([{ counter: 0 }])
.map('counter')
.max()
.value()
}
const setCounter = (num) => {
return counter = num
// TODO: fix this
setCounter: (num) => {
return counter = num
},
}
const defaults = function (state, config, obj) {
@@ -221,356 +221,363 @@ const defaults = function (state, config, obj) {
return obj
}
const Log = function (cy, state, config, obj) {
obj = defaults(state, config, obj)
class Log {
cy: any
state: any
config: any
fireChangeEvent: ((log) => (void | undefined))
obj: any
// private attributes of each log
const attributes = {}
private attributes: Record<string, any> = {}
return {
get (attr) {
if (attr) {
return attributes[attr]
constructor (cy, state, config, fireChangeEvent, obj) {
this.cy = cy
this.state = state
this.config = config
this.fireChangeEvent = fireChangeEvent
this.obj = defaults(state, config, obj)
extendEvents(this)
}
get (attr) {
if (attr) {
return this.attributes[attr]
}
return this.attributes
}
unset (key) {
return this.set(key, undefined)
}
invoke (key) {
const invoke = () => {
// ensure this is a callable function
// and set its default to empty object literal
const fn = this.get(key)
if (_.isFunction(fn)) {
return fn()
}
return attributes
},
return fn
}
unset (key) {
return this.set(key, undefined)
},
return invoke() || {}
}
invoke (key) {
const invoke = () => {
// ensure this is a callable function
// and set its default to empty object literal
const fn = this.get(key)
toJSON () {
return _
.chain(this.attributes)
.omit('error')
.omitBy(_.isFunction)
.extend({
err: $errUtils.wrapErr(this.get('error')),
consoleProps: this.invoke('consoleProps'),
renderProps: this.invoke('renderProps'),
})
.value()
}
if (_.isFunction(fn)) {
return fn()
}
set (key, val?) {
if (_.isString(key)) {
this.obj = {}
this.obj[key] = val
} else {
this.obj = key
}
return fn
if ('url' in this.obj) {
// always stringify the url property
this.obj.url = (this.obj.url != null ? this.obj.url : '').toString()
}
// convert onConsole to consoleProps
// for backwards compatibility
if (this.obj.onConsole) {
this.obj.consoleProps = this.obj.onConsole
}
// if we have an alias automatically
// figure out what type of alias it is
if (this.obj.alias) {
_.defaults(this.obj, { aliasType: this.obj.$el ? 'dom' : 'primitive' })
}
// dont ever allow existing id's to be mutated
if (this.attributes.id) {
delete this.obj.id
}
_.extend(this.attributes, this.obj)
// if we have an consoleProps function
// then re-wrap it
if (this.obj && _.isFunction(this.obj.consoleProps)) {
this.wrapConsoleProps()
}
if (this.obj && this.obj.$el) {
this.setElAttrs()
}
this.fireChangeEvent(this)
return this
}
pick (...args) {
return _.pick(this.attributes, args)
}
snapshot (name?, options: any = {}) {
// bail early and don't snapshot if we're in headless mode
// or we're not storing tests
if (!this.config('isInteractive') || (this.config('numTestsKeptInMemory') === 0)) {
return this
}
_.defaults(options, {
at: null,
next: null,
})
const snapshot = this.cy.createSnapshot(name, this.get('$el'))
const snapshots = this.get('snapshots') || []
// don't add snapshot if we couldn't create one, which can happen
// if the snapshotting process errors
// https://github.com/cypress-io/cypress/issues/15816
if (snapshot) {
// insert at index 'at' or whatever is the next position
snapshots[options.at || snapshots.length] = snapshot
}
this.set('snapshots', snapshots)
if (options.next) {
const fn = this.snapshot
this.snapshot = function () {
// restore the fn
this.snapshot = fn
// call orig fn with next as name
return fn.call(this, options.next)
}
}
return this
}
error (err) {
this.set({
ended: true,
error: err,
state: 'failed',
})
return this
}
end () {
// dont set back to passed if we've already ended
if (this.get('ended')) {
// we do need to trigger the change event since
// xhr onLoad and proxy-logging updateRequestWithResponse can sometimes
// happen in a different order and the log data in each is different
this.fireChangeEvent(this)
return
}
this.set({
ended: true,
state: 'passed',
})
return this
}
getError (err) {
return err.stack || err.message
}
setElAttrs () {
const $el = this.get('$el')
if (!$el) {
return
}
if (_.isElement($el)) {
// wrap the element in jquery
// if its just a plain element
return this.set('$el', $($el))
}
// if we've passed something like
// <window> or <document> here or
// a primitive then unset $el
if (!$dom.isJquery($el)) {
return this.unset('$el')
}
// make sure all $el elements are visible!
this.obj = {
highlightAttr: HIGHLIGHT_ATTR,
numElements: $el.length,
visible: $el.length === $el.filter(':visible').length,
}
return this.set(this.obj, { silent: true })
}
merge (log) {
// merges another logs attributes into
// ours by also removing / adding any properties
// on the original
// 1. calculate which properties to unset
const unsets = _.chain(this.attributes).keys().without(..._.keys(log.get())).value()
_.each(unsets, (unset) => {
return this.unset(unset)
})
// 2. merge in any other properties
return this.set(log.get())
}
_shouldAutoEnd () {
// must be autoEnd
// and not already ended
// and not an event
// and a command
return (this.get('autoEnd') !== false) &&
(this.get('ended') !== true) &&
(this.get('event') === false) &&
(this.get('instrument') === 'command')
}
finish () {
// end our command since our subject
// has been resolved at this point
// unless its already been 'ended'
// or has been specifically told not to auto resolve
if (this._shouldAutoEnd()) {
if (this.get('snapshot') !== false) {
this.snapshot()
}
return invoke() || {}
},
return this.end()
}
toJSON () {
return _
.chain(attributes)
.omit('error')
.omitBy(_.isFunction)
.extend({
err: $errUtils.wrapErr(this.get('error')),
consoleProps: this.invoke('consoleProps'),
renderProps: this.invoke('renderProps'),
})
.value()
},
return
}
set (key, val) {
if (_.isString(key)) {
obj = {}
obj[key] = val
wrapConsoleProps () {
const _this = this
const { consoleProps } = this.attributes
this.attributes.consoleProps = function (...args) {
const key = _this.get('event') ? 'Event' : 'Command'
const consoleObj: Record<string, any> = {}
consoleObj[key] = _this.get('name')
// merge in the other properties from consoleProps
_.extend(consoleObj, consoleProps.apply(this, args))
// TODO: right here we need to automatically
// merge in "Yielded + Element" if there is an $el
// and finally add error if one exists
if (_this.get('error')) {
_.defaults(consoleObj, {
Error: _this.getError(_this.get('error')),
})
}
// add note if no snapshot exists on command instruments
if ((_this.get('instrument') === 'command') && !_this.get('snapshots')) {
consoleObj.Snapshot = 'The snapshot is missing. Displaying current state of the DOM.'
} else {
obj = key
delete consoleObj.Snapshot
}
if ('url' in obj) {
// always stringify the url property
obj.url = (obj.url != null ? obj.url : '').toString()
}
// convert onConsole to consoleProps
// for backwards compatibility
if (obj.onConsole) {
obj.consoleProps = obj.onConsole
}
// if we have an alias automatically
// figure out what type of alias it is
if (obj.alias) {
_.defaults(obj, { aliasType: obj.$el ? 'dom' : 'primitive' })
}
// dont ever allow existing id's to be mutated
if (attributes.id) {
delete obj.id
}
_.extend(attributes, obj)
// if we have an consoleProps function
// then re-wrap it
if (obj && _.isFunction(obj.consoleProps)) {
this.wrapConsoleProps()
}
if (obj && obj.$el) {
this.setElAttrs()
}
this.fireChangeEvent()
return this
},
pick (...args) {
return _.pick(attributes, args)
},
snapshot (name, options = {}) {
// bail early and don't snapshot if we're in headless mode
// or we're not storing tests
if (!config('isInteractive') || (config('numTestsKeptInMemory') === 0)) {
return this
}
_.defaults(options, {
at: null,
next: null,
})
const snapshot = cy.createSnapshot(name, this.get('$el'))
const snapshots = this.get('snapshots') || []
// don't add snapshot if we couldn't create one, which can happen
// if the snapshotting process errors
// https://github.com/cypress-io/cypress/issues/15816
if (snapshot) {
// insert at index 'at' or whatever is the next position
snapshots[options.at || snapshots.length] = snapshot
}
this.set('snapshots', snapshots)
if (options.next) {
const fn = this.snapshot
this.snapshot = function () {
// restore the fn
this.snapshot = fn
// call orig fn with next as name
return fn.call(this, options.next)
}
}
return this
},
error (err) {
this.set({
ended: true,
error: err,
state: 'failed',
})
return this
},
end () {
// dont set back to passed
// if we've already ended
if (this.get('ended')) {
return
}
this.set({
ended: true,
state: 'passed',
})
return this
},
getError (err) {
return err.stack || err.message
},
setElAttrs () {
const $el = this.get('$el')
if (!$el) {
return
}
if (_.isElement($el)) {
// wrap the element in jquery
// if its just a plain element
return this.set('$el', $($el), { silent: true })
}
// if we've passed something like
// <window> or <document> here or
// a primitive then unset $el
if (!$dom.isJquery($el)) {
return this.unset('$el')
}
// make sure all $el elements are visible!
obj = {
highlightAttr: HIGHLIGHT_ATTR,
numElements: $el.length,
visible: $el.length === $el.filter(':visible').length,
}
return this.set(obj, { silent: true })
},
merge (log) {
// merges another logs attributes into
// ours by also removing / adding any properties
// on the original
// 1. calculate which properties to unset
const unsets = _.chain(attributes).keys().without(..._.keys(log.get())).value()
_.each(unsets, (unset) => {
return this.unset(unset)
})
// 2. merge in any other properties
return this.set(log.get())
},
_shouldAutoEnd () {
// must be autoEnd
// and not already ended
// and not an event
// and a command
return (this.get('autoEnd') !== false) &&
(this.get('ended') !== true) &&
(this.get('event') === false) &&
(this.get('instrument') === 'command')
},
finish () {
// end our command since our subject
// has been resolved at this point
// unless its already been 'ended'
// or has been specifically told not to auto resolve
if (this._shouldAutoEnd()) {
if (this.get('snapshot') !== false) {
this.snapshot()
}
return this.end()
}
},
wrapConsoleProps () {
const _this = this
const { consoleProps } = attributes
attributes.consoleProps = function (...args) {
const key = _this.get('event') ? 'Event' : 'Command'
const consoleObj = {}
consoleObj[key] = _this.get('name')
// merge in the other properties from consoleProps
_.extend(consoleObj, consoleProps.apply(this, args))
// TODO: right here we need to automatically
// merge in "Yielded + Element" if there is an $el
// and finally add error if one exists
if (_this.get('error')) {
_.defaults(consoleObj, {
Error: _this.getError(_this.get('error')),
})
}
// add note if no snapshot exists on command instruments
if ((_this.get('instrument') === 'command') && !_this.get('snapshots')) {
consoleObj.Snapshot = 'The snapshot is missing. Displaying current state of the DOM.'
} else {
delete consoleObj.Snapshot
}
return consoleObj
}
},
return consoleObj
}
}
}
export default {
reduceMemory,
class LogManager {
logs: Record<string, any> = {}
toSerializedJSON,
constructor () {
this.fireChangeEvent = this.fireChangeEvent.bind(this)
}
getDisplayProps,
getConsoleProps,
getSnapshotProps,
countLogsByTests,
setCounter,
create (Cypress, cy, state, config) {
counter = 0
const logs = {}
const trigger = function (log, event) {
trigger (log, event) {
// bail if we never fired our initial log event
if (!log._hasInitiallyLogged) {
return
}
// bail if we've reset the logs due to a Cypress.abort
if (!logs[log.get('id')]) {
return
}
const attrs = log.toJSON()
// only trigger this event if our last stored
// emitted attrs do not match the current toJSON
if (!_.isEqual(log._emittedAttrs, attrs)) {
log._emittedAttrs = attrs
log.emit(event, attrs)
return Cypress.action(event, attrs, log)
}
if (!log._hasInitiallyLogged) {
return
}
const triggerLog = function (log) {
log._hasInitiallyLogged = true
return trigger(log, 'command:log:added')
// bail if we've reset the logs due to a Cypress.abort
if (!this.logs[log.get('id')]) {
return
}
const addToLogs = function (log) {
const id = log.get('id')
const attrs = log.toJSON()
logs[id] = true
// only trigger this event if our last stored
// emitted attrs do not match the current toJSON
if (!_.isEqual(log._emittedAttrs, attrs)) {
log._emittedAttrs = attrs
log.emit(event, attrs)
return Cypress.action(event, attrs, log)
}
}
triggerLog (log) {
log._hasInitiallyLogged = true
return this.trigger(log, 'command:log:added')
}
addToLogs (log) {
const id = log.get('id')
this.logs[id] = true
}
// only fire the log:state:changed event
// as fast as every 4ms
fireChangeEvent (log) {
const triggerStateChanged = () => {
return this.trigger(log, 'command:log:changed')
}
const logFn = function (options = {}) {
const debounceFn = _.debounce(triggerStateChanged, 4)
return debounceFn()
}
createLogFn (cy, state, config) {
return (options: any = {}) => {
if (!_.isObject(options)) {
$errUtils.throwErrByPath('log.invalid_argument', { args: { arg: options } })
}
const log = Log(cy, state, config, options)
// add event emitter interface
$Events.extend(log)
const triggerStateChanged = () => {
return trigger(log, 'command:log:changed')
}
// only fire the log:state:changed event
// as fast as every 4ms
log.fireChangeEvent = _.debounce(triggerStateChanged, 4)
const log = new Log(cy, state, config, this.fireChangeEvent, options)
log.set(options)
@@ -583,11 +590,11 @@ export default {
// if end was passed in
// go ahead and end
if (log.get('end')) {
log.end({ silent: true })
log.end()
}
if (log.get('error')) {
log.error(log.get('error'), { silent: true })
log.error(log.get('error'))
}
log.wrapConsoleProps()
@@ -609,7 +616,7 @@ export default {
current.log(log)
}
addToLogs(log)
this.addToLogs(log)
if (options.sessionInfo) {
Cypress.emit('session:add', log.toJSON())
@@ -619,19 +626,22 @@ export default {
return
}
triggerLog(log)
this.triggerLog(log)
// if not current state then the log is being run
// with no command reference, so just end the log
if (!current) {
log.end({ silent: true })
log.end()
}
return log
}
logFn._logs = logs
return logFn
},
}
}
export function create (Cypress, cy, state, config) {
counter = 0
const logManager = new LogManager()
return logManager.createLogFn(cy, state, config)
}
+11 -9
View File
@@ -1,7 +1,6 @@
/* eslint-disable prefer-rest-params */
// @ts-nocheck
import _ from 'lodash'
import $errUtils from './error_utils'
import $errUtils, { CypressError } from './error_utils'
import $utils from './utils'
import $stackUtils from './stack_utils'
@@ -11,7 +10,7 @@ import * as mocha from 'mocha'
const { getTestFromRunnable } = $utils
const Mocha = mocha.Mocha != null ? mocha.Mocha : mocha
const Mocha = (mocha as any).Mocha != null ? (mocha as any).Mocha : mocha
const { Test, Runner, Runnable, Hook, Suite } = Mocha
@@ -33,8 +32,8 @@ const suiteAfterAll = Suite.prototype.afterAll
const suiteAfterEach = Suite.prototype.afterEach
// don't let mocha pollute the global namespace
delete window.mocha
delete window.Mocha
delete (window as any).mocha
delete (window as any).Mocha
function invokeFnWithOriginalTitle (ctx, originalTitle, mochaArgs, fn, _testConfig) {
const ret = fn.apply(ctx, mochaArgs)
@@ -68,7 +67,7 @@ function overloadMochaFnForConfig (fnName, specWindow) {
const origFn = subFn ? _fn[subFn] : _fn
if (args.length > 2 && _.isObject(args[1])) {
const _testConfig = _.extend({}, args[1])
const _testConfig = _.extend({}, args[1]) as any
const mochaArgs = [args[0], args[2]]
@@ -447,15 +446,18 @@ const patchSuiteHooks = (specWindow, config) => {
let invocationStack = hook.invocationDetails?.stack
if (!hook.invocationDetails) {
const invocationDetails = $stackUtils.getInvocationDetails(specWindow, config)
const invocationDetails = $stackUtils.getInvocationDetails(specWindow, config)!
hook.invocationDetails = invocationDetails
invocationStack = invocationDetails.stack
}
if (this._condensedHooks) {
throw $errUtils.errByPath('mocha.hook_registered_late', { hookTitle: fnName })
.setUserInvocationStack(invocationStack)
const err = $errUtils.errByPath('mocha.hook_registered_late', { hookTitle: fnName }) as CypressError
err.setUserInvocationStack(invocationStack)
throw err
}
return hook
+63 -47
View File
@@ -1,12 +1,9 @@
/* eslint-disable prefer-rest-params */
/* globals Cypress */
// @ts-nocheck
import _ from 'lodash'
import dayjs from 'dayjs'
import Promise from 'bluebird'
import $Log from './log'
import { LogUtils } from './log'
import $utils from './utils'
import $errUtils from './error_utils'
import $stackUtils from './stack_utils'
@@ -28,6 +25,10 @@ const RUNNABLE_PROPS = '_testConfig id order title _titlePath root hookName hook
const debug = debugFn('cypress:driver:runner')
const debugErrors = debugFn('cypress:driver:errors')
const duration = (before: Date, after: Date) => {
return Number(before) - Number(after)
}
const fire = (event, runnable, Cypress) => {
debug('fire: %o', { event })
if (runnable._fired == null) {
@@ -103,6 +104,8 @@ const testAfterRun = (test, Cypress) => {
// prevent loop comprehension
return null
}
return null
}
const setTestTimingsForHook = (test, hookName, obj) => {
@@ -126,7 +129,7 @@ const setTestTimings = (test, name, obj) => {
}
const setWallClockDuration = (test) => {
return test.wallClockDuration = new Date() - test.wallClockStartedAt
return test.wallClockDuration = duration(new Date(), test.wallClockStartedAt)
}
// we need to optimize wrap by converting
@@ -137,7 +140,7 @@ const wrap = (runnable) => {
return $utils.reduceProps(runnable, RUNNABLE_PROPS)
}
const wrapAll = (runnable) => {
const wrapAll = (runnable): any => {
return _.extend(
{},
$utils.reduceProps(runnable, RUNNABLE_PROPS),
@@ -201,7 +204,7 @@ const eachHookInSuite = (suite, fn) => {
// iterates over a suite's tests (including nested suites)
// and will return as soon as the callback is true
const findTestInSuite = (suite, fn = _.identity) => {
const findTestInSuite = (suite, fn: any = _.identity) => {
for (const test of suite.tests) {
if (fn(test)) {
return test
@@ -217,7 +220,7 @@ const findTestInSuite = (suite, fn = _.identity) => {
}
}
const findSuiteInSuite = (suite, fn = _.identity) => {
const findSuiteInSuite = (suite, fn: any = _.identity) => {
if (fn(suite)) {
return suite
}
@@ -240,7 +243,7 @@ const suiteHasSuite = (suite, suiteId) => {
}
// same as findTestInSuite but iterates backwards
const findLastTestInSuite = (suite, fn = _.identity) => {
const findLastTestInSuite = (suite, fn: any = _.identity) => {
for (let i = suite.suites.length - 1; i >= 0; i--) {
const test = findLastTestInSuite(suite.suites[i], fn)
@@ -259,7 +262,7 @@ const findLastTestInSuite = (suite, fn = _.identity) => {
}
const getAllSiblingTests = (suite, getTestById) => {
const tests = []
const tests: any[] = []
suite.eachTest((testRunnable) => {
// iterate through each of our suites tests.
@@ -271,6 +274,8 @@ const getAllSiblingTests = (suite, getTestById) => {
if (test) {
return tests.push(test)
}
return
})
return tests
@@ -300,7 +305,7 @@ const isLastSuite = (suite, tests) => {
// grab all of the suites from our filtered tests
// including all of their ancestor suites!
const suites = _.reduce(tests, (memo, test) => {
const suites = _.reduce<any, any[]>(tests, (memo, test) => {
let parent
while ((parent = test.parent)) {
@@ -309,8 +314,7 @@ const isLastSuite = (suite, tests) => {
}
return memo
}
, [])
}, [])
// intersect them with our parent suites and see if the last one is us
return _
@@ -357,7 +361,7 @@ const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, get
const test = getTest()
const allTests = getTests()
let shouldFireTestAfterRun = _.noop
let shouldFireTestAfterRun = () => false
switch (name) {
case 'afterEach':
@@ -375,6 +379,8 @@ const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, get
return true
}
}
return false
}
break
@@ -395,7 +401,7 @@ const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, get
// due to already being run on top navigation
// https://github.com/cypress-io/cypress/issues/9026
if (!testIsActuallyInSuite) {
return
return false
}
// 1. if we're the very last test in the entire allTests
@@ -410,6 +416,8 @@ const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, get
return true
}
}
return false
}
break
@@ -449,7 +457,7 @@ const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, get
const getTestResults = (tests) => {
return _.map(tests, (test) => {
const obj = _.pick(test, 'id', 'duration', 'state')
const obj: Record<string, any> = _.pick(test, 'id', 'duration', 'state')
obj.title = test.originalTitle
// TODO FIX THIS!
@@ -487,7 +495,7 @@ const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnab
// we hand back a normalized object but also
// create optimized lookups for the tests without
// traversing through it multiple times
const tests = {}
const tests: Record<string, any> = {}
const normalizedSuite = normalize(suite, tests, initialTests, onRunnable, onLogsById, getRunnableId, getHookId, getOnlyTestId, getOnlySuiteId, createEmptyOnlyTest)
if (setTestsById) {
@@ -518,6 +526,8 @@ const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnab
}
normalizedSuite.runtimeConfig[key] = v
return
})
return normalizedSuite
@@ -659,6 +669,8 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getRun
return normalizedChild
}))
}
return null
})
return normalizedRunnable
@@ -736,6 +748,8 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getRun
return normalizedChildSuite
}
return null
}))
}
@@ -1016,8 +1030,8 @@ export default {
create: (specWindow, mocha, Cypress, cy, state) => {
let _runnableId = 0
let _hookId = 0
let _uncaughtFn = null
let _resumedAtTestIndex = null
let _uncaughtFn: (() => never) | null = null
let _resumedAtTestIndex: number | null = null
const _runner = mocha.getRunner()
@@ -1098,19 +1112,19 @@ export default {
specWindow.addEventListener('unhandledrejection', onSpecError('unhandledrejection'))
// hold onto the _runnables for faster lookup later
let _test = null
let _tests = []
let _testsById = {}
const _testsQueue = []
const _testsQueueById = {}
let _test: any = null
let _tests: any[] = []
let _testsById: Record<string, any> = {}
const _testsQueue: any[] = []
const _testsQueueById: Record<string, any> = {}
// only used during normalization
const _runnables = []
const _logsById = {}
const _runnables: any[] = []
const _logsById: Record<string, any> = {}
let _emissions = {
started: {},
ended: {},
}
let _startTime = null
let _startTime: string | null = null
let _onlyTestId = null
let _onlySuiteId = null
@@ -1209,7 +1223,7 @@ export default {
const r = runnable
const isHook = r.type === 'hook'
const isTest = r.type === 'test'
const test = getTest() || getTestFromHook(runnable, getTestById)
const test = getTest() || getTestFromHook(runnable)
const hookName = isHook && getHookName(r)
const isBeforeEachHook = isHook && !!hookName.match(/before each/)
const isAfterEachHook = isHook && !!hookName.match(/after each/)
@@ -1382,11 +1396,11 @@ export default {
// runtime of a runnables fn execution duration
// and also the run of the runnable:after:run:async event
let lifecycleStart
let wallClockEnd = null
let fnDurationStart = null
let fnDurationEnd = null
let afterFnDurationStart = null
let afterFnDurationEnd = null
let wallClockEnd: Date | null = null
let fnDurationStart: Date | null = null
let fnDurationEnd: Date | null = null
let afterFnDurationStart: Date | null = null
let afterFnDurationEnd: Date | null = null
// when this is a hook, capture the real start
// date so we can calculate our test's duration
@@ -1432,12 +1446,12 @@ export default {
// reset runnable duration to include lifecycle
// and afterFn timings purely for the mocha runner.
// this is what it 'feels' like to the user
runnable.duration = wallClockEnd - wallClockStartedAt
runnable.duration = duration(wallClockEnd, wallClockStartedAt)
setTestTimingsForHook(test, hookName, {
hookId: runnable.hookId,
fnDuration: fnDurationEnd - fnDurationStart,
afterFnDuration: afterFnDurationEnd - afterFnDurationStart,
fnDuration: duration(fnDurationEnd!, fnDurationStart!),
afterFnDuration: duration(afterFnDurationEnd, afterFnDurationStart!),
})
break
@@ -1446,13 +1460,13 @@ export default {
// if we are currently on a test then
// recalculate its duration to be based
// against that (purely for the mocha reporter)
test.duration = wallClockEnd - test.wallClockStartedAt
test.duration = duration(wallClockEnd, test.wallClockStartedAt)
// but still preserve its actual function
// body duration for timings
setTestTimings(test, 'test', {
fnDuration: fnDurationEnd - fnDurationStart,
afterFnDuration: afterFnDurationEnd - afterFnDurationStart,
fnDuration: duration(fnDurationEnd!, fnDurationStart!),
afterFnDuration: duration(afterFnDurationEnd!, afterFnDurationStart!),
})
break
@@ -1559,7 +1573,7 @@ export default {
if (lifecycleStart) {
// capture how long the lifecycle took as part
// of the overall wallClockDuration of our test
setTestTimings(test, 'lifecycle', new Date() - lifecycleStart)
setTestTimings(test, 'lifecycle', duration(new Date(), lifecycleStart))
}
// capture the moment we're about to invoke
@@ -1594,7 +1608,7 @@ export default {
},
setNumLogs (num) {
return $Log.setCounter(num)
return LogUtils.setCounter(num)
},
getEmissions () {
@@ -1649,13 +1663,13 @@ export default {
_runner.removeAllListeners()
},
getDisplayPropsForLog: $Log.getDisplayProps,
getDisplayPropsForLog: LogUtils.getDisplayProps,
getConsolePropsForLogById (logId) {
const attrs = _logsById[logId]
if (attrs) {
return $Log.getConsoleProps(attrs)
return LogUtils.getConsoleProps(attrs)
}
},
@@ -1663,11 +1677,13 @@ export default {
const attrs = _logsById[logId]
if (attrs) {
return $Log.getSnapshotProps(attrs)
return LogUtils.getSnapshotProps(attrs)
}
return
},
resumeAtTest (id, emissions = {}) {
resumeAtTest (id, emissions: any = {}) {
_resumedAtTestIndex = getTestIndexFromId(id)
_emissions = emissions
@@ -1700,7 +1716,7 @@ export default {
// now, so lets store that
attrs._hasBeenCleanedUp = true
return $Log.reduceMemory(attrs)
return LogUtils.reduceMemory(attrs)
})
})
@@ -1776,7 +1792,7 @@ const mixinLogs = (test) => {
const logs = test[type]
if (logs) {
test[type] = _.map(logs, $Log.toSerializedJSON)
test[type] = _.map(logs, LogUtils.toSerializedJSON)
}
})
}
+4 -6
View File
@@ -1,5 +1,3 @@
// @ts-nocheck
import _ from 'lodash'
import $utils from './utils'
@@ -13,8 +11,8 @@ const _reset = () => {
screenshotOnRunFailure: true,
blackout: [],
overwrite: false,
onBeforeScreenshot () {},
onAfterScreenshot () {},
onBeforeScreenshot ($el) {},
onAfterScreenshot ($el, results) {},
}
}
@@ -111,8 +109,8 @@ const validateAndSetCallback = (props, values, cmd, log, option) => {
values[option] = value
}
const validate = (props, cmd, log) => {
const values = {}
const validate = (props, cmd, log?) => {
const values: Record<string, any> = {}
if (!_.isPlainObject(props)) {
$errUtils.throwErrByPath('screenshot.invalid_arg', {
+4 -6
View File
@@ -1,5 +1,3 @@
// @ts-nocheck
import _ from 'lodash'
import Bluebird from 'bluebird'
@@ -14,7 +12,7 @@ const fetchScript = (scriptWindow, script) => {
}
const extractSourceMap = ([script, contents]) => {
script.fullyQualifiedUrl = `${window.top.location.origin}${script.relativeUrl}`.replace(/ /g, '%20')
script.fullyQualifiedUrl = `${window.top!.location.origin}${script.relativeUrl}`.replace(/ /g, '%20')
const sourceMap = $sourceMapUtils.extractSourceMap(script, contents)
@@ -22,7 +20,7 @@ const extractSourceMap = ([script, contents]) => {
.return([script, contents])
}
const evalScripts = (specWindow, scripts = []) => {
const evalScripts = (specWindow, scripts: any = []) => {
_.each(scripts, ([script, contents]) => {
specWindow.eval(`${contents}\n//# sourceURL=${script.fullyQualifiedUrl}`)
})
@@ -32,7 +30,7 @@ const evalScripts = (specWindow, scripts = []) => {
const runScriptsFromUrls = (specWindow, scripts) => {
return Bluebird
.map(scripts, (script) => fetchScript(specWindow, script))
.map<any, any>(scripts, (script) => fetchScript(specWindow, script))
.map(extractSourceMap)
.then((scripts) => evalScripts(specWindow, scripts))
}
@@ -46,7 +44,7 @@ export default {
// NOTE: since in evalScripts, scripts are evaluated in order,
// we chose to respect this constraint here too.
// indeed _.each goes through the array in order
return Bluebird.each(scripts, (script) => script())
return Bluebird.each(scripts, (script: any) => script())
}
return runScriptsFromUrls(specWindow, scripts)
@@ -1,4 +1,3 @@
// @ts-nocheck
import _ from 'lodash'
import { SourceMapConsumer } from 'source-map'
import Promise from 'bluebird'
@@ -16,6 +15,7 @@ let sourceMapConsumers = {}
const initializeSourceMapConsumer = (file, sourceMap) => {
if (!sourceMap) return Promise.resolve(null)
// @ts-ignore
SourceMapConsumer.initialize({
'lib/mappings.wasm': mappingsWasm,
})
@@ -32,7 +32,7 @@ const extractSourceMap = (file, fileContents) => {
if (!sourceMapMatch) return null
const url = _.last(sourceMapMatch)
const url = _.last(sourceMapMatch) as any
const dataUrlMatch = url.match(regexDataUrl)
if (!dataUrlMatch) return null
+27 -8
View File
@@ -1,5 +1,4 @@
// See: ./errorScenarios.md for details about error messages and stack traces
// @ts-nocheck
import _ from 'lodash'
import path from 'path'
import errorStackParser from 'error-stack-parser'
@@ -52,7 +51,7 @@ const stackWithLinesRemoved = (stack, cb) => {
const stackWithLinesDroppedFromMarker = (stack, marker, includeLast = false) => {
return stackWithLinesRemoved(stack, (lines) => {
// drop lines above the marker
const withAboveMarkerRemoved = _.dropWhile(lines, (line) => {
const withAboveMarkerRemoved = _.dropWhile(lines, (line: any) => {
return !_.includes(line, marker)
})
@@ -96,6 +95,8 @@ const stackWithUserInvocationStackSpliced = (err, userInvocationStack): StackAnd
}
}
type InvocationDetails = LineDetail | {}
const getInvocationDetails = (specWindow, config) => {
if (specWindow.Error) {
let stack = (new specWindow.Error()).stack
@@ -110,12 +111,14 @@ const getInvocationDetails = (specWindow, config) => {
stack = stackWithLinesDroppedFromMarker(stack, '__cypress/tests', true)
}
const details = getSourceDetailsForFirstLine(stack, config('projectRoot')) || {}
const details: InvocationDetails = getSourceDetailsForFirstLine(stack, config('projectRoot')) || {};
details.stack = stack
(details as any).stack = stack
return details
return details as (InvocationDetails & { stack: any })
}
return
}
const getLanguageFromExtension = (filePath) => {
@@ -240,7 +243,7 @@ const parseLine = (line) => {
if (!isStackLine) return
const parsed = errorStackParser.parse({ stack: line })[0]
const parsed = errorStackParser.parse({ stack: line } as any)[0]
if (!parsed) return
@@ -270,7 +273,23 @@ const stripCustomProtocol = (filePath) => {
return filePath.replace(customProtocolRegex, '')
}
const getSourceDetailsForLine = (projectRoot, line) => {
type LineDetail =
{
message: any
whitespace: any
} |
{
function: any
fileUrl: any
originalFile: any
relativeFile: any
absoluteFile: any
line: any
column: number
whitespace: any
}
const getSourceDetailsForLine = (projectRoot, line): LineDetail => {
const whitespace = getWhitespace(line)
const generatedDetails = parseLine(line)
@@ -325,7 +344,7 @@ const reconstructStack = (parsedStack) => {
}).join('\n')
}
const getSourceStack = (stack, projectRoot) => {
const getSourceStack = (stack, projectRoot?) => {
if (!_.isString(stack)) return {}
const getSourceDetailsWithStackUtil = _.partial(getSourceDetailsForLine, projectRoot)
+5 -6
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import _ from 'lodash'
import capitalize from 'underscore.string/capitalize'
import methods from 'methods'
@@ -46,7 +45,7 @@ const USER_FRIENDLY_TYPE_DETECTORS = _.map([
[_.stubTrue, 'unknown'],
], ([fn, type]) => {
return [fn, _.constant(type)]
})
}) as [(val: any) => boolean, (val: Function) => Function][]
export default {
warning (msg) {
@@ -78,7 +77,7 @@ export default {
const item = [].concat(val)[0]
if ($jquery.isJquery(item)) {
return item.first()
return (item as JQuery<any>).first()
}
return item
@@ -154,7 +153,7 @@ export default {
memo.push(`${`${key}`.toLowerCase()}: ${this.stringifyActual(value)}`)
return memo
}, [])
}, [] as string[])
return `{${str.join(', ')}}`
},
@@ -185,7 +184,7 @@ export default {
if (_.isObject(value)) {
// Cannot use $dom.isJquery here because it causes infinite recursion.
if (value instanceof $) {
return `jQuery{${value.length}}`
return `jQuery{${(value as JQueryStatic).length}}`
}
const len = _.keys(value).length
@@ -396,7 +395,7 @@ export default {
*/
encodeBase64Unicode (str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode(`0x${p1}`)
return String.fromCharCode(Number(`0x${p1}`))
}))
},
@@ -1,5 +1,3 @@
// @ts-nocheck
import $errUtils from './error_utils'
const isCypressHeaderRe = /^X-Cypress-/i
@@ -15,6 +13,16 @@ const parseJSON = (text) => {
// maybe rename this to XMLHttpRequest ?
// so it shows up correctly as an instance in the console
class XMLHttpRequest {
xhr: any
id: any
url: any
method: any
status: any
statusMessage: any
request: any
response: any
duration: any
constructor (xhr) {
this.xhr = xhr
this.id = this.xhr.id
+1 -1
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import $ from 'jquery'
import _ from 'lodash'
@@ -8,6 +7,7 @@ const wrap = (obj) => {
}
const query = (selector, context) => {
// @ts-ignore
return new $.fn.init(selector, context)
}
-2
View File
@@ -1,5 +1,3 @@
// @ts-nocheck
import $jquery from './jquery'
import $document from './document'
+3 -3
View File
@@ -1,5 +1,3 @@
// @ts-nocheck
import _ from 'lodash'
// IE doesn't support Array.from or Map.prototype.keys
@@ -8,7 +6,7 @@ const getMapKeys = (map) => {
return Array.from(map.keys())
}
const keys = []
const keys: any[] = []
map.forEach((key) => {
keys.push(key)
@@ -18,6 +16,8 @@ const getMapKeys = (map) => {
}
class LimitedMap extends Map {
private _limit: number
constructor (limit = 100) {
super()
+1
View File
@@ -56,6 +56,7 @@ declare namespace Cypress {
mocha: $Mocha
configure: (config: Cypress.ObjectLike) => void
isMultiDomain: boolean
originalConfig: Cypress.ObjectLike
}
interface CypressUtils {
+1 -1
View File
@@ -21,7 +21,7 @@ export const splitStack = (stack: string) => {
}, [[], []] as MessageLines)
}
export const unsplitStack = (messageLines: string, stackLines: string[]) => {
export const unsplitStack = (messageLines: string | string[], stackLines: string[]) => {
return _.castArray(messageLines).concat(stackLines).join('\n')
}
+1 -1
View File
@@ -28,7 +28,7 @@
"devDependencies": {
"chai": "3.5.0",
"cross-env": "6.0.3",
"cypress-example-kitchensink": "1.15.2",
"cypress-example-kitchensink": "1.15.3",
"gulp": "4.0.2",
"gulp-clean": "0.4.0",
"gulp-gh-pages": "0.6.0-6",
+1 -1
View File
@@ -17,7 +17,7 @@
"execa": "4.0.0",
"fs-extra": "9.1.0",
"lodash": "^4.17.21",
"plist": "3.0.1",
"plist": "3.0.4",
"semver": "7.3.5"
},
"devDependencies": {
-4
View File
@@ -251,10 +251,6 @@ export function getBodyEncoding (req: CyHttpMessages.IncomingRequest): BodyEncod
if (contentType.includes('charset=utf-8') || contentType.includes('charset="utf-8"')) {
return 'utf8'
}
if (contentType.includes('multipart/form-data')) {
return 'binary'
}
}
// with fallback to inspecting the buffer using
@@ -69,19 +69,5 @@ describe('net-stubbing util', () => {
expect(getBodyEncoding(req), 'image').to.equal('binary')
})
it('returns binary for form-data bodies', () => {
const formDataRequest = {
body: Buffer.from('hello world'),
headers: {
'content-type': 'multipart/form-data',
},
method: 'POST',
url: 'somewhere',
httpVersion: '1.1',
}
expect(getBodyEncoding(formDataRequest)).to.equal('binary')
})
})
})
+1 -1
View File
@@ -11,7 +11,7 @@ export function byPortAndAddress (port: number, address: net.Address) {
// https://nodejs.org/api/net.html#net_net_connect_port_host_connectlistener
return new Bluebird<net.Address>((resolve, reject) => {
const onConnect = () => {
client.end()
client.destroy()
resolve(address)
}
@@ -0,0 +1,27 @@
import { connect } from '../..'
import { expect } from 'chai'
import sinon from 'sinon'
import net from 'net'
describe('lib/connect', () => {
context('.byPortAndAddress', () => {
it('destroy connection immediately onConnect', () => {
const socket = new net.Socket()
const destroy = sinon.spy(socket, 'destroy')
sinon.stub(net, 'connect').callsFake((port, address, onConnect) => {
process.nextTick(() => {
onConnect()
})
return socket
})
return connect.byPortAndAddress(1234, { address: '127.0.0.1' })
.then((address) => {
expect(address).to.deep.eq({ address: '127.0.0.1' })
expect(destroy).to.be.called
})
})
})
})
+2 -2
View File
@@ -19,7 +19,7 @@ import RequestMiddleware from './request-middleware'
import ResponseMiddleware from './response-middleware'
import { DeferredSourceMapCache } from '@packages/rewriter'
const debugRequests = Debug('cypress-verbose:proxy:http')
export const debugVerbose = Debug('cypress-verbose:proxy:http')
export enum HttpStages {
IncomingRequest,
@@ -238,7 +238,7 @@ export class Http {
socket: this.socket,
serverBus: this.serverBus,
debug: (formatter, ...args) => {
debugRequests(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args)
debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args)
},
deferSourceMapRewrite: (opts) => {
this.deferredSourceMapCache.defer({
+13 -2
View File
@@ -280,8 +280,8 @@ const SetInjectionLevel: ResponseMiddleware = function () {
return 'fullMultiDomain'
}
if (!isHTML || !isReqMatchOriginPolicy && !isAUTFrame) {
debug('- no injection (not html)')
if (!isHTML || (!isReqMatchOriginPolicy && !isAUTFrame)) {
this.debug('- no injection (not html)')
return false
}
@@ -311,6 +311,17 @@ const SetInjectionLevel: ResponseMiddleware = function () {
this.res.wantsInjection = getInjectionLevel()
}
if (this.res.wantsInjection) {
// Chrome plans to make document.domain immutable in Chrome 106, with the default value
// of the Origin-Agent-Cluster header becoming 'true'. We explicitly disable this header
// so that we can continue to support tests that visit multiple subdomains in a single spec.
// https://github.com/cypress-io/cypress/issues/20147
//
// We set the header here only for proxied requests that have scripts injected that set the domain.
// Other proxied requests are ignored.
this.res.setHeader('Origin-Agent-Cluster', '?0')
}
this.res.wantsSecurityRemoved = this.config.modifyObstructiveCode && isReqMatchOriginPolicy && (
(this.res.wantsInjection === 'full')
|| resContentTypeIsJavaScript(this.incomingRes)
@@ -1,5 +1,6 @@
import _ from 'lodash'
import ResponseMiddleware from '../../../lib/http/response-middleware'
import { debugVerbose } from '../../../lib/http'
import { expect } from 'chai'
import sinon from 'sinon'
import {
@@ -415,6 +416,31 @@ describe('http/response-middleware', function () {
})
})
it('does not set Origin-Agent-Cluster header to false when injection is not expected', function () {
prepareContext({})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.setHeader).not.to.be.calledWith('Origin-Agent-Cluster', '?0')
})
})
it('sets Origin-Agent-Cluster header to false when injection is expected', function () {
prepareContext({
incomingRes: {
headers: {
// simplest way to make injection expected
'x-cypress-file-server-error': true,
},
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.setHeader).to.be.calledWith('Origin-Agent-Cluster', '?0')
})
})
function prepareContext (props) {
ctx = {
incomingRes: {
@@ -423,11 +449,15 @@ describe('http/response-middleware', function () {
},
res: {
headers: {},
setHeader: sinon.stub(),
...props.res,
},
req: {
proxiedUrl: 'http://127.0.0.1:3501/multi-domain.html',
headers: {},
cookies: {
'__cypress.initial': true,
},
...props.req,
},
getRemoteState () {
@@ -438,7 +468,9 @@ describe('http/response-middleware', function () {
},
}
},
debug () {},
debug: (formatter, ...args) => {
debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args)
},
onError (error) {
throw error
},
@@ -447,3 +479,33 @@ describe('http/response-middleware', function () {
}
})
})
// beforeEach(function () {
// ctx = {
// req: {
// proxiedUrl: 'http://proxy.com',
// cookies: {
// '__cypress.initial': true,
// },
// headers: {
// accept: ['text/html', 'application/xhtml+xml'],
// },
// },
// res: {
// setHeader: sinon.stub(),
// },
// getRemoteState: () => {
// return {
// strategy: 'http',
// props: {
// domain: 'proxy',
// port: '80',
// tld: 'com',
// },
// }
// },
// getRenderedHTMLOrigins: () => {
// return {}
// },
// }
// })
@@ -135,5 +135,15 @@ describe('shortcuts', function () {
cy.get('button.stop').trigger('mouseover')
cy.get('.cy-tooltip').should('have.text', 'Stop Running S')
})
it('does not run shortcut if modifier keys are pressed', () => {
['{ctrl+f}', '{alt+f}', '{shift+f}', '{meta+f}'].forEach((text) => {
cy.get('body').type(text)
})
cy.then(() => {
expect(runner.emit).not.to.have.been.calledWith('focus:tests')
})
})
})
})
+5 -2
View File
@@ -1,5 +1,5 @@
// @ts-ignore
import dom from '@packages/driver/src/dom'
import $dom from '@packages/driver/src/dom'
import events from './events'
import appState from './app-state'
import { action } from 'mobx'
@@ -16,7 +16,10 @@ class Shortcuts {
_handleKeyDownEvent (event: KeyboardEvent) {
// if typing into an input, textarea, etc, don't trigger any shortcuts
// @ts-ignore
if (dom.isTextLike(event.target)) return
const isTextLike = $dom.isTextLike(event.target)
const isAnyModifierKeyPressed = event.altKey || event.ctrlKey || event.shiftKey || event.metaKey
if (isAnyModifierKeyPressed || isTextLike) return
switch (event.key) {
case 'r': !appState.studioActive && events.emit('restart')
@@ -31,6 +31,12 @@ export const iframesController = {
extraOptions,
})
// Chrome plans to make document.domain immutable in Chrome 106, with the default value
// of the Origin-Agent-Cluster header becoming 'true'. We explicitly disable this header
// so that we can continue to support tests that visit multiple subdomains in a single spec.
// https://github.com/cypress-io/cypress/issues/20147
res.setHeader('Origin-Agent-Cluster', '?0')
files.handleIframe(req, res, config, getRemoteState, extraOptions)
},
@@ -33,6 +33,12 @@ export const serveRunner = (runnerPkg: RunnerPkg, config: Cfg, res: Response) =>
const runnerPath = process.env.CYPRESS_INTERNAL_RUNNER_PATH || getPathToIndex(runnerPkg)
// Chrome plans to make document.domain immutable in Chrome 106, with the default value
// of the Origin-Agent-Cluster header becoming 'true'. We explicitly disable this header
// so that we can continue to support tests that visit multiple subdomains in a single spec.
// https://github.com/cypress-io/cypress/issues/20147
res.setHeader('Origin-Agent-Cluster', '?0')
return res.render(runnerPath, {
base64Config,
projectName: config.projectName,
+10 -1
View File
@@ -4,6 +4,8 @@ const debug = require('debug')('cypress:server:fixture')
const coffee = require('coffeescript')
const Promise = require('bluebird')
const jsonlint = require('jsonlint')
const stripAnsi = require('strip-ansi')
const errors = require('./errors')
const { fs } = require('./util/fs')
const glob = require('./util/glob')
@@ -60,7 +62,14 @@ module.exports = {
if (matches.length === 0) {
const relativePath = path.relative('.', p)
errors.throw('FIXTURE_NOT_FOUND', relativePath, extensions)
// TODO: there's no reason this error should be in
// the @packages/error list, it should be written in
// the driver since this error can only occur within
// driver commands and not outside of the test runner
const err = errors.get('FIXTURE_NOT_FOUND', relativePath, extensions)
err.message = stripAnsi(err.message)
throw err
}
debug('fixture matches found, using the first', matches)
+2 -1
View File
@@ -30,6 +30,7 @@ import * as settings from './util/settings'
import specsUtil from './util/specs'
import system from './util/system'
import Watchers from './watchers'
import stripAnsi from 'strip-ansi'
import type { LaunchArgs } from './open_project'
@@ -704,7 +705,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
return {
...browser,
warning: browser.warning || errors.getMsgByType('CHROME_WEB_SECURITY_NOT_SUPPORTED', browser.name),
warning: browser.warning || stripAnsi(errors.getMsgByType('CHROME_WEB_SECURITY_NOT_SUPPORTED', browser.name)),
}
})
}
+2 -2
View File
@@ -50,7 +50,7 @@
"data-uri-to-buffer": "2.0.1",
"dayjs": "^1.9.3",
"debug": "^4.3.2",
"dependency-tree": "8.1.0",
"dependency-tree": "8.1.2",
"duplexify": "4.1.1",
"electron-context-menu": "3.1.1",
"errorhandler": "1.5.1",
@@ -116,7 +116,7 @@
"tsconfig-paths": "3.10.1",
"tslib": "2.3.0",
"underscore.string": "3.3.5",
"url-parse": "1.5.6",
"url-parse": "1.5.9",
"uuid": "8.3.2",
"which": "2.0.2",
"widest-line": "3.1.0",
+32
View File
@@ -0,0 +1,32 @@
require('../spec_helper')
const { iframesController } = require(`${root}/lib/controllers/iframes`)
const files = require(`${root}/lib/controllers/files`)
describe('controllers/iframes', () => {
describe('e2e', () => {
it('sets Origin-Agent-Cluster response header to false', () => {
sinon.stub(files, 'handleIframe')
const mockReq = {}
const mockRes = {
setHeader: sinon.stub(),
}
const controllerOptions = {
getSpec: sinon.stub(),
getRemoteState: sinon.stub(),
config: {},
}
iframesController.e2e(controllerOptions, mockReq, mockRes)
expect(mockRes.setHeader).to.have.been.calledWith('Origin-Agent-Cluster', '?0')
expect(files.handleIframe).to.have.been.calledWith(
mockReq, mockRes, controllerOptions.config, controllerOptions.getRemoteState, sinon.match({
specFilter: undefined, specType: 'integration',
}),
)
})
})
})
+1 -3
View File
@@ -1,5 +1,3 @@
const { theme } = require('@packages/errors/src/errTemplate')
require('../spec_helper')
const mockedEnv = require('mocked-env')
@@ -248,7 +246,7 @@ describe('lib/project-base', () => {
family: 'some-other-family',
name: 'some-other-name',
warning: `\
Your project has set the configuration option: ${theme.yellow('chromeWebSecurity')} to ${theme.blue('false')}
Your project has set the configuration option: chromeWebSecurity to false
This option will not have an effect in Some-other-name. Tests that rely on web security being disabled will not run as expected.\
`,
+19
View File
@@ -0,0 +1,19 @@
require('../spec_helper')
const { serveRunner } = require(`${root}/lib/controllers/runner`)
describe('controllers/runner', () => {
describe('serveRunner', () => {
it('sets Origin-Agent-Cluster response header to false', () => {
const mockRes = {
setHeader: sinon.stub(),
render: sinon.stub(),
}
serveRunner('runner', {}, mockRes)
expect(mockRes.setHeader).to.have.been.calledWith('Origin-Agent-Cluster', '?0')
expect(mockRes.render).to.have.been.called
})
})
})
+1
View File
@@ -114,6 +114,7 @@ export async function buildCypressApp (options: BuildCypressAppOpts) {
meta.distDir('**', 'esprima', 'test'),
meta.distDir('**', 'bmp-js', 'test'),
meta.distDir('**', 'exif-parser', 'test'),
meta.distDir('**', 'app-module-path', 'test'),
], { force: true })
console.log('Deleted excess directories')
-268
View File
@@ -1,268 +0,0 @@
const _ = require('lodash')
const Promise = require('bluebird')
const bumpercar = require('@cypress/bumpercar')
const path = require('path')
const la = require('lazy-ass')
const check = require('check-more-types')
const { configFromEnvOrJsonFile, filenameToShellVariable } = require('@cypress/env-or-json-file')
const makeEmptyGithubCommit = require('make-empty-github-commit')
const parse = require('parse-github-repo-url')
const { setCommitStatus } = require('@cypress/github-commit-status-check')
let car = null
// all the projects to trigger / run / change environment variables for
const _PROVIDERS = {
circle: {
main: 'cypress-io/cypress',
linux: [
'cypress-io/cypress-test-module-api',
],
},
}
const remapProjects = function (projectsByProvider) {
const list = []
_.mapValues(projectsByProvider, (provider, name) => {
const remapPlatform = (platform, repos) => {
return repos.forEach((repo) => {
return list.push({
repo,
provider: name,
platform,
})
})
}
if (provider.win32) {
remapPlatform('win32', provider.win32)
}
if (provider.linux) {
remapPlatform('linux', provider.linux)
}
if (provider.darwin) {
return remapPlatform('darwin', provider.darwin)
}
})
return list
}
// make flat list of objects
// {repo, provider, platform}
const PROJECTS = remapProjects(_PROVIDERS)
const getCiConfig = function () {
const key = path.join('scripts', 'support', 'ci.json')
const config = configFromEnvOrJsonFile(key)
if (!config) {
console.error('⛔️ Cannot find CI credentials')
console.error('Using @cypress/env-or-json-file module')
console.error('and filename', key)
console.error('which is environment variable', filenameToShellVariable(key))
throw new Error('CI config not found')
}
return config
}
const awaitEachProjectAndProvider = function (projects, fn, filter = (val) => val) {
const creds = getCiConfig()
// configure a new Bumpercar
const providers = {}
if (check.unemptyString(creds.githubToken)) {
providers.travis = {
githubToken: process.env.GH_TOKEN,
}
}
if (check.unemptyString(creds.circleToken)) {
providers.circle = {
circleToken: creds.circleToken,
}
}
const providerNames = Object.keys(providers)
console.log('configured providers', providerNames)
la(check.not.empty(providerNames), 'empty list of providers')
car = bumpercar.create({ providers })
const filteredProjects = projects.filter(filter)
if (check.empty(filteredProjects)) {
console.log('⚠️ zero filtered projects left after filtering')
}
console.log('filtered projects:')
console.table(filteredProjects)
return Promise.mapSeries(filteredProjects, (project) => {
return fn(project.repo, project.provider, creds)
})
}
// do not trigger all projects if there is specific provider
const getFilterByProvider = function (providerName, platformName) {
return (val) => {
if (providerName && val.provider !== providerName) {
return false
}
if (platformName && val.platform !== platformName) {
return false
}
return val
}
}
module.exports = {
_PROVIDERS,
remapProjects,
getFilterByProvider,
// in each project, set a couple of environment variables
version (nameOrUrl, binaryVersionOrUrl, platform, providerName) {
console.log('All possible projects:')
console.table(PROJECTS)
la(check.unemptyString(nameOrUrl),
'missing cypress name or url to set', nameOrUrl)
if (check.semver(nameOrUrl)) {
console.log('for version', nameOrUrl)
nameOrUrl = `cypress@${nameOrUrl}`
console.log('full NPM install name is', nameOrUrl)
}
la(check.unemptyString(binaryVersionOrUrl),
'missing binary version or url', binaryVersionOrUrl)
const result = {
versionName: nameOrUrl,
binary: binaryVersionOrUrl,
}
const projectFilter = getFilterByProvider(providerName)
const updateProject = function (project, provider) {
console.log('setting environment variables in', project)
return car.updateProjectEnv(project, provider, {
CYPRESS_NPM_PACKAGE_NAME: nameOrUrl,
CYPRESS_INSTALL_BINARY: binaryVersionOrUrl,
})
}
return awaitEachProjectAndProvider(PROJECTS, updateProject, projectFilter)
.then(() => result)
},
// triggers test projects on multiple CIs
// the test projects will exercise the new version of
// the Cypress test runner we just built
runTestProjects (getStatusAndMessage, providerName, version, platform) {
const projectFilter = getFilterByProvider(providerName, platform)
const makeCommit = function (project, provider, creds) {
// make empty commit to trigger CIs
// project is owner/repo string like cypress-io/cypress-test-tiny
console.log('making commit to project', project)
// print if we have a few github variables present
console.log('do we have GH_APP_ID?', Boolean(process.env.GH_APP_ID))
console.log('do we have GH_INSTALLATION_ID?', Boolean(process.env.GH_INSTALLATION_ID))
console.log('do we have GH_PRIVATE_KEY?', Boolean(process.env.GH_PRIVATE_KEY))
console.log('do we have GH_TOKEN?', Boolean(process.env.GH_TOKEN))
const parsedRepo = parse(project)
const owner = parsedRepo[0]
const repo = parsedRepo[1]
let { status, message } = getStatusAndMessage(repo)
if (!message) {
message =
`\
Testing new Cypress version ${version}
\
`
if (process.env.CIRCLE_BUILD_URL) {
message += '\n'
message += `Circle CI build url ${process.env.CIRCLE_BUILD_URL}`
}
}
const defaultOptions = {
owner,
repo,
message,
token: process.env.GH_TOKEN,
}
const createGithubCommitStatusCheck = function ({ sha }) {
if (!status) {
return
}
// status is {owner, repo, sha} and maybe a few other properties
const isStatus = check.schema({
owner: check.unemptyString,
repo: check.unemptyString,
sha: check.commitId,
context: check.unemptyString,
platform: check.unemptyString,
arch: check.unemptyString,
})
if (!isStatus(status)) {
console.error('Invalid status object %o', status)
}
const targetUrl = `https://github.com/${owner}/${repo}/commit/${sha}`
const commitStatusOptions = {
targetUrl,
owner: status.owner,
repo: status.repo,
sha: status.sha,
context: status.context,
state: 'pending',
description: `${owner}/${repo}`,
}
console.log(
'creating commit status check',
commitStatusOptions.description,
commitStatusOptions.context,
)
return setCommitStatus(commitStatusOptions)
}
if (!version) {
return makeEmptyGithubCommit(defaultOptions).then(createGithubCommitStatusCheck)
}
// first try to commit to branch for next upcoming version
return makeEmptyGithubCommit({ ...defaultOptions, branch: version })
.catch(() => {
// maybe there is no branch for next version
// try default branch
return makeEmptyGithubCommit(defaultOptions)
}).then(createGithubCommitStatusCheck)
}
return awaitEachProjectAndProvider(PROJECTS, makeCommit, projectFilter)
},
}
-18
View File
@@ -16,7 +16,6 @@ const rp = require('@cypress/request-promise')
const zip = require('./zip')
const ask = require('./ask')
const bump = require('./bump')
const meta = require('./meta')
const build = require('./build')
const upload = require('./upload')
@@ -104,23 +103,6 @@ const deploy = {
return opts
},
bump () {
return ask.whichBumpTask()
.then((task) => {
switch (task) {
case 'run':
return bump.runTestProjects()
case 'version':
return ask.whichVersion(meta.distDir(''))
.then((v) => {
return bump.version(v)
})
default:
throw new Error('unknown task')
}
})
},
release () {
// read off the argv
const options = this.parseOptions(process.argv)
+11 -4
View File
@@ -113,6 +113,8 @@ export const prompts = {
export const moveBinaries = async (args = []) => {
debug('moveBinaries with args %o', args)
const options = arg({
'--s3bucket': String,
'--s3folder': String,
'--commit': String,
'--version': String,
// optional, if passed, only the binary for that platform will be moved
@@ -136,8 +138,13 @@ export const moveBinaries = async (args = []) => {
version: options['--version'],
}
const aws = uploadUtils.getS3Credentials()
const s3 = s3helpers.makeS3(aws)
const credentials = await uploadUtils.getS3Credentials()
const aws = {
'bucket': options['--s3bucket'] || uploadUtils.S3Configuration.bucket,
'folder': options['--s3folder'] || uploadUtils.S3Configuration.releaseFolder,
}
const s3 = s3helpers.makeS3(credentials)
// found s3 paths with last build for same commit for all platforms
const lastBuilds: Desktop[] = []
@@ -164,12 +171,12 @@ export const moveBinaries = async (args = []) => {
platformArch,
})
console.log('finding binary for %s in %s', platformArch, uploadDir)
console.log('finding binary in %s for %s in %s', aws.bucket, platformArch, uploadDir)
const list: string[] = await s3helpers.listS3Objects(uploadDir, aws.bucket, s3)
if (debug.enabled) {
console.log('all found subfolders')
console.log('all found sub-folders')
console.log(list.join('\n'))
}
+11 -5
View File
@@ -13,12 +13,18 @@ export const hasOnlyStringValues = (o) => {
*/
export const s3helpers = {
makeS3 (aws) {
la(is.unemptyString(aws.key), 'missing aws key')
la(is.unemptyString(aws.secret), 'missing aws secret')
la(is.unemptyString(aws.accessKeyId), 'missing aws accessKeyId')
la(is.unemptyString(aws.secretAccessKey), 'missing aws secretAccessKey')
if (!process.env.CIRCLECI) {
// sso is not required for CirceCI
la(is.unemptyString(aws.sessionToken), 'missing aws sessionToken')
}
return new S3({
accessKeyId: aws.key,
secretAccessKey: aws.secret,
accessKeyId: aws.accessKeyId,
secretAccessKey: aws.secretAccessKey,
sessionToken: aws.sessionToken,
})
},
@@ -40,7 +46,7 @@ export const s3helpers = {
debug('s3 data for %s', zipFile)
debug(data)
resolve()
resolve(null)
})
})
},
+7 -16
View File
@@ -9,16 +9,7 @@ const upload = require('./upload')
const uploadUtils = require('./util/upload')
const { s3helpers } = require('./s3-api')
const uploadTypes = {
binary: {
uploadFolder: 'binary',
uploadFileName: 'cypress.zip',
},
'npm-package': {
uploadFolder: 'npm',
uploadFileName: 'cypress.tgz',
},
}
const uploadTypes = uploadUtils.S3Configuration.betaUploadTypes
const getCDN = function (uploadPath) {
return [uploadUtils.getUploadUrl(), uploadPath].join('/')
@@ -32,16 +23,16 @@ const getUploadDirForPlatform = function (options) {
// the artifact will be uploaded for every platform and uploaded into under a unique folder
// https://cdn.cypress.io/beta/(binary|npm)/<version>/<platform>/<some unique version info>/cypress.zip
// For binary:
// beta/binary/9.4.2/win32-x64/circle-develop-219138ca4e952edc4af831f2ae16ce659ebdb50b/cypress.zip
// beta/binary/9.4.2/win32-x64/develop-219138ca4e952edc4af831f2ae16ce659ebdb50b/cypress.zip
// For NPM package:
// beta/npm/9.4.2/circle-develop-219138ca4e952edc4af831f2ae16ce659ebdb50b/cypress.tgz
// beta/npm/9.4.2/develop-219138ca4e952edc4af831f2ae16ce659ebdb50b/cypress.tgz
const getUploadPath = function (options) {
const { hash, uploadFileName } = options
return [getUploadDirForPlatform(options), hash, uploadFileName].join('/')
}
const setChecksum = (filename, key) => {
const setChecksum = async (filename, key) => {
console.log('setting checksum for file %s', filename)
console.log('on s3 object %s', key)
@@ -56,7 +47,7 @@ const setChecksum = (filename, key) => {
console.log('SHA256 checksum %s', checksum)
console.log('size', size)
const aws = uploadUtils.getS3Credentials()
const aws = await uploadUtils.getS3Credentials()
const s3 = s3helpers.makeS3(aws)
// S3 object metadata can only have string values
const metadata = {
@@ -66,7 +57,7 @@ const setChecksum = (filename, key) => {
// by default s3.copyObject does not preserve ACL when copying
// thus we need to reset it for our public files
return s3helpers.setUserMetadata(aws.bucket, key, metadata,
return s3helpers.setUserMetadata(uploadUtils.S3Configuration.bucket, key, metadata,
'application/zip', 'public-read', s3)
}
@@ -128,7 +119,7 @@ const uploadArtifactToS3 = function (args = []) {
.then(uploadUtils.saveUrl(`${options.type}-url.json`))
.catch((e) => {
console.error('There was an issue uploading the artifact.')
console.error(e)
throw e
})
}
+46 -49
View File
@@ -15,17 +15,13 @@ fs = Promise.promisifyAll(fs)
// TODO: refactor this
// system expects desktop application to be inside a file
// with this name
const zipName = 'cypress.zip'
const zipName = uploadUtils.S3Configuration.binaryZipName
module.exports = {
zipName,
getPublisher () {
return uploadUtils.getPublisher(this.getAwsObj)
},
getAwsObj () {
return uploadUtils.getS3Credentials()
async getPublisher () {
return uploadUtils.getPublisher()
},
// returns desktop folder for a given folder without platform
@@ -43,7 +39,7 @@ module.exports = {
let { folder, version, platformArch, name } = options
if (!folder) {
folder = this.getAwsObj().folder
folder = uploadUtils.S3Configuration.releaseFolder
}
la(check.unemptyString(folder), 'missing folder', options)
@@ -104,34 +100,34 @@ module.exports = {
},
s3Manifest (version) {
const publisher = this.getPublisher()
return this.getPublisher()
.then((publisher) => {
const { releaseFolder } = uploadUtils.S3Configuration
const aws = this.getAwsObj()
const headers = {
'Cache-Control': 'no-cache',
}
let manifest = null
const headers = {}
return new Promise((resolve, reject) => {
return this.createRemoteManifest(releaseFolder, version)
.then((src) => {
manifest = src
headers['Cache-Control'] = 'no-cache'
return gulp.src(src)
.pipe(rename((p) => {
p.dirname = `${releaseFolder}/${p.dirname}`
let manifest = null
return new Promise((resolve, reject) => {
return this.createRemoteManifest(aws.folder, version)
.then((src) => {
manifest = src
return gulp.src(src)
.pipe(rename((p) => {
p.dirname = `${aws.folder}/${p.dirname}`
return p
})).pipe(gulpDebug())
.pipe(publisher.publish(headers))
.pipe(awspublish.reporter())
.on('error', reject)
.on('end', resolve)
return p
})).pipe(gulpDebug())
.pipe(publisher.publish(headers))
.pipe(awspublish.reporter())
.on('error', reject)
.on('end', resolve)
})
}).finally(() => {
return fs.removeAsync(manifest)
})
}).finally(() => {
return fs.removeAsync(manifest)
})
},
@@ -144,26 +140,27 @@ module.exports = {
la(check.extension(path.extname(uploadPath))(file),
'invalid file to upload extension', file)
return new Promise((resolve, reject) => {
const publisher = this.getPublisher()
return this.getPublisher()
.then((publisher) => {
const headers = {
'Cache-Control': 'no-cache',
}
const headers = {}
return new Promise((resolve, reject) => {
return gulp.src(file)
.pipe(rename((p) => {
// rename to standard filename for upload
p.basename = path.basename(uploadPath, path.extname(uploadPath))
p.dirname = path.dirname(uploadPath)
headers['Cache-Control'] = 'no-cache'
return gulp.src(file)
.pipe(rename((p) => {
// rename to standard filename for upload
p.basename = path.basename(uploadPath, path.extname(uploadPath))
p.dirname = path.dirname(uploadPath)
return p
}))
.pipe(gulpDebug())
.pipe(publisher.publish(headers))
.pipe(awspublish.reporter())
.on('error', reject)
.on('end', resolve)
return p
}))
.pipe(gulpDebug())
.pipe(publisher.publish(headers))
.pipe(awspublish.reporter())
.on('error', reject)
.on('end', resolve)
})
})
},
}
+33 -29
View File
@@ -1,5 +1,4 @@
const _ = require('lodash')
const path = require('path')
const awspublish = require('gulp-awspublish')
const human = require('human-interval')
const la = require('lazy-ass')
@@ -7,7 +6,8 @@ const check = require('check-more-types')
const fse = require('fs-extra')
const os = require('os')
const Promise = require('bluebird')
const { configFromEnvOrJsonFile, filenameToShellVariable } = require('@cypress/env-or-json-file')
const { fromSSO, fromEnv } = require('@aws-sdk/credential-providers')
const konfig = require('../get-config')()
const { purgeCloudflareCache } = require('./purge-cloudflare-cache')
@@ -25,47 +25,50 @@ const formHashFromEnvironment = function () {
} = process
if (env.CIRCLECI) {
return `circle-${env.CIRCLE_BRANCH}-${env.CIRCLE_SHA1}`
return `${env.CIRCLE_BRANCH}-${env.CIRCLE_SHA1}`
}
throw new Error('Do not know how to form unique build hash on this CI')
}
const getS3Credentials = function () {
const key = path.join('scripts', 'support', 'aws-credentials.json')
const config = configFromEnvOrJsonFile(key)
if (!config) {
console.error('⛔️ Cannot find AWS credentials')
console.error('Using @cypress/env-or-json-file module')
console.error('and filename', key)
console.error('which is environment variable', filenameToShellVariable(key))
console.error('available environment variable keys')
console.error(Object.keys(process.env))
throw new Error('AWS config not found')
}
la(check.unemptyString(config.bucket), 'missing AWS config bucket')
la(check.unemptyString(config.folder), 'missing AWS config folder')
la(check.unemptyString(config.key), 'missing AWS key')
la(check.unemptyString(config.secret), 'missing AWS secret key')
return config
const S3Configuration = {
bucket: 'cdn.cypress.io',
releaseFolder: 'desktop',
binaryZipName: 'cypress.zip',
betaUploadTypes: {
binary: {
uploadFolder: 'binary',
uploadFileName: 'cypress.zip',
},
'npm-package': {
uploadFolder: 'npm',
uploadFileName: 'cypress.tgz',
},
},
}
const getPublisher = function (getAwsObj = getS3Credentials) {
const aws = getAwsObj()
const getS3Credentials = async function () {
// sso is not required for CirceCI
if (process.env.CIRCLECI) {
return fromEnv()()
}
return fromSSO({ profile: process.env.AWS_PROFILE || 'production' })()
}
const getPublisher = async function () {
const aws = await getS3Credentials()
// console.log("aws.bucket", aws.bucket)
return awspublish.create({
httpOptions: {
timeout: human('10 minutes'),
},
params: {
Bucket: aws.bucket,
Bucket: S3Configuration.bucket,
},
accessKeyId: aws.key,
secretAccessKey: aws.secret,
accessKeyId: aws.accessKeyId,
secretAccessKey: aws.secretAccessKey,
sessionToken: aws.sessionToken,
})
}
@@ -156,6 +159,7 @@ const saveUrl = (filename) => {
}
module.exports = {
S3Configuration,
getS3Credentials,
getPublisher,
purgeDesktopAppFromCache,
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
set -e # exit on error
PLATFORM=$(node -p 'process.platform')
if [[ $PLATFORM != "linux" && $PLATFORM != "darwin" ]]; then
echo "Currently, create-stable-npm-package is only supported on Linux and MacOS."
echo "See https://github.com/cypress-io/cypress/pull/20296#discussion_r817115583"
exit 1
fi
if [[ ! $1 ]]; then
echo "publish-npm-package takes the .tgz URL as the first argument"
exit 1
fi
if [[ $1 != *"linux-x64"* ]]; then
echo "Only publish the 'linux-x64' .tgz. A non-linux-x64 .tgz was passed."
exit 1
fi
set -x # log commands
TGZ_URL=$1
PREPROD_TGZ_PATH=/tmp/cypress-preprod.tgz
UNPACKED_PATH=/tmp/unpacked-cypress
PROD_TGZ_PATH=/tmp/cypress-prod.tgz
echo "Downloading tgz from TGZ_URL=$TGZ_URL"
curl $TGZ_URL -o $PREPROD_TGZ_PATH
echo "Untarring PREPROD_TGZ_PATH=$PREPROD_TGZ_PATH"
rm -rf $UNPACKED_PATH || true
mkdir $UNPACKED_PATH
tar -xzvf $PREPROD_TGZ_PATH -C $UNPACKED_PATH
export PKG_JSON_PATH=$UNPACKED_PATH/package/package.json
echo "Patching stable: true to package.json"
node <<EOF
const fs = require('fs')
const pkg = require("$PKG_JSON_PATH")
pkg.buildInfo.stable = true
const json = JSON.stringify(pkg, null, 2)
fs.writeFileSync("$PKG_JSON_PATH", json)
EOF
echo "New package.json:"
cat $UNPACKED_PATH/package/package.json
echo "Tarring..."
cd $UNPACKED_PATH
tar -czvf $PROD_TGZ_PATH *
set +x
echo "Prod NPM package built at:"
echo " $PROD_TGZ_PATH"
+13 -9
View File
@@ -2,13 +2,14 @@
// See ../guides/next-version.md for documentation.
const path = require('path')
const semver = require('semver')
const Bluebird = require('bluebird')
const bumpCb = require('conventional-recommended-bump')
const { promisify } = require('util')
const currentVersion = require('../package.json').version
const bump = Bluebird.promisify(bumpCb)
const bump = promisify(bumpCb)
const paths = ['packages', 'cli']
let nextVersion
@@ -30,14 +31,17 @@ if (require.main !== module) {
return
}
Bluebird.mapSeries(paths, async (path) => {
const pathNextVersion = await getNextVersionForPath(path)
(async () => {
process.chdir(path.join(__dirname, '..'))
if (!nextVersion || semver.gt(pathNextVersion, nextVersion)) {
nextVersion = pathNextVersion
for (const path of paths) {
const pathNextVersion = await getNextVersionForPath(path)
if (!nextVersion || semver.gt(pathNextVersion, nextVersion)) {
nextVersion = pathNextVersion
}
}
})
.then(() => {
if (!nextVersion) {
throw new Error('Unable to determine next version.')
}
@@ -51,4 +55,4 @@ Bluebird.mapSeries(paths, async (path) => {
}
console.log(nextVersion)
})
})()
+27
View File
@@ -0,0 +1,27 @@
const minimist = require('minimist')
const shelljs = require('shelljs')
const args = minimist(process.argv.slice(2))
if (!/^[a-z0-9]{40}$/.test(args.sha)) {
throw new Error('A valid (40 character) commit SHA must be passed in `--sha`.')
}
if (!/^\d+\.\d+\.\d+$/.test(args.version)) {
throw new Error('A valid semantic version (X.Y.Z) must be passed in `--version`.')
}
// eslint-disable-next-line no-console
const log = (...args) => console.log('🏗', ...args)
const exec = args['dry-run'] ?
(...args) => log('Dry run, not executing:', ...args)
: (...args) => shelljs.exec(...args)
log('Running `move-binaries`...')
exec(`node ./scripts/binary.js move-binaries --sha ${args.sha} --version ${args.version}`)
const prereleaseNpmUrl = `https://cdn.cypress.io/beta/npm/${args.version}/linux-x64/develop-${args.sha}/cypress.tgz`
log('Running `create-stable-npm-package`...')
exec(`./scripts/create-stable-npm-package.sh ${prereleaseNpmUrl}`)
-139
View File
@@ -1,139 +0,0 @@
const la = require('lazy-ass')
const is = require('check-more-types')
const { getNameAndBinary, getJustVersion, getShortCommit } = require('./utils')
const bump = require('./binary/bump')
const { stripIndent } = require('common-tags')
const os = require('os')
const minimist = require('minimist')
const { getInstallJson } = require('@cypress/commit-message-install')
/* eslint-disable no-console */
// See ../guides/testing-other-projects.md for documentation.
const { npm, binary } = getNameAndBinary(process.argv)
la(is.unemptyString(npm), 'missing npm url')
la(is.unemptyString(binary), 'missing binary url')
const platform = os.platform()
const arch = os.arch()
console.log('bumping versions for other projects')
console.log(' npm:', npm)
console.log(' binary:', binary)
console.log(' platform:', platform)
console.log(' arch:', arch)
const cliOptions = minimist(process.argv, {
string: 'provider',
alias: {
provider: 'p',
},
})
/**
* Returns given string surrounded by ```json + ``` quotes
* @param {string} s
*/
const toJsonCodeBlock = (s) => {
const start = '```json'
const finish = '```'
return `${start}\n${s}\n${finish}\n`
}
/**
* Converts given JSON object into markdown text block
* @param {object} object
*/
const toMarkdownJsonBlock = (object) => {
la(object, 'expected an object to convert to JSON', object)
const str = JSON.stringify(object, null, 2)
return toJsonCodeBlock(str)
}
console.log('starting each test projects')
const shortNpmVersion = getJustVersion(npm)
console.log('short NPM version', shortNpmVersion)
let subject = `Testing new ${platform} ${arch} Cypress version ${shortNpmVersion}`
const commitInfo = getShortCommit()
if (commitInfo) {
subject += ` ${commitInfo.short}`
}
// instructions for installing this binary,
// see "@cypress/commit-message-install"
const env = {
CYPRESS_INSTALL_BINARY: binary,
}
const getStatusAndMessage = (projectRepoName) => {
// also pass "status" object that points back at this repo and this commit
// so that other projects can report their test success as GitHub commit status check
let status = null
const commit = commitInfo && commitInfo.sha
if (commit && is.commitId(commit)) {
// commit is full 40 character hex string
const platform = os.platform()
const arch = os.arch()
status = {
owner: 'cypress-io',
repo: 'cypress',
sha: commit,
platform,
arch,
context: `[${platform}-${arch}] ${projectRepoName}`,
}
}
const commitMessageInstructions = getInstallJson({
packages: npm,
env,
platform,
arch,
branch: shortNpmVersion, // use as version as branch name on test projects
commit,
status,
})
const jsonBlock = toMarkdownJsonBlock(commitMessageInstructions)
const footer =
'Use tool `@cypress/commit-message-install` to install from above block'
let message = `${subject}\n\n${jsonBlock}\n${footer}\n`
if (process.env.CIRCLE_BUILD_URL) {
message += '\n'
message += stripIndent`
CircleCI job url: ${process.env.CIRCLE_BUILD_URL}
`
}
console.log('commit message:')
console.log(message)
return {
status,
message,
}
}
const onError = (e) => {
console.error('could not bump test projects')
console.error(e)
process.exit(1)
}
bump
.runTestProjects(
getStatusAndMessage,
cliOptions.provider,
shortNpmVersion,
platform,
)
.catch(onError)
-48
View File
@@ -1,48 +0,0 @@
const la = require('lazy-ass')
const snapshot = require('snap-shot-it')
const _ = require('lodash')
const bump = require('../../binary/bump')
/* eslint-env mocha */
describe('bump', () => {
context('remapProjects', () => {
it('returns flat list of projects', () => {
la(bump._PROVIDERS, 'has _PROVIDERS', bump)
const list = bump.remapProjects(bump._PROVIDERS)
snapshot('list of all projects', list)
})
})
context('getFilterByProvider', () => {
it('returns a filter function without provider name', () => {
const projects = bump.remapProjects(bump._PROVIDERS)
const filter = bump.getFilterByProvider()
// should return ALL projects
const filtered = projects.filter(filter)
la(
_.isEqual(filtered, projects),
'should have kept all projects',
filtered,
)
})
it('returns a filter function for circle and linux', () => {
const projects = bump.remapProjects(bump._PROVIDERS)
la(
projects.length,
'there should be at least a few projects in the list of projects',
projects,
)
const filter = bump.getFilterByProvider('circle', 'linux')
const filtered = projects.filter(filter)
la(filtered.length, 'there should be at least a few projects', filtered)
snapshot('should have just circle and linux projects', filtered)
})
})
})
@@ -0,0 +1,10 @@
const shelljs = require('shelljs')
const snapshot = require('snap-shot-it')
describe('prepare-release-artifacts', () => {
it('runs expected commands', () => {
const stdout = shelljs.exec('yarn prepare-release-artifacts --dry-run --sha 57d0a85108fad6f77b39db88b8a7d8a3bfdb51a2 --version 1.2.3')
snapshot(stdout)
})
})
@@ -182,13 +182,13 @@ exports['testConfigOverrides / fails when passing invalid config values - [chrom
1) inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`""\`\`
Instead the value was: \`""\`
[stack trace lines]
2) inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"null"\`\`
Instead the value was: \`"null"\`
[stack trace lines]
3) context config overrides throws error
@@ -197,7 +197,7 @@ Instead the value was: \`"null"\`\`
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
@@ -209,7 +209,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
@@ -221,7 +221,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
@@ -234,7 +234,7 @@ https://on.cypress.io/config
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"not_an_http_url"\`\`
Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
Error
@@ -246,7 +246,7 @@ https://on.cypress.io/config
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
@@ -260,7 +260,7 @@ Because this error occurred during a \`before all\` hook we are skipping the rem
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
@@ -350,14 +350,14 @@ exports['testConfigOverrides / fails when passing invalid config values with bef
inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`""\`\`
Instead the value was: \`""\`
[stack trace lines]
2) runs all tests
inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"null"\`\`
Instead the value was: \`"null"\`
[stack trace lines]
3) runs all tests
@@ -367,7 +367,7 @@ Instead the value was: \`"null"\`\`
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
@@ -380,7 +380,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
@@ -393,7 +393,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
Error
@@ -407,7 +407,7 @@ https://on.cypress.io/config
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"not_an_http_url"\`\`
Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
Error
@@ -420,7 +420,7 @@ https://on.cypress.io/config
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
@@ -435,7 +435,7 @@ Because this error occurred during a \`before all\` hook we are skipping the rem
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
@@ -517,7 +517,7 @@ exports['testConfigOverrides / correctly fails when invalid config values for it
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
Error
@@ -601,13 +601,13 @@ exports['testConfigOverrides / fails when passing invalid config values - [firef
1) inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`""\`\`
Instead the value was: \`""\`
[stack trace lines]
2) inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"null"\`\`
Instead the value was: \`"null"\`
[stack trace lines]
3) context config overrides throws error
@@ -616,7 +616,7 @@ Instead the value was: \`"null"\`\`
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
@@ -627,7 +627,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
@@ -638,7 +638,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
@@ -650,7 +650,7 @@ https://on.cypress.io/config
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"not_an_http_url"\`\`
Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
[stack trace lines]
@@ -661,7 +661,7 @@ https://on.cypress.io/config
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
@@ -674,7 +674,7 @@ Because this error occurred during a \`before all\` hook we are skipping the rem
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
@@ -763,14 +763,14 @@ exports['testConfigOverrides / fails when passing invalid config values with bef
inline test config override throws error:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`""\`\`
Instead the value was: \`""\`
[stack trace lines]
2) runs all tests
inline test config override throws error when executed within cy cmd:
Error: Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"null"\`\`
Instead the value was: \`"null"\`
[stack trace lines]
3) runs all tests
@@ -780,7 +780,7 @@ Instead the value was: \`"null"\`\`
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
@@ -792,7 +792,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
@@ -804,7 +804,7 @@ https://on.cypress.io/config
Expected \`defaultCommandTimeout\` to be a number.
Instead the value was: \`"500"\`\`
Instead the value was: \`"500"\`
https://on.cypress.io/config
[stack trace lines]
@@ -817,7 +817,7 @@ https://on.cypress.io/config
Expected \`baseUrl\` to be a fully qualified URL (starting with \`http://\` or \`https://\`).
Instead the value was: \`"not_an_http_url"\`\`
Instead the value was: \`"not_an_http_url"\`
https://on.cypress.io/config
[stack trace lines]
@@ -829,7 +829,7 @@ https://on.cypress.io/config
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
@@ -843,7 +843,7 @@ Because this error occurred during a \`before all\` hook we are skipping the rem
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
@@ -924,7 +924,7 @@ exports['testConfigOverrides / correctly fails when invalid config values for it
Expected \`retries\` to be a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls.
Instead the value was: \`"1"\`\`
Instead the value was: \`"1"\`
https://on.cypress.io/config
[stack trace lines]
+8
View File
@@ -0,0 +1,8 @@
import { expect as _expect } from 'chai'
import _sinon from 'sinon'
declare global {
// these are made global in `spec_helper`
const expect: typeof _expect
const sinon: typeof _sinon
}
+7 -1
View File
@@ -57,7 +57,12 @@ class DockerProcess extends EventEmitter implements SpawnerResult {
for (const k in opts.env) {
// skip problematic env vars that we don't wanna preserve from `process.env`
if (['DISPLAY', 'USER', 'HOME', 'USERNAME', 'PATH'].includes(k)) continue
if (
['DISPLAY', 'USER', 'HOME', 'USERNAME', 'PATH'].includes(k)
|| k.startsWith('npm_')
) {
continue
}
containerCreateEnv.push([k, opts.env[k]].join('='))
}
@@ -78,6 +83,7 @@ class DockerProcess extends EventEmitter implements SpawnerResult {
Entrypoint: 'bash',
Tty: false, // so we can use stdout and stderr
Env: containerCreateEnv,
Privileged: true,
Binds: [
[path.join(__dirname, '..', '..'), '/cypress'],
// map tmpDir to the same absolute path on the container to make it easier to reason about paths in tests
+3 -3
View File
@@ -174,7 +174,7 @@ export async function scaffoldProjectNodeModules (project: string, updateYarnLoc
const runCmd = async (cmd) => {
console.log(`📦 Running "${cmd}" in ${projectDir}`)
await execa.shell(cmd, { cwd: projectDir, stdio: 'inherit' })
await execa(cmd, { cwd: projectDir, stdio: 'inherit', shell: true })
}
const cacheDir = _path.join(cachedir('cy-system-tests-node-modules'), project, 'node_modules')
@@ -326,8 +326,8 @@ export function remove () {
// returns the path to project fixture
// in the cyTmpDir
export function project (...args) {
return this.projectPath.apply(this, args)
export function project (name) {
return projectPath(name)
}
export function projectPath (name) {
-2
View File
@@ -2,8 +2,6 @@ import systemTests from './system-tests'
import dayjs from 'dayjs'
import _ from 'lodash'
const expect = global.expect as unknown as Chai.ExpectStatic
const STATIC_DATE = '2018-02-01T20:14:19.323Z'
const expectDurationWithin = function (obj, duration, low, high, reset) {
+1 -1
View File
@@ -166,7 +166,7 @@ const getResponse = function (responseSchema) {
}
const sendResponse = function (req, res, responseBody) {
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
const _writeRaw = res._writeRaw
res._writeRaw = function () {

Some files were not shown because too many files have changed in this diff Show More