mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-02 13:00:18 -05:00
Merge branch 'develop' into feature-multidomain
This commit is contained in:
@@ -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 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
`
|
||||
`
|
||||
|
||||
@@ -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.
|
||||
|
||||
`
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 }) => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -356,6 +356,10 @@ const util = {
|
||||
return process.cwd()
|
||||
},
|
||||
|
||||
pkgBuildInfo () {
|
||||
return pkg.buildInfo
|
||||
},
|
||||
|
||||
pkgVersion () {
|
||||
return pkg.version
|
||||
},
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(' ')
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||

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

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

|
||||
- 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,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).
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -187,3 +187,4 @@ describe('#startDevServer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
.timeout(5000)
|
||||
|
||||
+5
-8
@@ -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', {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import $jquery from './jquery'
|
||||
import $document from './document'
|
||||
|
||||
|
||||
@@ -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
@@ -56,6 +56,7 @@ declare namespace Cypress {
|
||||
mocha: $Mocha
|
||||
configure: (config: Cypress.ObjectLike) => void
|
||||
isMultiDomain: boolean
|
||||
originalConfig: Cypress.ObjectLike
|
||||
}
|
||||
|
||||
interface CypressUtils {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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({
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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.\
|
||||
`,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Executable
+57
@@ -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"
|
||||
@@ -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)
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -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}`)
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
|
||||
Vendored
+8
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user