Merge branch 'develop'

This commit is contained in:
Chris Breiding
2021-04-05 18:37:14 -04:00
523 changed files with 13470 additions and 11203 deletions

26
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,26 @@
# Each line is a file pattern followed by one or more owners.
# Test Runner team are owners of code within root packages and cli
/cli/ @cypress-io/test-runner
/packages/coffee/ @cypress-io/test-runner
/packages/datetime-utils/ @cypress-io/test-runner
/packages/desktop-gui/ @cypress-io/test-runner
/packages/driver/ @cypress-io/test-runner
/packages/electron/ @cypress-io/test-runner
/packages/example/ @cypress-io/test-runner
/packages/extension/ @cypress-io/test-runner
/packages/https-proxy/ @cypress-io/test-runner
/packages/launcher/ @cypress-io/test-runner
/packages/net-stubbing/ @cypress-io/test-runner
/packages/network/ @cypress-io/test-runner
/packages/proxy/ @cypress-io/test-runner
/packages/reporter/ @cypress-io/test-runner
/packages/rewriter/ @cypress-io/test-runner
/packages/root/ @cypress-io/test-runner
/packages/runner/ @cypress-io/test-runner
/packages/server/ @cypress-io/test-runner
/packages/socket/ @cypress-io/test-runner
/packages/static/ @cypress-io/test-runner
/packages/ts/ @cypress-io/test-runner
/packages/ui-components/ @cypress-io/test-runner
/packages/web-config/ @cypress-io/test-runner

6
.gitignore vendored
View File

@@ -21,13 +21,17 @@ packages/desktop-gui/src/jsconfig.json
packages/driver/cypress/videos
packages/driver/cypress/screenshots
# from runner
packages/runner/cypress/videos
packages/runner/cypress/screenshots
# npm packages
npm/**/cypress/screenshots
# from example
packages/example/app
packages/example/build
packages/example/cypress
packages/example/cypress/integration
# from server
packages/server/.cy

View File

@@ -1 +1 @@
12.18.3
14.16.0

View File

@@ -2,14 +2,14 @@ branches:
only:
- master
- develop
- sem-next-ver
- 7.0-release
- /win*/
- feature/cross-platform-wizard
# https://www.appveyor.com/docs/lang/nodejs-iojs/
environment:
# use matching version of Node.js
nodejs_version: "12.18.3"
nodejs_version: "14.16.0"
# encode secure variables which will NOT be used
# in pull requests
# https://www.appveyor.com/docs/build-configuration/#secure-variables
@@ -48,7 +48,10 @@ platform:
# Install scripts. (runs after repo cloning)
install:
- ps: Install-Product node $env:nodejs_version $env:platform
# it is not a given that AppVeyor has the exact version of Node we need predownloaded
# see https://www.appveyor.com/docs/lang/nodejs-iojs/#installing-any-version-of-nodejs-or-iojs
- ps: Update-NodeJsInstallation $env:nodejs_version $env:platform
# - ps: Install-Product node $env:nodejs_version $env:platform
# upgrade npm
- yarn global add @bahmutov/print-env@1
# Output useful info for debugging.

View File

@@ -8,7 +8,7 @@ macBuildFilters: &macBuildFilters
branches:
only:
- develop
- fix-next-version
- 7.0-release
- include-electron-node-version
defaults: &defaults
@@ -36,7 +36,7 @@ testBinaryFirefox: &testBinaryFirefox
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- create-build-artifacts
@@ -44,7 +44,7 @@ executors:
# the Docker image with Cypress dependencies and Chrome browser
cy-doc:
docker:
- image: cypress/browsers:node12.18.3-chrome83-ff77
- image: cypress/browsers:node14.16.0-chrome89-ff77
environment:
PLATFORM: linux
@@ -58,7 +58,7 @@ executors:
# Docker image with non-root "node" user
non-root-docker-user:
docker:
- image: cypress/browsers:node12.18.3-chrome83-ff77
- image: cypress/browsers:node14.16.0-chrome89-ff77
user: node
environment:
PLATFORM: linux
@@ -69,7 +69,7 @@ executors:
mac:
macos:
# Executor should have Node >= required version
xcode: "11.3.1"
xcode: "12.2.0"
environment:
PLATFORM: mac
@@ -849,6 +849,8 @@ jobs:
- run: yarn test-mocha
# test binary build code
- run: yarn test-scripts
# check for compile errors with the releaserc scripts
- run: yarn test-npm-package-release-script
# make sure our snapshots are compared correctly
- run: yarn test-mocha-snapshot
# make sure packages with TypeScript can be transpiled to JS
@@ -1035,27 +1037,13 @@ jobs:
path: /tmp/artifacts
- store-npm-logs
desktop-gui-component-tests:
<<: *defaults
parallelism: 1
steps:
- attach_workspace:
at: ~/
- run:
# builds JS and CSS, and we need the app CSS
# to correctly apply component styles
command: |
yarn build-prod
ls -la dist
working_directory: packages/desktop-gui
# for now, because we have baseUrl in cypress.json
# we must start it, even if we don't use it for component tests
# https://github.com/cypress-io/cypress/issues/7800
- run:
name: Unnecessary Desktop GUI server
command: yarn start
working_directory: packages/desktop-gui
background: true
- run:
# will use PERCY_TOKEN environment variable if available
command: |
@@ -1063,7 +1051,7 @@ jobs:
PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_ID \
PERCY_PARALLEL_TOTAL=-1 \
yarn percy exec -- \
yarn cypress:run --spec 'src/**/*_spec.jsx'
yarn cypress:run:ct
working_directory: packages/desktop-gui
- verify-mocha-results
# we don't really need any artifacts - we are only interested in visual screenshots
@@ -1176,7 +1164,13 @@ jobs:
at: ~/
- run:
name: Run tests
command: yarn workspace @cypress/vite-dev-server test
command: yarn test --reporter cypress-circleci-reporter --reporter-options resultsDir=./test_results
working_directory: npm/vite-dev-server
- store_test_results:
path: npm/vite-dev-server/test_results
- store_artifacts:
path: npm/vite-dev-server/cypress/videos
- store-npm-logs
npm-rollup-dev-server:
<<: *defaults
@@ -1206,8 +1200,12 @@ jobs:
command: yarn workspace @cypress/vue build
- run:
name: Run tests
command: yarn test
command: yarn test --reporter cypress-circleci-reporter --reporter-options resultsDir=./test_results
working_directory: npm/vue
- store_test_results:
path: npm/vue/test_results
- store_artifacts:
path: npm/vue/test_results
- store-npm-logs
npm-design-system:
@@ -1287,7 +1285,7 @@ jobs:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "fix-next-version" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "7.0-release" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
@@ -1436,7 +1434,7 @@ jobs:
test-npm-module-on-minimum-node-version:
<<: *defaults
docker:
- image: cypress/base:10.0.0
- image: cypress/base:12.0.0-libgbm
steps:
- attach_workspace:
at: ~/
@@ -1776,6 +1774,7 @@ linux-workflow: &linux-workflow
jobs:
- build
- lint:
name: Linux lint
requires:
- build
- percy-finalize:
@@ -1854,9 +1853,11 @@ linux-workflow: &linux-workflow
- npm-webpack-dev-server:
requires:
- build
- npm-vite-dev-server:
requires:
- build
# TODO: add this back when flake with vite-electron has been resolved
# See branch `fix-branch`
# - npm-vite-dev-server:
# requires:
# - build
- npm-rollup-dev-server:
requires:
- build
@@ -1895,13 +1896,12 @@ linux-workflow: &linux-workflow
- npm-webpack-batteries-included-preprocessor
- npm-webpack-preprocessor
- npm-rollup-dev-server
- npm-vite-dev-server
# - npm-vite-dev-server
- npm-webpack-dev-server
- run-launcher
- ui-components-integration-tests
- reporter-integration-tests
- lint
- percy-finalize
- Linux lint
- desktop-gui-component-tests
- desktop-gui-integration-tests-2x
- runner-ct-integration-tests-chrome
@@ -1927,7 +1927,7 @@ linux-workflow: &linux-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- build
- test-kitchensink:
@@ -1939,7 +1939,7 @@ linux-workflow: &linux-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- build
- create-build-artifacts:
@@ -1989,7 +1989,7 @@ linux-workflow: &linux-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- create-build-artifacts
- test-npm-module-and-verify-binary:
@@ -1997,7 +1997,7 @@ linux-workflow: &linux-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- create-build-artifacts
- test-binary-against-staging:
@@ -2006,7 +2006,7 @@ linux-workflow: &linux-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- create-build-artifacts
@@ -2032,7 +2032,7 @@ linux-workflow: &linux-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- create-build-artifacts
@@ -2095,7 +2095,7 @@ mac-workflow: &mac-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- darwin-create-build-artifacts
@@ -2107,7 +2107,7 @@ mac-workflow: &mac-workflow
branches:
only:
- develop
- fix-next-version
- 7.0-release
requires:
- darwin-create-build-artifacts

View File

@@ -211,8 +211,10 @@ exports['cli help command shows help 1'] = `
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
open-ct [options] Opens Cypress component testing interactive mode.
run-ct [options] Runs all Cypress Component Testing suites
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -247,8 +249,10 @@ exports['cli help command shows help for -h 1'] = `
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
open-ct [options] Opens Cypress component testing interactive mode.
run-ct [options] Runs all Cypress Component Testing suites
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -283,8 +287,10 @@ exports['cli help command shows help for --help 1'] = `
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
open-ct [options] Opens Cypress component testing interactive mode.
run-ct [options] Runs all Cypress Component Testing suites
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -320,8 +326,10 @@ exports['cli unknown command shows usage and exits 1'] = `
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
open-ct [options] Opens Cypress component testing interactive mode.
run-ct [options] Runs all Cypress Component Testing suites
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -443,8 +451,10 @@ exports['cli CYPRESS_INTERNAL_ENV allows and warns when staging environment 1']
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
open-ct [options] Opens Cypress component testing interactive mode.
run-ct [options] Runs all Cypress Component Testing suites
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and

View File

@@ -4,8 +4,7 @@ exports['lib/exec/spawn .start forces colors and streams when supported 1'] = {
"MOCHA_COLORS": "1",
"FORCE_STDIN_TTY": "1",
"FORCE_STDOUT_TTY": "1",
"FORCE_STDERR_TTY": "1",
"NODE_OPTIONS": "--max-http-header-size=1048576"
"FORCE_STDERR_TTY": "1"
}
exports['lib/exec/spawn .start does not force colors and streams when not supported 1'] = {
@@ -13,8 +12,7 @@ exports['lib/exec/spawn .start does not force colors and streams when not suppor
"DEBUG_COLORS": "0",
"FORCE_STDIN_TTY": "0",
"FORCE_STDOUT_TTY": "0",
"FORCE_STDERR_TTY": "0",
"NODE_OPTIONS": "--max-http-header-size=1048576"
"FORCE_STDERR_TTY": "0"
}
exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = `

View File

@@ -137,8 +137,8 @@ const knownCommands = [
'--help',
'install',
'open',
'open-ct',
'run',
'open-ct',
'run-ct',
'verify',
'-v',
@@ -380,6 +380,26 @@ module.exports = {
showVersions(args)
})
program
.command('open')
.usage('[options]')
.description('Opens Cypress in the interactive GUI.')
.option('-b, --browser <browser-path>', text('browserOpenMode'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('-d, --detached [bool]', text('detached'), coerceFalse)
.option('-e, --env <env>', text('env'))
.option('--global', text('global'))
.option('-p, --port <port>', text('port'))
.option('-P, --project <project-path>', text('project'))
.option('--dev', text('dev'), coerceFalse)
.action((opts) => {
debug('opening Cypress')
require('./exec/open')
.start(util.parseOpts(opts))
.catch(util.logErrorExit1)
})
addCypressRunCommand(program)
.action((...fnArgs) => {
debug('running Cypress with args %o', fnArgs)
@@ -390,8 +410,7 @@ module.exports = {
})
program
// TODO make this command public once CT will be merged completely
.command('open-ct', { hidden: true })
.command('open-ct')
.usage('[options]')
.description('Opens Cypress component testing interactive mode.')
.option('-b, --browser <browser-path>', text('browserOpenMode'))
@@ -411,8 +430,7 @@ module.exports = {
})
program
// TODO make this command public once CT will be merged completely
.command('run-ct', { hidden: true })
.command('run-ct')
.usage('[options]')
.description('Runs all Cypress Component Testing suites')
.option('-b, --browser <browser-name-or-path>', text('browserRunMode'))
@@ -443,26 +461,6 @@ module.exports = {
.catch(util.logErrorExit1)
})
program
.command('open')
.usage('[options]')
.description('Opens Cypress in the interactive GUI.')
.option('-b, --browser <browser-path>', text('browserOpenMode'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('-d, --detached [bool]', text('detached'), coerceFalse)
.option('-e, --env <env>', text('env'))
.option('--global', text('global'))
.option('-p, --port <port>', text('port'))
.option('-P, --project <project-path>', text('project'))
.option('--dev', text('dev'), coerceFalse)
.action((opts) => {
debug('opening Cypress')
require('./exec/open')
.start(util.parseOpts(opts))
.catch(util.logErrorExit1)
})
program
.command('install')
.usage('[options]')

View File

@@ -36,7 +36,7 @@ module.exports = {
}
if (isComponentTesting) {
args.push('--componentTesting')
args.push('--testing-type', 'component')
}
debug('opening from options %j', options)

View File

@@ -184,7 +184,7 @@ module.exports = {
}
if (isComponentTesting) {
args.push('--componentTesting')
args.push('--testing-type', 'component')
}
debug('run to spawn.start args %j', args)

View File

@@ -197,7 +197,6 @@ const parseOpts = (opts) => {
'cacheClear',
'cachePrune',
'ciBuildId',
'componentTesting',
'config',
'configFile',
'cypressVersion',
@@ -281,35 +280,9 @@ const util = {
.mapValues((value) => { // stringify to 1 or 0
return value ? '1' : '0'
})
.extend(util.getNodeOptions(options))
.value()
},
getNodeOptions (options, nodeVersion) {
if (!nodeVersion) {
nodeVersion = Number(process.versions.node.split('.')[0])
}
if (options.dev && nodeVersion < 12) {
// `node` is used instead of Electron when --dev is passed, so this won't work if Node is too old
debug('NODE_OPTIONS=--max-http-header-size could not be set because we\'re in dev mode and Node is < 12.0.0')
return
}
// https://github.com/cypress-io/cypress/issues/5431
const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2}`
if (_.isString(process.env.NODE_OPTIONS)) {
return {
NODE_OPTIONS: `${NODE_OPTIONS} ${process.env.NODE_OPTIONS}`,
ORIGINAL_NODE_OPTIONS: process.env.NODE_OPTIONS || '',
}
}
return { NODE_OPTIONS }
},
getForceTty () {
return {
FORCE_STDIN_TTY: util.isTty(process.stdin.fd),

View File

@@ -13,7 +13,7 @@
"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)",
"test-dependencies": "dependency-check . --no-dev",
"test-dependencies": "dependency-check . --missing --no-dev --verbose",
"test-unit": "yarn unit",
"test-watch": "yarn unit --watch",
"types": "yarn dtslint",
@@ -23,11 +23,11 @@
"@cypress/listr-verbose-renderer": "^0.4.1",
"@cypress/request": "^2.88.5",
"@cypress/xvfb": "^1.2.4",
"@types/node": "12.12.50",
"@types/sinonjs__fake-timers": "^6.0.1",
"@types/node": "^14.14.31",
"@types/sinonjs__fake-timers": "^6.0.2",
"@types/sizzle": "^2.3.2",
"arch": "^2.1.2",
"blob-util": "2.0.2",
"arch": "^2.2.0",
"blob-util": "^2.0.2",
"bluebird": "^3.7.2",
"cachedir": "^2.3.0",
"chalk": "^4.1.0",
@@ -35,66 +35,65 @@
"cli-table3": "~0.6.0",
"commander": "^5.1.0",
"common-tags": "^1.8.0",
"dayjs": "^1.9.3",
"dayjs": "^1.10.4",
"debug": "4.3.2",
"eventemitter2": "^6.4.2",
"execa": "^4.0.2",
"eventemitter2": "^6.4.3",
"execa": "4.1.0",
"executable": "^4.1.1",
"extract-zip": "^1.7.0",
"fs-extra": "^9.0.1",
"fs-extra": "^9.1.0",
"getos": "^3.2.1",
"is-ci": "^2.0.0",
"is-installed-globally": "^0.3.2",
"is-ci": "^3.0.0",
"is-installed-globally": "~0.4.0",
"lazy-ass": "^1.6.0",
"listr": "^0.14.3",
"lodash": "^4.17.19",
"lodash": "^4.17.21",
"log-symbols": "^4.0.0",
"minimist": "^1.2.5",
"moment": "^2.29.1",
"ospath": "^1.2.2",
"pretty-bytes": "^5.4.1",
"pretty-bytes": "^5.6.0",
"ramda": "~0.27.1",
"request-progress": "^3.0.0",
"supports-color": "^7.2.0",
"supports-color": "^8.1.1",
"tmp": "~0.2.1",
"untildify": "^4.0.0",
"url": "^0.11.0",
"yauzl": "^2.10.0"
},
"devDependencies": {
"@babel/cli": "7.8.4",
"@babel/preset-env": "7.9.5",
"@babel/cli": "7.13.0",
"@babel/preset-env": "7.13.5",
"@cypress/sinon-chai": "2.9.1",
"@packages/root": "0.0.0-development",
"@types/bluebird": "3.5.29",
"@types/chai": "4.2.7",
"@types/bluebird": "3.5.33",
"@types/chai": "4.2.15",
"@types/chai-jquery": "1.1.40",
"@types/jquery": "3.3.31",
"@types/lodash": "4.14.149",
"@types/lodash": "4.14.168",
"@types/minimatch": "3.0.3",
"@types/mocha": "5.2.7",
"@types/sinon": "7.5.1",
"@types/sinon-chai": "3.2.3",
"@types/sinon-chai": "3.2.5",
"chai": "3.5.0",
"chai-as-promised": "7.1.1",
"chai-string": "1.5.0",
"cross-env": "6.0.3",
"dependency-check": "3.4.1",
"dtslint": "0.9.0",
"cross-env": "7.0.3",
"dependency-check": "4.1.0",
"dtslint": "4.0.7",
"execa-wrap": "1.4.0",
"hasha": "5.0.0",
"hasha": "5.2.2",
"mocha": "6.2.2",
"mock-fs": "4.12.0",
"mocked-env": "1.2.4",
"nock": "12.0.2",
"postinstall-postinstall": "2.0.0",
"mock-fs": "4.13.0",
"mocked-env": "1.3.2",
"nock": "13.0.7",
"postinstall-postinstall": "2.1.0",
"proxyquire": "2.1.3",
"resolve-pkg": "2.0.0",
"shelljs": "0.8.3",
"shelljs": "0.8.4",
"sinon": "7.2.2",
"snap-shot-it": "7.9.3",
"snap-shot-it": "7.9.6",
"spawn-mock": "1.0.0",
"strip-ansi": "4.0.0"
"strip-ansi": "6.0.0"
},
"files": [
"bin",
@@ -107,7 +106,7 @@
"cypress": "bin/cypress"
},
"engines": {
"node": ">=10.0.0"
"node": ">=12.0.0"
},
"types": "types"
}

View File

@@ -2,280 +2,296 @@
"title": "JSON schema for the https://cypress.io Test Runner's configuration file. Details at https://on.cypress.io/configuration",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"baseUrl": {
"type": "string",
"description": "Url used as prefix for cy.visit() or cy.request() commands url. Example http://localhost:3030 or https://test.my-domain.com"
},
"env": {
"type": "object",
"description": "Any values to be set as environment variables. See https://on.cypress.io/environment-variables",
"body": {}
},
"ignoreTestFiles": {
"type": [
"string",
"array"
],
"items": {
"type": "string"
},
"description": "A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using http://globtester.com to test what files would match."
},
"numTestsKeptInMemory": {
"type": "number",
"default": 50,
"description": "The number of tests for which snapshots and command data are kept in memory. Reduce this number if you are experiencing high memory consumption in your browser during a test run."
},
"port": {
"type": "number",
"default": null,
"description": "Port used to host Cypress. Normally this is a randomly generated port"
},
"reporter": {
"type": "string",
"default": "spec",
"description": "The reporter used when running headlessly or in CI. See https://on.cypress.io/reporters"
},
"reporterOptions": {
"type": "object",
"default": null,
"description": "The reporter options used. Supported options depend on the reporter. See https://on.cypress.io/reporters#Reporter-Options"
},
"testFiles": {
"type": [
"string",
"array"
],
"default": "**/*.*",
"description": "A String or Array of string glob patterns of the test files to load. See https://on.cypress.io/configuration#Global"
},
"watchForFileChanges": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will watch and restart tests on test file changes"
},
"defaultCommandTimeout": {
"type": "number",
"default": 4000,
"description": "Time, in milliseconds, to wait until most DOM based commands are considered timed out"
},
"execTimeout": {
"type": "number",
"default": 60000,
"description": "Time, in milliseconds, to wait for a system command to finish executing during a cy.exec() command"
},
"taskTimeout": {
"type": "number",
"default": 60000,
"description": "Time, in milliseconds, to wait for a task to finish executing during a cy.task() command"
},
"pageLoadTimeout": {
"type": "number",
"default": 60000,
"description": "Time, in milliseconds, to wait for page transition events or cy.visit(), cy.go(), cy.reload() commands to fire their page load events. Network requests are limited by the underlying operating system, and may still time out if this value is increased."
},
"requestTimeout": {
"type": "number",
"default": 5000,
"description": "Time, in milliseconds, to wait for an XHR request to go out in a cy.wait() command"
},
"responseTimeout": {
"type": "number",
"default": 30000,
"description": "Time, in milliseconds, to wait until a response in a cy.request(), cy.wait(), cy.fixture(), cy.getCookie(), cy.getCookies(), cy.setCookie(), cy.clearCookie(), cy.clearCookies(), and cy.screenshot() commands"
},
"fileServerFolder": {
"type": "string",
"default": "root project folder",
"description": "Path to folder where application files will attempt to be served from"
},
"fixturesFolder": {
"type": [
"string",
"boolean"
],
"default": "cypress/fixtures",
"description": "Path to folder containing fixture files (Pass false to disable)"
},
"integrationFolder": {
"type": "string",
"default": "cypress/integration",
"description": "Path to folder containing integration test files"
},
"downloadsFolder": {
"type": "string",
"default": "cypress/downloads",
"description": "Path to folder where files downloaded during a test are saved"
},
"experimentalComponentTesting": {
"type": "boolean",
"default": false,
"description": "Enabled experimental component testing, see https://github.com/cypress-io/cypress/issues/5922"
},
"componentFolder": {
"type": [
"string",
"boolean"
],
"default": false,
"description": "Path to folder containing component test files (Pass false to disable)"
},
"pluginsFile": {
"type": [
"string",
"boolean"
],
"default": "cypress/plugins/index.js",
"description": "Path to plugins file. (Pass false to disable)"
},
"screenshotOnRunFailure": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will take a screenshot when a test fails during cypress run"
},
"screenshotsFolder": {
"type": "string",
"default": "cypress/screenshots",
"description": "Path to folder where screenshots will be saved from cy.screenshot() command or after a test fails during cypress run"
},
"supportFile": {
"type": [
"string",
"boolean"
],
"default": "cypress/support/index.js",
"description": "Path to file to load before test files load. This file is compiled and bundled. (Pass false to disable)"
},
"videosFolder": {
"type": "string",
"default": "cypress/videos",
"description": "Path to folder where videos will be saved during cypress run"
},
"trashAssetsBeforeRuns": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will trash assets within the screenshotsFolder and videosFolder before tests run with cypress run"
},
"videoCompression": {
"type": [
"number",
"boolean"
],
"default": 32,
"description": "The quality setting for the video compression, in Constant Rate Factor (CRF). The value can be false to disable compression or a value between 0 and 51, where a lower value results in better quality (at the expense of a higher file size)."
},
"video": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will capture a video of the tests run with cypress run"
},
"videoUploadOnPasses": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will process, compress, and upload videos to the Dashboard even when all tests in a spec file are passing. This only applies when recording your runs to the Dashboard. Turn this off if youd like to only upload the spec files video when there are failing tests."
},
"chromeWebSecurity": {
"type": "boolean",
"default": true,
"description": "Whether Chrome Web Security for same-origin policy and insecure mixed content is enabled. Read more about this at https://on.cypress.io/web-security"
},
"userAgent": {
"type": "string",
"default": null,
"description": "Enables you to override the default user agent the browser sends in all request headers. User agent values are typically used by servers to help identify the operating system, browser, and browser version. See User-Agent MDN Documentation for example user agent values here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent"
},
"blockHosts": {
"type": [
"string",
"array"
],
"items": {
"type": "string"
},
"default": null,
"description": "A String or Array of hosts that you wish to block traffic for. Please read the notes for examples on using this https://on.cypress.io/configuration#blockHosts"
},
"modifyObstructiveCode": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will search for and replace obstructive JS code found in .js or .html files that prevent Cypress from working. Please read the notes for more information on this setting. https://on.cypress.io/configuration#modifyObstructiveCode"
},
"viewportHeight": {
"type": "number",
"default": 660,
"description": "Default height in pixels for the application under tests viewport (Override with cy.viewport() command)"
},
"viewportWidth": {
"type": "number",
"default": 1000,
"description": "Default width in pixels for the application under tests viewport. (Override with cy.viewport() command)"
},
"animationDistanceThreshold": {
"type": "number",
"default": 5,
"description": "The distance in pixels an element must exceed over time to be considered animating"
},
"waitForAnimations": {
"type": "boolean",
"default": true,
"description": "Whether to wait for elements to finish animating before executing commands"
},
"scrollBehavior": {
"enum": [
false,
"center",
"top",
"bottom",
"nearest"
],
"default": "top",
"description": "Viewport position to which an element should be scrolled prior to action commands. Setting `false` disables scrolling."
},
"projectId": {
"type": "string",
"default": null,
"description": "A 6 character string use to identify this project in the Cypress Dashboard. See https://on.cypress.io/dashboard-service#Identification"
},
"nodeVersion": {
"enum": [
"system",
"bundled"
],
"default": "bundled",
"description": "If set to 'system', Cypress will try to find a Node.js executable on your path to use when executing your plugins. Otherwise, Cypress will use the Node version bundled with Cypress."
},
"experimentalSourceRewriting": {
"type": "boolean",
"default": false,
"description": "Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm."
},
"experimentalFetchPolyfill": {
"type": "boolean",
"default": false,
"description": "Polyfills `window.fetch` to enable Network spying and stubbing"
},
"experimentalStudio": {
"type": "boolean",
"default": false,
"description": "Generate and save commands directly to your test suite by interacting with your app as an end user would."
},
"retries": {
"type": [
"object",
"number",
"null"
],
"default": {
"runMode": 0,
"openMode": 0
},
"description": "The number of times to retry a failing. Can be configured to apply only in runMode or openMode"
},
"includeShadowDom": {
"type": "boolean",
"default": false,
"description": "Enables including elements within the shadow DOM when using querying commands (e.g. cy.get(), cy.find()). Can be set globally in cypress.json, per-suite or per-test in the test configuration object, or programmatically with Cypress.config()"
"definitions": {
"cypressConfig": {
"properties": {
"baseUrl": {
"type": "string",
"description": "Url used as prefix for cy.visit() or cy.request() commands url. Example http://localhost:3030 or https://test.my-domain.com"
},
"env": {
"type": "object",
"description": "Any values to be set as environment variables. See https://on.cypress.io/environment-variables",
"body": {}
},
"ignoreTestFiles": {
"type": [
"string",
"array"
],
"items": {
"type": "string"
},
"description": "A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using http://globtester.com to test what files would match."
},
"numTestsKeptInMemory": {
"type": "number",
"default": 50,
"description": "The number of tests for which snapshots and command data are kept in memory. Reduce this number if you are experiencing high memory consumption in your browser during a test run."
},
"port": {
"type": "number",
"default": null,
"description": "Port used to host Cypress. Normally this is a randomly generated port"
},
"reporter": {
"type": "string",
"default": "spec",
"description": "The reporter used when running headlessly or in CI. See https://on.cypress.io/reporters"
},
"reporterOptions": {
"type": "object",
"default": null,
"description": "The reporter options used. Supported options depend on the reporter. See https://on.cypress.io/reporters#Reporter-Options"
},
"testFiles": {
"type": [
"string",
"array"
],
"default": "**/*.*",
"description": "A String or Array of string glob patterns of the test files to load. See https://on.cypress.io/configuration#Global"
},
"watchForFileChanges": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will watch and restart tests on test file changes"
},
"defaultCommandTimeout": {
"type": "number",
"default": 4000,
"description": "Time, in milliseconds, to wait until most DOM based commands are considered timed out"
},
"execTimeout": {
"type": "number",
"default": 60000,
"description": "Time, in milliseconds, to wait for a system command to finish executing during a cy.exec() command"
},
"taskTimeout": {
"type": "number",
"default": 60000,
"description": "Time, in milliseconds, to wait for a task to finish executing during a cy.task() command"
},
"pageLoadTimeout": {
"type": "number",
"default": 60000,
"description": "Time, in milliseconds, to wait for page transition events or cy.visit(), cy.go(), cy.reload() commands to fire their page load events. Network requests are limited by the underlying operating system, and may still time out if this value is increased."
},
"requestTimeout": {
"type": "number",
"default": 5000,
"description": "Time, in milliseconds, to wait for an XHR request to go out in a cy.wait() command"
},
"responseTimeout": {
"type": "number",
"default": 30000,
"description": "Time, in milliseconds, to wait until a response in a cy.request(), cy.wait(), cy.fixture(), cy.getCookie(), cy.getCookies(), cy.setCookie(), cy.clearCookie(), cy.clearCookies(), and cy.screenshot() commands"
},
"fileServerFolder": {
"type": "string",
"default": "root project folder",
"description": "Path to folder where application files will attempt to be served from"
},
"fixturesFolder": {
"type": [
"string",
"boolean"
],
"default": "cypress/fixtures",
"description": "Path to folder containing fixture files (Pass false to disable)"
},
"integrationFolder": {
"type": "string",
"default": "cypress/integration",
"description": "Path to folder containing integration test files"
},
"downloadsFolder": {
"type": "string",
"default": "cypress/downloads",
"description": "Path to folder where files downloaded during a test are saved"
},
"componentFolder": {
"type": [
"string",
"boolean"
],
"default": false,
"description": "Path to folder containing component test files (Pass false to disable)"
},
"pluginsFile": {
"type": [
"string",
"boolean"
],
"default": "cypress/plugins/index.js",
"description": "Path to plugins file. (Pass false to disable)"
},
"screenshotOnRunFailure": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will take a screenshot when a test fails during cypress run"
},
"screenshotsFolder": {
"type": "string",
"default": "cypress/screenshots",
"description": "Path to folder where screenshots will be saved from cy.screenshot() command or after a test fails during cypress run"
},
"supportFile": {
"type": [
"string",
"boolean"
],
"default": "cypress/support/index.js",
"description": "Path to file to load before test files load. This file is compiled and bundled. (Pass false to disable)"
},
"videosFolder": {
"type": "string",
"default": "cypress/videos",
"description": "Path to folder where videos will be saved during cypress run"
},
"trashAssetsBeforeRuns": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will trash assets within the screenshotsFolder and videosFolder before tests run with cypress run"
},
"videoCompression": {
"type": [
"number",
"boolean"
],
"default": 32,
"description": "The quality setting for the video compression, in Constant Rate Factor (CRF). The value can be false to disable compression or a value between 0 and 51, where a lower value results in better quality (at the expense of a higher file size)."
},
"video": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will capture a video of the tests run with cypress run"
},
"videoUploadOnPasses": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will process, compress, and upload videos to the Dashboard even when all tests in a spec file are passing. This only applies when recording your runs to the Dashboard. Turn this off if youd like to only upload the spec files video when there are failing tests."
},
"chromeWebSecurity": {
"type": "boolean",
"default": true,
"description": "Whether Chrome Web Security for same-origin policy and insecure mixed content is enabled. Read more about this at https://on.cypress.io/web-security"
},
"userAgent": {
"type": "string",
"default": null,
"description": "Enables you to override the default user agent the browser sends in all request headers. User agent values are typically used by servers to help identify the operating system, browser, and browser version. See User-Agent MDN Documentation for example user agent values here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent"
},
"blockHosts": {
"type": [
"string",
"array"
],
"items": {
"type": "string"
},
"default": null,
"description": "A String or Array of hosts that you wish to block traffic for. Please read the notes for examples on using this https://on.cypress.io/configuration#blockHosts"
},
"modifyObstructiveCode": {
"type": "boolean",
"default": true,
"description": "Whether Cypress will search for and replace obstructive JS code found in .js or .html files that prevent Cypress from working. Please read the notes for more information on this setting. https://on.cypress.io/configuration#modifyObstructiveCode"
},
"viewportHeight": {
"type": "number",
"default": 660,
"description": "Default height in pixels for the application under tests viewport (Override with cy.viewport() command)"
},
"viewportWidth": {
"type": "number",
"default": 1000,
"description": "Default width in pixels for the application under tests viewport. (Override with cy.viewport() command)"
},
"animationDistanceThreshold": {
"type": "number",
"default": 5,
"description": "The distance in pixels an element must exceed over time to be considered animating"
},
"waitForAnimations": {
"type": "boolean",
"default": true,
"description": "Whether to wait for elements to finish animating before executing commands"
},
"scrollBehavior": {
"enum": [
false,
"center",
"top",
"bottom",
"nearest"
],
"default": "top",
"description": "Viewport position to which an element should be scrolled prior to action commands. Setting `false` disables scrolling."
},
"projectId": {
"type": "string",
"default": null,
"description": "A 6 character string use to identify this project in the Cypress Dashboard. See https://on.cypress.io/dashboard-service#Identification"
},
"nodeVersion": {
"enum": [
"system",
"bundled"
],
"default": "bundled",
"description": "If set to 'system', Cypress will try to find a Node.js executable on your path to use when executing your plugins. Otherwise, Cypress will use the Node version bundled with Cypress."
},
"experimentalSourceRewriting": {
"type": "boolean",
"default": false,
"description": "Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm."
},
"experimentalFetchPolyfill": {
"type": "boolean",
"default": false,
"description": "Polyfills `window.fetch` to enable Network spying and stubbing"
},
"experimentalStudio": {
"type": "boolean",
"default": false,
"description": "Generate and save commands directly to your test suite by interacting with your app as an end user would."
},
"retries": {
"type": [
"object",
"number",
"null"
],
"default": {
"runMode": 0,
"openMode": 0
},
"description": "The number of times to retry a failing. Can be configured to apply only in runMode or openMode"
},
"includeShadowDom": {
"type": "boolean",
"default": false,
"description": "Enables including elements within the shadow DOM when using querying commands (e.g. cy.get(), cy.find()). Can be set globally in cypress.json, per-suite or per-test in the test configuration object, or programmatically with Cypress.config()"
}
}
}
}
},
"allOf": [
{
"$ref": "#/definitions/cypressConfig"
},
{
"properties": {
"component": {
"description": "Any component runner specific overrides",
"$ref": "#/definitions/cypressConfig"
},
"e2e": {
"description": "Any e2e runner specific overrides",
"$ref": "#/definitions/cypressConfig"
}
}
}
]
}

View File

@@ -630,23 +630,25 @@ describe('cli', () => {
it('spawns server with correct args for component-testing', () => {
this.exec('open-ct --dev')
expect(spawn.start.firstCall.args[0]).to.include('--componentTesting')
expect(spawn.start.firstCall.args[0]).to.include('--testing-type')
expect(spawn.start.firstCall.args[0]).to.include('component')
})
it('runs server with correct args for component-testing', () => {
this.exec('run-ct --dev')
expect(spawn.start.firstCall.args[0]).to.include('--componentTesting')
expect(spawn.start.firstCall.args[0]).to.include('--testing-type')
expect(spawn.start.firstCall.args[0]).to.include('component')
})
it('does not display open-ct command in the help', () => {
it('does display open-ct command in the help', () => {
return execa('bin/cypress', ['help']).then((result) => {
expect(result).to.not.include('open-ct')
expect(result).to.include('open-ct')
})
})
it('does not display run-ct command in the help', () => {
it('does display run-ct command in the help', () => {
return execa('bin/cypress', ['help']).then((result) => {
expect(result).to.not.include('run-ct')
expect(result).to.include('run-ct')
})
})
})

View File

@@ -3,7 +3,6 @@ require('../spec_helper')
const os = require('os')
const tty = require('tty')
const snapshot = require('../support/snapshot')
const mockedEnv = require('mocked-env')
const supportsColor = require('supports-color')
const proxyquire = require('proxyquire')
const hasha = require('hasha')
@@ -12,9 +11,6 @@ const la = require('lazy-ass')
const util = require(`${lib}/util`)
const logger = require(`${lib}/logger`)
// https://github.com/cypress-io/cypress/issues/5431
const expectedNodeOptions = `--max-http-header-size=${1024 * 1024}`
describe('util', () => {
beforeEach(() => {
sinon.stub(process, 'exit')
@@ -217,7 +213,6 @@ describe('util', () => {
FORCE_COLOR: '1',
DEBUG_COLORS: '1',
MOCHA_COLORS: '1',
NODE_OPTIONS: expectedNodeOptions,
})
util.supportsColor.returns(false)
@@ -229,49 +224,10 @@ describe('util', () => {
FORCE_STDERR_TTY: '0',
FORCE_COLOR: '0',
DEBUG_COLORS: '0',
NODE_OPTIONS: expectedNodeOptions,
})
})
})
context('.getNodeOptions', () => {
let restoreEnv
afterEach(() => {
if (restoreEnv) {
restoreEnv()
restoreEnv = null
}
})
it('adds required NODE_OPTIONS', () => {
restoreEnv = mockedEnv({
NODE_OPTIONS: undefined,
})
expect(util.getNodeOptions({})).to.deep.eq({
NODE_OPTIONS: expectedNodeOptions,
})
})
it('includes existing NODE_OPTIONS', () => {
restoreEnv = mockedEnv({
NODE_OPTIONS: '--foo --bar',
})
expect(util.getNodeOptions({})).to.deep.eq({
NODE_OPTIONS: `${expectedNodeOptions} --foo --bar`,
ORIGINAL_NODE_OPTIONS: '--foo --bar',
})
})
it('does not return if dev is set and version < 12', () => {
expect(util.getNodeOptions({
dev: true,
}, 11)).to.be.undefined
})
})
context('.getForceTty', () => {
it('forces when each stream is a tty', () => {
sinon.stub(tty, 'isatty')

View File

@@ -1,7 +0,0 @@
import moment = require('moment')
export = Moment
export as namespace Moment
declare namespace Moment {
type MomentStatic = typeof moment
}

View File

@@ -113,7 +113,7 @@ declare namespace Cypress {
/**
* Spec type for the given test. "integration" is the default, but
* tests run using experimentalComponentTesting will be "component"
* tests run using `open-ct` will be "component"
*
* @see https://on.cypress.io/experiments
*/
@@ -177,18 +177,6 @@ declare namespace Cypress {
* @see https://on.cypress.io/minimatch
*/
minimatch: typeof Minimatch.minimatch
/**
* @deprecated Will be removed in a future version.
* Consider including your own datetime formatter in your tests.
*
* Cypress automatically includes moment.js and exposes it as Cypress.moment.
*
* @see https://on.cypress.io/moment
* @see http://momentjs.com/
* @example
* const todaysDate = Cypress.moment().format("MMM DD, YYYY")
*/
moment: Moment.MomentStatic
/**
* Cypress automatically includes Bluebird and exposes it as Cypress.Promise.
*
@@ -527,6 +515,18 @@ declare namespace Cypress {
* @see https://on.cypress.io/catalog-of-events#App-Events
*/
off: Actions
/**
* Trigger action
* @private
*/
action: (action: string, ...args: any[]) => void
/**
* Load files
* @private
*/
onSpecWindow: (window: Window, specList: string[] | Array<() => Promise<void>>) => void
}
type CanReturnChainable = void | Chainable | Promise<unknown>
@@ -1784,7 +1784,7 @@ declare namespace Cypress {
/**
* Run a task in Node via the plugins file.
*
* @see https://on.cypress.io/task
* @see https://on.cypress.io/api/task
*/
task<S = unknown>(event: string, arg?: any, options?: Partial<Loggable & Timeoutable>): Chainable<S>
@@ -1812,6 +1812,12 @@ declare namespace Cypress {
* @see https://on.cypress.io/then
*/
then<S>(options: Partial<Timeoutable>, fn: (this: ObjectLike, currentSubject: Subject) => PromiseLike<S>): Chainable<S>
/**
* Enables you to work with the subject yielded from the previous command / promise.
*
* @see https://on.cypress.io/then
*/
then<S extends HTMLElement>(fn: (this: ObjectLike, currentSubject: Subject) => S): Chainable<JQuery<S>>
/**
* Enables you to work with the subject yielded from the previous command / promise.
*
@@ -1824,6 +1830,12 @@ declare namespace Cypress {
* @see https://on.cypress.io/then
*/
then<S>(fn: (this: ObjectLike, currentSubject: Subject) => S): ThenReturn<Subject, S>
/**
* Enables you to work with the subject yielded from the previous command / promise.
*
* @see https://on.cypress.io/then
*/
then<S extends HTMLElement>(options: Partial<Timeoutable>, fn: (this: ObjectLike, currentSubject: Subject) => S): Chainable<JQuery<S>>
/**
* Enables you to work with the subject yielded from the previous command / promise.
*
@@ -1872,7 +1884,7 @@ declare namespace Cypress {
* // or use this shortcut
* cy.tick(5000).invoke('restore')
*/
tick(milliseconds: number): Chainable<Clock>
tick(milliseconds: number, options?: Partial<Loggable>): Chainable<Clock>
/**
* Get the `document.title` property of the page that is currently active.
@@ -2138,6 +2150,21 @@ declare namespace Cypress {
```
*/
writeFile<C extends FileContents>(filePath: string, contents: C, options?: Partial<WriteFileOptions>): Chainable<C>
/**
* Write to a file with the specified encoding and contents.
*
* An `encoding` option in `options` will override the `encoding` argument.
*
* @see https://on.cypress.io/writefile
```
cy.writeFile('path/to/ascii.txt', 'Hello World', 'utf8', {
flag: 'a+',
}).then((text) => {
expect(text).to.equal('Hello World') // true
})
```
*/
writeFile<C extends FileContents>(filePath: string, contents: C, encoding: Encodings, options?: Partial<WriteFileOptions>): Chainable<C>
/**
* jQuery library bound to the AUT
@@ -2495,6 +2522,11 @@ declare namespace Cypress {
* @default "cypress/plugins/index.js"
*/
pluginsFile: string | false
/**
* The application under test cannot redirect more than this limit.
* @default 20
*/
redirectionLimit: number
/**
* If `nodeVersion === 'system'` and a `node` executable is found, this will be the full filesystem path to that executable.
* @default null
@@ -2608,6 +2640,18 @@ declare namespace Cypress {
* @default false
*/
includeShadowDom: boolean
/**
* Override default config options for Component Testing runner.
* @default {}
*/
component: ResolvedConfigOptions
/**
* Override default config options for E2E Testing runner.
* @default {}
*/
e2e: ResolvedConfigOptions
}
/**
@@ -2636,10 +2680,6 @@ declare namespace Cypress {
* Path to folder containing component test files.
*/
componentFolder: string
/**
* Whether component testing is enabled.
*/
experimentalComponentTesting: boolean
/**
* Hosts mappings to IP addresses.
*/
@@ -2732,6 +2772,10 @@ declare namespace Cypress {
* Absolute path to the root of the project
*/
projectRoot: string
/**
* Type of test and associated runner that was launched.
*/
testingType: 'e2e' | 'component'
/**
* Cypress version.
*/
@@ -5160,7 +5204,7 @@ declare namespace Cypress {
*/
interface Actions {
/**
* Fires when an uncaught exception occurs in your application.
* Fires when an uncaught exception or unhandled rejection occurs in your application. If it's an unhandled rejection, the rejected promise will be the 3rd argument.
* Cypress will fail the test when this fires.
* Return `false` from this event and Cypress will not fail the test. Also useful for debugging purposes because the actual `error` instance is provided to you.
* @see https://on.cypress.io/catalog-of-events#App-Events
@@ -5186,7 +5230,7 @@ declare namespace Cypress {
})
```
*/
(action: 'uncaught:exception', fn: (error: Error, runnable: Mocha.Runnable) => false | void): Cypress
(action: 'uncaught:exception', fn: (error: Error, runnable: Mocha.Runnable, promise?: Promise<any>) => false | void): Cypress
/**
* Fires when your app calls the global `window.confirm()` method.
* Cypress will auto accept confirmations. Return `false` from this event and the confirmation will be canceled.
@@ -5425,7 +5469,7 @@ declare namespace Cypress {
allRequestResponses: any[]
body: any
duration: number
headers: { [key: string]: string }
headers: { [key: string]: string | string[] }
isOkStatusCode: boolean
redirects?: string[]
redirectedToUrl?: string

View File

@@ -9,7 +9,6 @@
/// <reference path="./cy-blob-util.d.ts" />
/// <reference path="./cy-bluebird.d.ts" />
/// <reference path="./cy-moment.d.ts" />
/// <reference path="./cy-minimatch.d.ts" />
/// <reference path="./cy-chai.d.ts" />
/// <reference path="./lodash/index.d.ts" />

View File

@@ -1,6 +1,7 @@
Cypress.on('uncaught:exception', (error, runnable) => {
Cypress.on('uncaught:exception', (error, runnable, promise) => {
error // $ExpectType Error
runnable // $ExpectType Runnable
promise // $ExpectType Promise<any> | undefined
})
Cypress.on('window:confirm', (text) => {

View File

@@ -465,6 +465,10 @@ cy.writeFile('../file.path', '', {
flag: 'a+',
encoding: 'utf-8'
})
cy.writeFile('../file.path', '', 'ascii', {
flag: 'a+',
encoding: 'utf-8'
})
cy.get('foo').click()
cy.get('foo').click({

View File

@@ -5,14 +5,6 @@ namespace CypressLodashTests {
})
}
namespace CypressMomentTests {
Cypress.moment() // $ExpectType Moment
Cypress.moment('1982-08-23') // $ExpectType Moment
Cypress.moment(Date()) // $ExpectType Moment
Cypress.moment(Date()).format() // $ExpectType string
Cypress.moment().startOf('week') // $ExpectType Moment
}
namespace CypressSinonTests {
Cypress.sinon // $ExpectType SinonStatic
@@ -236,6 +228,26 @@ describe('then', () => {
s // $ExpectType string
})
})
it('HTMLElement', () => {
cy.get('div')
.then(($div) => {
$div // $ExpectType JQuery<HTMLDivElement>
return $div[0]
})
.then(($div) => {
$div // $ExpectType JQuery<HTMLDivElement>
})
cy.get('p')
.then(($p) => {
$p // $ExpectType JQuery<HTMLParagraphElement>
return $p[0]
})
.then({timeout: 3000}, ($p) => {
$p // $ExpectType JQuery<HTMLParagraphElement>
})
})
})
cy.wait(['@foo', '@bar'])
@@ -334,14 +346,16 @@ namespace CypressAUTWindowTests {
}
namespace CypressOnTests {
Cypress.on('uncaught:exception', (error, runnable) => {
Cypress.on('uncaught:exception', (error, runnable, promise) => {
error // $ExpectType Error
runnable // $ExpectType Runnable
promise // $ExpectType Promise<any> | undefined
})
cy.on('uncaught:exception', (error, runnable) => {
cy.on('uncaught:exception', (error, runnable, promise) => {
error // $ExpectType Error
runnable // $ExpectType Runnable
promise // $ExpectType Promise<any> | undefined
})
// you can chain multiple callbacks

View File

@@ -87,7 +87,8 @@ cy.request({
}).then((resp) => {
resp // $ExpectType Response
resp.redirectedToUrl // $ExpectType string | undefined
resp.redirects // $ExpectTyep string[] | undefined
resp.redirects // $ExpectType string[] | undefined
resp.headers // $ExpectType { [key: string]: string | string[]; }
})
// specify query parameters

View File

@@ -4,7 +4,10 @@ const injectDevServer = require('@cypress/react/plugins/babel');
const something = require("something");
module.exports = (on, config) => {
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config);
}
return config; // IMPORTANT to return a config
};
`

View File

@@ -1,6 +1,5 @@
exports['Injected overridden webpack template cypress.json'] = `
{
"experimentalComponentTesting": true,
"componentFolder": "cypress/component",
"testFiles": "**/*.spec.{js,ts,jsx,tsx}"
}
@@ -10,8 +9,11 @@ exports['Injected overridden webpack template plugins/index.js'] = `
const injectDevServer = require("@cypress/react/plugins/react-scripts");
module.exports = (on, config) => {
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config);
}
return config; // IMPORTANT to return a config
};
`
@@ -22,7 +24,6 @@ import "./commands.js";
exports['injects guessed next.js template cypress.json'] = `
{
"experimentalComponentTesting": true,
"componentFolder": "src",
"testFiles": "**/*.spec.{js,ts,jsx,tsx}"
}
@@ -32,8 +33,11 @@ exports['injects guessed next.js template plugins/index.js'] = `
const injectDevServer = require("@cypress/react/plugins/next");
module.exports = (on, config) => {
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config);
}
return config; // IMPORTANT to return a config
};
`

View File

@@ -4,7 +4,10 @@ const injectDevServer = require('@cypress/react/plugins/next');
const something = require("something");
module.exports = (on, config) => {
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config);
}
return config; // IMPORTANT to return a config
};
`

View File

@@ -4,7 +4,10 @@ const injectDevServer = require('@cypress/react/plugins/react-scripts');
const something = require("something");
module.exports = (on, config) => {
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config);
}
return config; // IMPORTANT to return a config
};
`

View File

@@ -4,10 +4,14 @@ const injectDevServer = require("@cypress/react/plugins/load-webpack");
const something = require("something");
module.exports = (on, config) => {
// TODO replace with valid webpack config path
config.env.webpackFilename = './webpack.config.js';
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config, {
// TODO replace with valid webpack config path
webpackFileName: './webpack.config.js'
});
}
return config; // IMPORTANT to return a config
};
`
@@ -17,8 +21,12 @@ const injectDevServer = require("@cypress/react/plugins/load-webpack");
const something = require("something");
module.exports = (on, config) => {
config.env.webpackFilename = 'config/webpack.config.js';
injectDevServer(on, config);
return config; // IMPORTANT to return the config object
if (config.testingType === "component") {
injectDevServer(on, config, {
webpackFileName: 'config/webpack.config.js'
});
}
return config; // IMPORTANT to return a config
};
`

View File

@@ -8,14 +8,16 @@ const {
const something = require("something");
module.exports = (on, config) => {
on("dev-server:start", async options => {
return startDevServer({
options,
// TODO replace with valid rollup config path
rollupConfig: path.resolve(__dirname, 'rollup.config.js')
if (config.testingType === "component") {
on("dev-server:start", async options => {
return startDevServer({
options,
// TODO replace with valid rollup config path
rollupConfig: path.resolve(__dirname, 'rollup.config.js')
});
});
});
return config; // IMPORTANT to return the config object
return config; // IMPORTANT to return the config object
}
};
`
@@ -29,12 +31,14 @@ const {
const something = require("something");
module.exports = (on, config) => {
on("dev-server:start", async options => {
return startDevServer({
options,
rollupConfig: path.resolve(__dirname, 'config/rollup.config.js')
if (config.testingType === "component") {
on("dev-server:start", async options => {
return startDevServer({
options,
rollupConfig: path.resolve(__dirname, 'config/rollup.config.js')
});
});
});
return config; // IMPORTANT to return the config object
return config; // IMPORTANT to return the config object
}
};
`

View File

@@ -6,8 +6,10 @@ const {
const something = require("something");
module.exports = (on, config) => {
on("dev-server:start", async options => startDevServer({
options
}));
if (config.testingType === "component") {
on("dev-server:start", async options => startDevServer({
options
}));
}
};
`

View File

@@ -8,9 +8,11 @@ const webpackConfig = require("@vue/cli-service/webpack.config.js");
const something = require("something");
module.exports = (on, config) => {
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
if (config.testingType === "component") {
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
}
};
`

View File

@@ -9,10 +9,12 @@ const webpackConfig = require("./webpack.config.js"); // TODO replace with valid
const something = require("something");
module.exports = (on, config) => {
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
if (config.testingType === "component") {
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
}
};
`
@@ -26,9 +28,11 @@ const webpackConfig = require("build/webpack.config.js");
const something = require("something");
module.exports = (on, config) => {
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
if (config.testingType === "component") {
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
}
};
`

View File

@@ -8,31 +8,33 @@ const {
const something = require("something");
module.exports = (on, config) => {
/** @type import("webpack").Configuration */
const webpackConfig = {
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx']
},
mode: 'development',
devtool: false,
output: {
publicPath: '/',
chunkFilename: '[name].bundle.js'
},
// TODO: update with valid configuration for your components
module: {
rules: [{
test: /\\.(js|jsx|mjs|ts|tsx)$/,
loader: 'babel-loader',
options: {
cacheDirectory: path.resolve(__dirname, '.babel-cache')
}
}]
}
};
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
if (config.testingType === "component") {
/** @type import("webpack").Configuration */
const webpackConfig = {
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx']
},
mode: 'development',
devtool: false,
output: {
publicPath: '/',
chunkFilename: '[name].bundle.js'
},
// TODO: update with valid configuration for your components
module: {
rules: [{
test: /\\.(js|jsx|mjs|ts|tsx)$/,
loader: 'babel-loader',
options: {
cacheDirectory: path.resolve(__dirname, '.babel-cache')
}
}]
}
};
on('dev-server:start', options => startDevServer({
options,
webpackConfig
}));
}
};
`

View File

@@ -30,7 +30,7 @@
"@types/babel__core": "^7.1.2",
"@types/inquirer": "7.3.1",
"@types/mock-fs": "4.10.0",
"@types/node": "9.6.49",
"@types/node": "14.14.31",
"@types/ora": "^3.2.0",
"copy": "0.3.2",
"mocha": "7.1.1",

View File

@@ -6,8 +6,8 @@ describe('babel transform utils', () => {
context('Plugins config babel plugin', () => {
it('injects code into the plugins file based on ast', () => {
const plugin = createTransformPluginsFileBabelPlugin({
Require: babel.template.ast('require("something")'),
ModuleExportsBody: babel.template.ast('yey()'),
RequireAst: babel.template.ast('require("something")'),
IfComponentTestingPluginsAst: babel.template.ast('yey()'),
})
const output = babel.transformSync([
@@ -23,7 +23,10 @@ describe('babel transform utils', () => {
'',
'module.exports = (on, config) => {',
' on("do");',
' yey();',
'',
' if (config.testingType === "component") {',
' yey();',
' }',
'};',
].join(`\n`))
})

View File

@@ -3,7 +3,13 @@ import * as fs from 'fs-extra'
import * as babel from '@babel/core'
import * as babelTypes from '@babel/types'
export type PluginsConfigAst = Record<'Require' | 'ModuleExportsBody', ReturnType<typeof babel.template.ast>>
type AST = ReturnType<typeof babel.template.ast>
export type PluginsConfigAst = {
RequireAst: AST
IfComponentTestingPluginsAst: AST
requiresReturnConfig?: true
}
function tryRequirePrettier () {
try {
@@ -12,6 +18,13 @@ function tryRequirePrettier () {
return null
}
}
const sharedBabelOptions = {
// disable user config
configFile: false,
babelrc: false,
presets: [],
root: process.env.BABEL_TEST_ROOT, // for testing
}
async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) {
try {
@@ -21,7 +34,7 @@ async function transformFileViaPlugin (filePath: string, babelPlugin: babel.Plug
filename: path.basename(filePath),
filenameRelative: path.relative(process.cwd(), filePath),
plugins: [babelPlugin],
presets: [],
...sharedBabelOptions,
})
if (!updatedResult) {
@@ -48,11 +61,13 @@ async function transformFileViaPlugin (filePath: string, babelPlugin: babel.Plug
}
}
const returnConfigAst = babel.template.ast('return config; // IMPORTANT to return a config', { preserveComments: true })
export function createTransformPluginsFileBabelPlugin (ast: PluginsConfigAst): babel.PluginObj {
return {
visitor: {
Program: (path) => {
path.unshiftContainer('body', ast.Require)
path.unshiftContainer('body', ast.RequireAst)
},
Function: (path) => {
if (!babelTypes.isAssignmentExpression(path.parent)) {
@@ -80,7 +95,24 @@ export function createTransformPluginsFileBabelPlugin (ast: PluginsConfigAst): b
path.parent.right.params.push(babelTypes.identifier('config'))
}
path.get('body').pushContainer('body' as never, ast.ModuleExportsBody)
const statementToInject = Array.isArray(ast.IfComponentTestingPluginsAst)
? ast.IfComponentTestingPluginsAst
: [ast.IfComponentTestingPluginsAst]
const ifComponentMode = babelTypes.ifStatement(
babelTypes.binaryExpression(
'===',
babelTypes.identifier('config.testingType'),
babelTypes.stringLiteral('component'),
),
babelTypes.blockStatement(statementToInject as babelTypes.Statement[] | babelTypes.Statement[]),
)
path.get('body').pushContainer('body' as never, ifComponentMode as babel.Node)
if (ast.requiresReturnConfig) {
path.get('body').pushContainer('body' as never, returnConfigAst)
}
}
},
},
@@ -102,7 +134,7 @@ export async function getPluginsSourceExample (ast: PluginsConfigAst) {
const babelResult = await babel.transformAsync(exampleCode, {
filename: 'nothing.js',
plugins: [createTransformPluginsFileBabelPlugin(ast)],
presets: [],
...sharedBabelOptions,
})
if (!babelResult?.code) {

View File

@@ -32,6 +32,8 @@ describe('init component tests script', () => {
await fs.remove(e2eTestOutputPath)
await fs.mkdir(e2eTestOutputPath)
process.env.BABEL_TEST_ROOT = e2eTestOutputPath
})
afterEach(() => {
@@ -170,7 +172,7 @@ describe('init component tests script', () => {
createTempFiles({
'/cypress.json': '{}',
'/webpack.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { react: '*', vue: '*' } }),
'/package.json': JSON.stringify({ dependencies: { react: '*', vue: '^2.4.5' } }),
})
promptSpy = sinon.stub(inquirer, 'prompt')
@@ -187,12 +189,44 @@ describe('init component tests script', () => {
await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true })
expect(
someOfSpyCallsIncludes(global.console.log, `It looks like all these frameworks: ${chalk.yellow('react, vue')} are available from this directory.`),
someOfSpyCallsIncludes(global.console.log, `It looks like all these frameworks: ${chalk.yellow('react, vue@2')} are available from this directory.`),
).to.be.true
})
it('installs the right adapter', () => {
it('installs the right adapter', async () => {
createTempFiles({
'/cypress.json': '{}',
'/webpack.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { react: '16.4.5' } }),
})
promptSpy = sinon.stub(inquirer, 'prompt')
.onCall(0)
.returns(Promise.resolve({
chosenTemplateName: 'vite',
componentFolder: 'src',
}) as any)
await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true })
expect(execStub).to.be.calledWith('yarn add @cypress/react --dev')
})
it('installs the right adapter for vue 3', async () => {
createTempFiles({
'/cypress.json': '{}',
'/vite.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { vue: '^3.0.0' } }),
})
promptSpy = sinon.stub(inquirer, 'prompt')
.onCall(0)
.returns(Promise.resolve({
chosenTemplateName: 'vite',
componentFolder: 'src',
}) as any)
await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true })
expect(execStub).to.be.calledWith('yarn add @cypress/vue@3 --dev')
})
it('suggest the right instruction based on user template choice', async () => {
@@ -240,7 +274,6 @@ describe('init component tests script', () => {
expect(injectedCode).to.equal(JSON.stringify(
{
experimentalComponentTesting: true,
componentFolder: 'cypress/component',
testFiles: '**/*.spec.{js,ts,jsx,tsx}',
},
@@ -273,4 +306,36 @@ describe('init component tests script', () => {
),
).to.be.true
})
it('Doesn\'t affect injected code if user has custom babel.config.js', async () => {
createTempFiles({
'/cypress/plugins/index.js': 'module.exports = (on, config) => {}',
'/cypress.json': '{}',
'babel.config.js': `module.exports = ${JSON.stringify({
presets: [
'@babel/preset-env',
],
})}`,
'/package.json': JSON.stringify({
dependencies: {
babel: '*',
react: '^16.0.0',
},
}),
})
sinon.stub(inquirer, 'prompt').returns(Promise.resolve({
chosenTemplateName: 'create-react-app',
componentFolder: 'cypress/component',
}) as any)
await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true })
const babelPluginsOutput = await fs.readFile(
path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'),
'utf-8',
)
expect(babelPluginsOutput).not.to.contain('use strict')
expect(babelPluginsOutput).to.contain('module.exports = (on, config) => {')
})
})

View File

@@ -56,7 +56,6 @@ async function injectAndShowCypressJsonConfig (
componentFolder: string,
) {
const configToInject = {
experimentalComponentTesting: true,
componentFolder,
testFiles: '**/*.spec.{js,ts,jsx,tsx}',
}

View File

@@ -6,8 +6,9 @@ import { installDependency } from '../utils'
async function guessOrAskForFramework (cwd: string): Promise<'react' | 'vue'> {
// please sort this alphabetically
const frameworks = {
react: () => scanFSForAvailableDependency(cwd, ['react', 'react-dom']),
vue: () => scanFSForAvailableDependency(cwd, ['vue']),
react: () => scanFSForAvailableDependency(cwd, { react: '*', 'react-dom': '*' }),
'vue@2': () => scanFSForAvailableDependency(cwd, { vue: '2.x' }),
'vue@3': () => scanFSForAvailableDependency(cwd, { vue: '3.x' }),
}
const guesses = Object.keys(frameworks).filter((framework) => {

View File

@@ -34,11 +34,11 @@ export const RollupTemplate: Template<{ rollupConfigPath: string }> = {
: 'rollup.config.js'
return {
Require: babel.template.ast([
RequireAst: babel.template.ast([
'const path = require("path")',
'const { startDevServer } = require("@cypress/rollup-dev-server")',
].join('\n')),
ModuleExportsBody: babel.template.ast([
IfComponentTestingPluginsAst: babel.template.ast([
`on("dev-server:start", async (options) => {`,
` return startDevServer({`,
` options,`,

View File

@@ -10,17 +10,17 @@ export const ViteTemplate: Template = {
dependencies: ['@cypress/vite-dev-server'],
getPluginsCodeAst: () => {
return {
Require: babel.template.ast(
RequireAst: babel.template.ast(
'const { startDevServer } = require("@cypress/vite-dev-server");',
),
ModuleExportsBody: babel.template.ast([
IfComponentTestingPluginsAst: babel.template.ast([
'on("dev-server:start", async (options) => startDevServer({ options }))',
].join('\n'), { preserveComments: true }),
}
},
test: (root) => {
return {
success: scanFSForAvailableDependency(root, ['vite']),
success: scanFSForAvailableDependency(root, { vite: '*' }),
}
},
}

View File

@@ -15,10 +15,10 @@ export const BabelTemplate: Template = {
getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/babel',
getPluginsCodeAst: () => {
return {
Require: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/babel\')'),
ModuleExportsBody: babel.template.ast([
requiresReturnConfig: true,
RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/babel\')'),
IfComponentTestingPluginsAst: babel.template.ast([
'injectDevServer(on, config)',
'return config // IMPORTANT to return the config object',
].join('\n'), { preserveComments: true }),
}
},

View File

@@ -13,10 +13,10 @@ export const NextTemplate: Template = {
dependencies: ['@cypress/webpack-dev-server'],
getPluginsCodeAst: () => {
return {
Require: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/next\')'),
ModuleExportsBody: babel.template.ast([
requiresReturnConfig: true,
RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/next\')'),
IfComponentTestingPluginsAst: babel.template.ast([
'injectDevServer(on, config)',
'return config // IMPORTANT to return the config object',
].join('\n'), { preserveComments: true }),
}
},

View File

@@ -16,10 +16,10 @@ export const ReactScriptsTemplate: Template = {
},
getPluginsCodeAst: () => {
return {
Require: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/react-scripts\')'),
ModuleExportsBody: babel.template.ast([
requiresReturnConfig: true,
RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/react-scripts\')'),
IfComponentTestingPluginsAst: babel.template.ast([
'injectDevServer(on, config)',
'return config // IMPORTANT to return the config object',
].join('\n'), { preserveComments: true }),
}
},

View File

@@ -18,14 +18,15 @@ export const WebpackTemplate: Template<{ webpackConfigPath: string }> = {
: './webpack.config.js'
return {
Require: babel.template.ast('const injectDevServer = require("@cypress/react/plugins/load-webpack")'),
ModuleExportsBody: babel.template.ast([
requiresReturnConfig: true,
RequireAst: babel.template.ast('const injectDevServer = require("@cypress/react/plugins/load-webpack")'),
IfComponentTestingPluginsAst: babel.template.ast([
'injectDevServer(on, config, {',
includeWarnComment
? '// TODO replace with valid webpack config path'
? ' // TODO replace with valid webpack config path'
: '',
`config.env.webpackFilename = '${webpackConfigPath}'`,
'injectDevServer(on, config)',
'return config // IMPORTANT to return the config object',
` webpackFileName: '${webpackConfigPath}'`,
'})',
].join('\n'), { preserveComments: true }),
}
},

View File

@@ -15,11 +15,11 @@ export const WebpackOptions: Template = {
dependencies: ['webpack', '@cypress/webpack-dev-server'],
getPluginsCodeAst: () => {
return {
Require: babel.template.ast([
RequireAst: babel.template.ast([
'const path = require("path")',
'const { startDevServer } = require("@cypress/webpack-dev-Server")',
].join('\n')),
ModuleExportsBody: babel.template.ast(
IfComponentTestingPluginsAst: babel.template.ast(
fs.readFileSync(path.resolve(__dirname, 'webpack-options-module-exports.template.js'), { encoding: 'utf-8' }),
{ preserveComments: true },
),

View File

@@ -10,17 +10,17 @@ export const VueCliTemplate: Template = {
dependencies: ['@cypress/webpack-dev-server'],
getPluginsCodeAst: () => {
return {
Require: babel.template.ast([
RequireAst: babel.template.ast([
'const { startDevServer } = require("@cypress/webpack-dev-server")',
`const webpackConfig = require("@vue/cli-service/webpack.config.js")`,
].join('\n')),
ModuleExportsBody: babel.template.ast([
IfComponentTestingPluginsAst: babel.template.ast([
`on('dev-server:start', (options) => startDevServer({ options, webpackConfig }))`,
].join('\n'), { preserveComments: true }),
}
},
test: (root) => {
const hasVueCliService = scanFSForAvailableDependency(root, ['@vue/cli-service'])
const hasVueCliService = scanFSForAvailableDependency(root, { '@vue/cli-service': '>=4' })
return {
success: hasVueCliService,

View File

@@ -16,7 +16,7 @@ export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = {
: './webpack.config.js'
return {
Require: babel.template.ast([
RequireAst: babel.template.ast([
'const { startDevServer } = require("@cypress/webpack-dev-server")',
`const webpackConfig = require("${webpackConfigPath}")`,
@@ -24,7 +24,7 @@ export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = {
? '// TODO replace with valid webpack config path'
: '',
].join('\n'), { preserveComments: true }),
ModuleExportsBody: babel.template.ast([
IfComponentTestingPluginsAst: babel.template.ast([
`on('dev-server:start', (options) => startDevServer({ options, webpackConfig }))`,
].join('\n')),
}

View File

@@ -1,4 +1,4 @@
export const MIN_SUPPORTED_VERSION = {
'react-scripts': '^=3.x || ^=4.x',
next: '^=9.x',
next: '^=9.x || ^=10.x',
}

View File

@@ -1,6 +1,7 @@
import path from 'path'
import fs from 'fs'
import findUp from 'find-up'
import { validateSemverVersion } from './utils'
type PackageJsonLike = {
name?: string
@@ -91,7 +92,7 @@ export function createFindPackageJsonIterator (rootPath = process.cwd()) {
}
}
export function scanFSForAvailableDependency (cwd: string, deps: string[]) {
export function scanFSForAvailableDependency (cwd: string, lookingForDeps: Record<string, string>) {
const { success } = createFindPackageJsonIterator(cwd)
.map(({ dependencies, devDependencies }, path) => {
if (!dependencies && !devDependencies) {
@@ -99,8 +100,13 @@ export function scanFSForAvailableDependency (cwd: string, deps: string[]) {
}
return {
success: Object.keys({ ...dependencies, ...devDependencies })
.some((dependency) => deps.includes(dependency)),
success: Object.entries({ ...dependencies, ...devDependencies })
.some(([dependency, version]) => {
return (
Boolean(lookingForDeps[dependency])
&& validateSemverVersion(version, lookingForDeps[dependency], dependency)
)
}),
}
})

View File

@@ -38,7 +38,7 @@ async function shouldUseYarn () {
}
function shouldUseTypescript () {
return scanFSForAvailableDependency(process.cwd(), ['typescript'])
return scanFSForAvailableDependency(process.cwd(), { typescript: '*' })
}
async function askForComponentTesting () {
@@ -51,16 +51,22 @@ async function askForComponentTesting () {
return shouldSetupComponentTesting
}
function printCypressCommandsHelper ({ useYarn }: { useYarn: boolean }) {
const displayedCommand = useYarn ? 'yarn' : 'npx'
function printCypressCommandsHelper (options: { shouldSetupComponentTesting: boolean, useYarn: boolean }) {
const printCommand = (command: string, description: string) => {
const displayedRunner = options.useYarn ? 'yarn' : 'npx'
console.log('Inside this directory, you can run several commands:')
console.log()
console.log(chalk.cyan(` ${displayedCommand} cypress open`))
console.log(' Opens cypress local development app.')
console.log()
console.log(chalk.cyan(` ${displayedCommand} cypress run`))
console.log(' Runs tests in headless mode.')
console.log()
console.log(chalk.cyan(` ${displayedRunner} ${command}`))
console.log(` ${description}`)
}
printCommand('cypress open', 'Opens cypress local development app.')
printCommand('cypress run', 'Runs tests in headless mode.')
if (options.shouldSetupComponentTesting) {
printCommand('cypress open-ct', 'Opens cypress component-testing web app.')
printCommand('cypress run', 'Runs component testing in headless mode.')
}
}
export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExamples }: MainArgv) {
@@ -92,7 +98,7 @@ export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExa
}
console.log(`\n👍 Success! Cypress is installed and ready to run tests.`)
printCypressCommandsHelper({ useYarn })
printCypressCommandsHelper({ useYarn, shouldSetupComponentTesting })
console.log(`\nHappy testing with ${chalk.green('cypress.io')} 🌲\n`)
}

View File

@@ -1,47 +0,0 @@
{
"plugins": [
"cypress",
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
"plugin:@cypress/dev/tests",
"plugin:@cypress/dev/react",
"../../packages/reporter/src/.eslintrc.json"
],
"parser": "@typescript-eslint/parser",
"env": {
"cypress/globals": true
},
"rules": {
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".js",
".jsx",
".tsx"
]
}
]
},
"overrides": [
{
"files": [
"lib/*"
],
"rules": {
"no-console": 1
}
},
{
"files": [
"**/*.json"
],
"rules": {
"quotes": "off",
"comma-dangle": "off"
}
}
]
}

View File

@@ -0,0 +1,137 @@
{
"plugins": [
"cypress",
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
"plugin:@cypress/dev/tests",
"plugin:@cypress/dev/react",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"../../packages/reporter/src/.eslintrc.json"
],
"parser": "@typescript-eslint/parser",
"env": {
"cypress/globals": true
},
"rules": {
"react/display-name": "off",
"react/function-component-definition": [
"error",
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
],
"react/jsx-boolean-value": [
"error",
"always"
],
"react/jsx-closing-bracket-location": [
"error",
"line-aligned"
],
"react/jsx-closing-tag-location": "error",
"react/jsx-curly-brace-presence": [
"error",
{
"props": "never",
"children": "never"
}
],
"react/jsx-curly-newline": "error",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".js",
".jsx",
".tsx"
]
}
],
"react/jsx-first-prop-new-line": "error",
"react/jsx-max-props-per-line": [
"error",
{
"maximum": 1,
"when": "multiline"
}
],
"react/jsx-no-bind": [
"error",
{
"ignoreDOMComponents": true
}
],
"react/jsx-no-useless-fragment": "error",
"react/jsx-one-expression-per-line": [
"error",
{
"allow": "literal"
}
],
"react/jsx-sort-props": [
"error",
{
"callbacksLast": true,
"ignoreCase": true,
"noSortAlphabetically": true,
"reservedFirst": true
}
],
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always"
}
],
"react/jsx-wrap-multilines": [
"error",
{
"declaration": "parens-new-line",
"assignment": "parens-new-line",
"return": "parens-new-line",
"arrow": "parens-new-line",
"condition": "parens-new-line",
"logical": "parens-new-line",
"prop": "parens-new-line"
}
],
"react/no-array-index-key": "error",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"quote-props": [
"error",
"as-needed"
]
},
"overrides": [
{
"files": [
"lib/*"
],
"rules": {
"no-console": 1
}
},
{
"files": [
"**/*.json"
],
"rules": {
"quotes": "off",
"comma-dangle": "off"
}
},
{
"files": "*.spec.tsx",
"rules": {
"no-unused-vars": "off",
"react/jsx-no-bind": "off"
}
}
]
}

View File

@@ -1,4 +1,5 @@
module.exports = {
plugins: ['@babel/plugin-proposal-optional-chaining'],
presets: [
'@babel/preset-env',
'@babel/preset-react',

View File

@@ -12,7 +12,5 @@
"**/__image_snapshots__/*"
],
"componentFolder": "src",
"experimentalComponentTesting": true,
"experimentalFetchPolyfill": true,
"fixturesFolder": false
}
}

View File

@@ -1,72 +1,11 @@
// @ts-check
const { startDevServer } = require('@cypress/webpack-dev-server')
const path = require('path')
const babelConfig = require('../../babel.config.js')
/** @type import("webpack").Configuration */
const webpackConfig = {
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.scss', '.css'],
},
mode: 'development',
devtool: false,
output: {
publicPath: '/',
chunkFilename: '[name].bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx|mjs|ts|tsx)$/,
loader: 'babel-loader',
options: { ...babelConfig, cacheDirectory: path.resolve(__dirname, '..', '..', '.babel-cache') },
},
{
test: /\.modules?\.s[ac]ss$/i,
exclude: [/node_modules/],
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
'sass-loader',
],
},
{
test: /\.s[ac]ss$/i,
exclude: [/node_modules/, /\.modules?\.s[ac]ss$/i],
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
// some of our examples import SVG
test: /\.svg$/,
loader: 'svg-url-loader',
},
{
// some of our examples import SVG
test: /\.svg$/,
loader: 'svg-url-loader',
},
{
test: /\.(png|jpg)$/,
use: ['file-loader'],
},
{
test: /\.(svg|eot|woff|woff2|ttf)$/,
use: ['file-loader'],
},
],
},
}
const { startDevServer } = require('@cypress/vite-dev-server')
/**
* @type Cypress.PluginConfig
*/
module.exports = (on, config) => {
on('dev-server:start', (options) => startDevServer({ options, webpackConfig, disableLazyCompilation: false }))
on('dev-server:start', (options) => startDevServer({ options }))
return config
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AUT Frame</title>
</head>
<body>
</body>
</html>

View File

@@ -7,9 +7,9 @@
"build": "rimraf dist && yarn rollup -c rollup.config.js",
"build-prod": "yarn build",
"cy:open": "node ../../scripts/cypress.js open-ct --project ${PWD}",
"cy:open:debug": "NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}",
"cy:open:debug": "node --inspect-brk ../../scripts/start.js --component-testing --project ${PWD}",
"cy:run": "node ../../scripts/cypress.js run-ct --project ${PWD}",
"cy:run:debug": "NODE_OPTIONS=--max-http-header-size=1048576 node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}",
"cy:run:debug": "node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}",
"pretest": "yarn transpile",
"test": "yarn cy:run",
"transpile": "tsc",
@@ -21,42 +21,40 @@
"@fortawesome/free-brands-svg-icons": "5.15.2",
"@fortawesome/free-solid-svg-icons": "5.15.2",
"@fortawesome/react-fontawesome": "0.1.14",
"@iconify/icons-vscode-icons": "1.1.1",
"@iconify/react": "2.0.0-rc.8",
"classnames": "2.2.6",
"debug": "4.3.2"
},
"devDependencies": {
"@babel/core": "7.4.5",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/preset-env": "7.4.5",
"@babel/preset-react": "7.0.0",
"@babel/preset-typescript": "7.10.4",
"@cypress/react": "0.0.0-development",
"@cypress/webpack-dev-server": "0.0.0-development",
"@cypress/vite-dev-server": "0.0.0-development",
"@percy/cypress": "2.3.2",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-image": "2.0.6",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.1.1",
"@types/node": "12.12.50",
"@types/node": "14.14.31",
"@types/semver": "7.3.4",
"babel-loader": "8.0.6",
"css-loader": "2.1.1",
"cypress": "0.0.0-development",
"cypress-real-events": "1.1.0",
"mocha": "^7.0.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"react": "16.8.6",
"react-dom": "16.8.6",
"rollup": "^2.38.5",
"rollup-plugin-copy-assets": "2.0.3",
"rollup-plugin-peer-deps-external": "2.2.4",
"rollup-plugin-postcss-modules": "2.0.2",
"rollup-plugin-typescript2": "^0.29.0",
"sass": "1.32.8",
"sass-loader": "10.1.1",
"style-loader": "0.23.1",
"svg-url-loader": "3.0.3",
"typescript": "4.0.3",
"webpack": "4.44.1"
"vite": "2.1.3"
},
"peerDependencies": {
"react": "^=16.x || ^=17.x",
@@ -90,4 +88,4 @@
"expect"
]
}
}
}

View File

@@ -6,6 +6,7 @@ import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import postcss from 'rollup-plugin-postcss-modules'
import pkg from './package.json'
import image from '@rollup/plugin-image'
import copy from 'rollup-plugin-copy-assets'
const banner = `
/**
@@ -29,7 +30,17 @@ function createEntry (options) {
'react-dom',
],
plugins: [
peerDepsExternal(), resolve(), json(), commonjs(), postcss({ writeDefinitions: false }), image(),
peerDepsExternal(),
resolve(),
json(),
commonjs(),
postcss({ writeDefinitions: false }),
image(),
copy({
assets: [
'./index.scss',
],
}),
],
output: {
banner,

View File

@@ -1,4 +1,6 @@
import React from 'react'
import { button } from './Button.module.scss'
export const Button = () => (<button className={button}>Hello World</button>)
export const Button = () => (
<button className={button}>Hello World</button>
)

View File

@@ -16,8 +16,10 @@ interface LogoProps {
export const CypressLogo: React.FC<LogoProps> = (props) => {
return (
<img className={styles.logo}
<img
className={styles.logo}
style={{ width: sizes[props.size] }}
src={LogoPng} />
src={LogoPng}
/>
)
}

View File

@@ -1,48 +0,0 @@
@use '../../index.scss' as *;
.nav {
user-select: none;
white-space: nowrap;
}
.ul {
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 0;
&:before {
display: none;
}
}
.li.li {
padding-left: 20px;
}
.ul, .li {
position: relative;
list-style: none;
font-size: $text-s;
line-height: 1.6;
}
.a {
position: relative;
color: unset;
text-decoration: none;
display: inline-block;
width: 100%;
&:hover {
cursor: pointer;
}
}
.ul .ul {
margin-inline-start: $text-xs;
}
.isSelected, .isSelected:hover {
text-decoration: underline;
}

View File

@@ -1,130 +0,0 @@
import { mount } from '@cypress/react'
import React from 'react'
import { FileExplorer, FileComponentProps, FolderComponentProps } from './FileExplorer'
import { FileNode, makeFileHierarchy, TreeNode } from './helpers/makeFileHierarchy'
import styles from './FileExplorer.module.scss'
const specs: Cypress.Cypress['spec'][] = [
{
relative: 'foo/bar/foo.spec.js',
absolute: 'Users/code/foo/bar/foo.spec.js',
name: 'foo/bar/foo.spec.js',
},
{
relative: 'bar/foo.spec.tsx',
absolute: 'bar/foo.spec.tsx',
name: 'bar/foo.spec.tsx',
},
{
relative: 'merp/map.spec.ts',
absolute: 'merp/map.spec.ts',
name: 'merp/map.spec.ts',
},
]
interface FileExplorerTestProps {
clickFileStub: typeof cy.stub
clickFolderStub: typeof cy.stub
}
function createFileExplorer (testProps: FileExplorerTestProps): React.FC {
return () => {
const [selectedFile, setSelectedFile] = React.useState<string>()
const onFileClick = (file: FileNode) => {
setSelectedFile(file.absolute)
}
const files = makeFileHierarchy(specs.map((spec) => spec.relative))
const FileComponent: React.FC<FileComponentProps> = (props) => {
return (
<div onClick={() => {
testProps.clickFileStub(props.item)
props.onClick(props.item)
}}>
{props.item.name}
</div>
)
}
const FolderComponent: React.FC<FolderComponentProps> = (props) => {
return (
<div onClick={() => {
testProps.clickFolderStub()
props.onClick()
}}>
{props.item.name}
</div>
)
}
return (
<FileExplorer
files={files}
cssModule={styles}
selectedFile={selectedFile}
fileComponent={FileComponent}
folderComponent={FolderComponent}
onFileClick={onFileClick}
/>
)
}
}
describe('FileExplorer', () => {
it('basic usage', () => {
const files: TreeNode[] = [
{
type: 'folder',
name: 'foo',
absolute: 'foo',
files: [
{
type: 'file',
name: 'bar.js',
absolute: 'foo/bar.js',
},
],
},
]
const FileComponent: React.FC<FileComponentProps> = (props) => <div>{props.item.name}</div>
const FolderComponent: React.FC<FolderComponentProps> = (props) => <div>{props.item.name}</div>
mount(
<FileExplorer
files={files}
selectedFile={undefined}
fileComponent={FileComponent}
folderComponent={FolderComponent}
onFileClick={() => {}}
/>,
)
})
it('clicks file and folders', () => {
const clickFolderStub = cy.stub()
const clickFileStub = cy.stub()
const Wrapper = createFileExplorer({
clickFolderStub,
clickFileStub,
})
mount(<Wrapper />)
cy.get('div').contains('bar').click().then(() => {
expect(clickFolderStub).to.have.been.calledWith()
})
cy.get('div').contains('map.spec.ts').click().then(() => {
expect(clickFileStub).to.have.been.calledWith({
type: 'file',
absolute: 'merp/map.spec.ts',
name: 'map.spec.ts',
})
})
})
})

View File

@@ -1,244 +0,0 @@
import React from 'react'
import cs from 'classnames'
import { FileNode, FolderNode, TreeNode } from './helpers/makeFileHierarchy'
export { FileNode, FolderNode, TreeNode }
export interface FolderComponentProps {
item: FolderNode
depth: number
isOpen: boolean
onClick: () => void
}
export interface FileComponentProps {
item: FileNode
depth: number
onClick: (file: FileNode) => void
}
export interface FileExplorerProps extends React.HTMLAttributes<HTMLDivElement> {
files: TreeNode[]
fileComponent: React.FC<FileComponentProps>
folderComponent: React.FC<FolderComponentProps>
selectedFile?: string
searchInput?: JSX.Element
onFileClick: (file: FileNode) => void
// Styles. They should be a *.module.scss.
// TODO: Can we type these? Do we want to couple to CSS modules?
cssModule?: {
nav: any
ul: any
li: any
a: any
isSelected: any
}
}
export interface FileTreeProps extends FileExplorerProps {
depth: number
openFolders: Record<string, boolean>
style?: React.CSSProperties
setSelectedFile: (absolute: string) => void
}
export const FileExplorer: React.FC<FileExplorerProps> = (props) => {
/**
* Whether a folder is open or not is a **UI** concern.
* From a file system point of view, there is no such concept as "open" or "closed",
* only from a user's point of view.
* For this reason we save the open state as part of the UI component. The easiest
* way to do this is a key/value pair, mapping the absolute path of a directory to a boolean
*
* {
* 'foo': true,
* 'foo/bar': true
* 'foo/bar/qux': false
* }
*
* Every directory is set to open by default. When you add a new directory
* or file via your file system (eg mkdir foo/bar && touch foo/bar/hello.js) it will be added
* without losing the current state of open/closed directories.
*/
const [openFolders, setOpenFolders] = React.useState<Record<string, boolean>>({})
React.useLayoutEffect(() => {
const openFoldersTmp:Record<string, boolean> = {}
function walk (nodes: TreeNode[]) {
for (const node of nodes) {
if (node.type === 'folder') {
// only update with newly created folders.
// we want to maintain the current state (open/closed) of existing folders.
if (!(node.absolute in openFoldersTmp)) {
openFoldersTmp[node.absolute] = true
}
walk(node.files)
}
}
}
walk(props.files)
setOpenFolders(openFoldersTmp)
}, [props.files])
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const files: TreeNode[] = []
function flatten (nodes: TreeNode[]) {
for (const node of nodes) {
if (node.type === 'folder') {
// only update with newly created folders.
// we want to maintain the current state (open/closed) of existing folders.
if (openFolders[node.absolute]) {
files.push(node)
flatten(node.files)
} else {
files.push(node)
}
} else {
files.push(node)
}
}
}
flatten(props.files)
const selectSpecByIndex = (index: number) => {
const file = typeof index !== 'number' || index < 0
? files[0]
: files[index]
const specElement = document.querySelector(`[data-item="${file.absolute}"]`) as HTMLDivElement
if (specElement) {
specElement.focus()
}
}
const selectedSpecIndex = files.findIndex((file) => {
return file.absolute === (document.activeElement as HTMLElement).dataset.item
})
if (e.key === 'Enter') {
const selected = files[selectedSpecIndex]
if (selected.type === 'file') {
// Run the spec.
props.onFileClick(selected)
}
// Toggle the folder open/closed.
return setSelectedFile(selected.absolute)
}
if (e.key === 'ArrowUp') {
return selectSpecByIndex(selectedSpecIndex - 1)
}
if (e.key === 'ArrowDown') {
return selectSpecByIndex(selectedSpecIndex + 1)
}
}
const setSelectedFile = (absolute: string) => {
setOpenFolders({ ...openFolders, [absolute]: !openFolders[absolute] })
}
return (
<nav
className={cs(props.className, props.cssModule && props.cssModule.nav)}
onKeyDown={handleKeyDown}
data-cy='specs-list'
>
<FileTree
{...props}
setSelectedFile={setSelectedFile}
openFolders={openFolders}
depth={0}
/>
</nav>
)
}
export const FileTree: React.FC<FileTreeProps> = (props) => {
// Negative margins let the <a> tag take full width (a11y)
// while the <li> tag with text content can be positioned relatively
// This gives us HTML + cssModule-only highlight and click handling
const fileTree = (item: TreeNode) => {
if (item.type !== 'folder') {
return
}
return (
<FileTree
fileComponent={props.fileComponent}
folderComponent={props.folderComponent}
openFolders={props.openFolders}
setSelectedFile={props.setSelectedFile}
onFileClick={props.onFileClick}
selectedFile={props.selectedFile}
depth={props.depth + 1}
cssModule={props.cssModule}
files={props.openFolders[item.absolute] ? item.files : []}
/>
)
}
const renderFolder = (item: FolderNode) => {
return (
<props.folderComponent
depth={props.depth}
item={item}
isOpen={props.openFolders[item.absolute]}
onClick={() => props.setSelectedFile(item.absolute)}
/>
)
}
const renderFile = (item: FileNode) => {
return (
<props.fileComponent
depth={props.depth}
item={item}
onClick={props.onFileClick}
/>
)
}
return (
<>
{props.searchInput}
<ul className={props.cssModule && props.cssModule.ul}>
{
props.files.map((item) => {
return (
<React.Fragment key={item.absolute}>
<a
data-item={item.absolute}
style={{
marginLeft: `-${20 * props.depth}px`,
width: `calc(100% + (20px * ${props.depth}))`,
}}
className={cs(props.cssModule && props.cssModule.a, {
[props.cssModule && props.cssModule.isSelected]: item.absolute === props.selectedFile,
})}
tabIndex={0}
>
<li
style={{ ...props.style, marginLeft: `${20 * props.depth}px` }}
className={props.cssModule && props.cssModule.li}>
{item.type === 'folder' ? renderFolder(item) : renderFile(item)}
</li>
</a>
{fileTree(item)}
</React.Fragment>
)
})
}
</ul>
</>
)
}

View File

@@ -59,7 +59,8 @@ describe('LeftNav', () => {
href: '#foo',
},
},
]} />)
]}
/>)
cy.get('a').first().eq(0).click().url().should('include', '#foo')
})
@@ -71,7 +72,8 @@ describe('LeftNav', () => {
<div style={{
height: 1000,
width: 1000,
}}>
}}
>
This is the main page content
</div>
</div>

View File

@@ -54,8 +54,8 @@ export const LeftNav: React.FC<LeftNavProps> = ({ items, activeIndex, leftNavCla
const topNav = (
<nav
className={styles.top}
key='nav-section-top'
className={styles.top}
>
{mappedItems.top.map((item) => navItem(item))}
</nav>
@@ -63,8 +63,8 @@ export const LeftNav: React.FC<LeftNavProps> = ({ items, activeIndex, leftNavCla
const bottomNav = (
<nav
className={styles.bottom}
key='nav-section-bottom'
className={styles.bottom}
>
{mappedItems.bottom.map((item) => navItem(item))}
</nav>

View File

@@ -12,13 +12,15 @@ library.add(fab)
describe('Playground', () => {
it('cypress logo', () => {
mount(<>
<CypressLogo size="small" />
<br/>
<CypressLogo size="medium" />
<br/>
<CypressLogo size="large" />
</>)
mount(
<>
<CypressLogo size="small" />
<br />
<CypressLogo size="medium" />
<br />
<CypressLogo size="large" />
</>,
)
})
it('search input', () => {
@@ -26,26 +28,31 @@ describe('Playground', () => {
const [value, setValue] = React.useState(props.value || '')
const inputRef = React.useRef<HTMLInputElement>(null)
return (<SearchInput
prefixIcon={props.prefixIcon}
onSuffixClicked={() => {
setValue('')
inputRef.current.focus()
}}
placeholder={props.placeholder}
inputRef={inputRef}
value={value}
onChange={(event) => setValue(event.target.value)}>
</SearchInput>)
return (
<SearchInput
prefixIcon={props.prefixIcon}
placeholder={props.placeholder}
inputRef={inputRef}
value={value}
onSuffixClicked={() => {
setValue('')
inputRef.current.focus()
}}
onChange={(event) => setValue(event.target.value)}
>
</SearchInput>
)
}
mount(<>
<Wrapper placeholder="Find components..." prefixIcon="search" />
<br/>
{/* <Wrapper placeholder="Find components..." prefixIcon="coffee"/> */}
<br/>
{/* <Wrapper placeholder="Find components..." prefixIcon="search" suffixIcon="times"/> */}
</>)
mount(
<>
<Wrapper placeholder="Find components..." prefixIcon="search" />
<br />
{/* <Wrapper placeholder="Find components..." prefixIcon="coffee"/> */}
<br />
{/* <Wrapper placeholder="Find components..." prefixIcon="search" suffixIcon="times"/> */}
</>,
)
cy.get('input').should('exist')
cy.get('input').should('exist').first()

View File

@@ -2,9 +2,7 @@
$active-color: $metal-50;
$inactive-color: $metal-20;
$input-color: $metal-50;
$input-width: 120px;
@mixin quick-transition($param) {
transition: $param 0.15s ease-in-out;
@@ -16,13 +14,12 @@ $input-width: 120px;
color: $input-color;
padding: 0 $text-m 0 $text-xxs;
width: 100%;
max-width: $input-width;
border: unset;
outline: unset;
border-bottom: 1px solid $inactive-color;
&.hasPrefix {
padding: 0 $text-m;
padding: 0.2rem $text-m;
}
}
@@ -45,11 +42,14 @@ $icon-margin: calc(#{$icon-size} / 3);
.suffix {
margin: $icon-margin $icon-negative-margin;
right: 20px;
}
.inputButton {
position: relative;
display: flex;
margin: 1rem;
align-items: center;
&:active, &:focus-within {
.searchInput {

View File

@@ -12,6 +12,8 @@ interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
export const SearchInput: React.FC<SearchInputProps> = (props) => {
const { onSuffixClicked } = props
const prefixIcon = props.prefixIcon && (
<FontAwesomeIcon
className={styles.prefix}
@@ -19,27 +21,34 @@ export const SearchInput: React.FC<SearchInputProps> = (props) => {
/>
)
const onKeyPress = React.useCallback((e: React.KeyboardEvent<SVGSVGElement>) => {
if (e.key === 'Enter') {
onSuffixClicked?.()
}
}, [onSuffixClicked])
return (
<span className={styles.inputButton}>
{prefixIcon}
<input
type="text"
ref={props.inputRef}
type="text"
className={cs([styles.searchInput, props.prefixIcon ? styles.hasPrefix : ''])}
placeholder={props.placeholder}
value={props.value}
onChange={props.onChange}
/>
{
props.value &&
<FontAwesomeIcon
data-testid="close"
className={styles.suffix}
tabIndex={0}
icon="times"
onClick={props.onSuffixClicked}
onKeyPress={(e) => e.key === 'Enter' && props.onSuffixClicked && props.onSuffixClicked()}
/>
props.value && (
<FontAwesomeIcon
data-testid="close"
className={styles.suffix}
tabIndex={0}
icon="times"
onClick={props.onSuffixClicked}
onKeyPress={onKeyPress}
/>
)
}
</span>
)

View File

@@ -1,104 +0,0 @@
/// <reference types="cypress-real-events" />
import { mount } from '@cypress/react'
import React from 'react'
import { FileNode } from '../FileExplorer/helpers/makeFileHierarchy'
import { SpecList } from './SpecList'
const specs: Cypress.Cypress['spec'][] = [
{
relative: 'foo/bar/foo.spec.js',
absolute: 'Users/code/foo/bar/foo.spec.js',
name: 'foo/bar/foo.spec.js',
},
{
relative: 'qux/foo.spec.tsx',
absolute: 'qux/foo.spec.tsx',
name: 'qux/foo.spec.tsx',
},
{
relative: 'merp/foo.spec.ts',
absolute: 'merp/foo.spec.ts',
name: 'merp/foo.spec.ts',
},
]
describe('SpecList', () => {
const createSpecList = (selectStub: typeof cy.stub): React.FC => {
return () => {
const [selectedFile, setSelectedFile] = React.useState<string>()
const onFileClick = (file: FileNode) => {
selectStub(file)
setSelectedFile(file.absolute)
}
return (
<SpecList
specs={specs}
onFileClick={onFileClick}
selectedFile={selectedFile}
/>
)
}
}
it('renders and selects a file', () => {
const selectStub = cy.stub()
const Subject = createSpecList(selectStub)
mount(<Subject />)
cy.get('div').contains('foo.spec.tsx').click().then(() => {
expect(selectStub).to.have.been.calledWith({
type: 'file',
absolute: 'qux/foo.spec.tsx',
name: 'foo.spec.tsx',
})
})
})
it('closes a folder', () => {
const Subject = createSpecList(cy.stub())
mount(<Subject />)
cy.get('div').contains('foo.spec.tsx').should('exist')
// qux folder contains foo.spec.tsx. If we close it, it should not exist anymore.
cy.get('div').contains('qux').click().then(() => {
cy.get('div').contains('foo.spec.tsx').should('not.exist')
})
})
it('navigates with arrow keys', () => {
const selectStub = cy.stub()
const Subject = createSpecList(selectStub)
mount(<Subject />)
// close the "foo" directory
cy.get('div').contains('foo').click()
// navigate to "qux"
cy.realPress('ArrowDown')
cy.get('div').contains('foo.spec.tsx').should('exist')
// collapse "qux", hiding "foo.spec.tsx"
cy.realPress('{enter}')
cy.get('div').contains('foo.spec.tsx').should('not.exist')
// uncollapse "qux", revealing "foo.spec.tsx"
cy.realPress('{enter}')
cy.get('div').contains('foo.spec.tsx').should('exist')
// navigate to "foo.spec.tsx"
cy.realPress('ArrowDown')
cy.realPress('{enter}').then(() => {
expect(selectStub).to.have.been.calledWith({
type: 'file',
absolute: 'qux/foo.spec.tsx',
name: 'foo.spec.tsx',
})
})
})
})

View File

@@ -1,81 +0,0 @@
import React from 'react'
import {
FileComponentProps,
FolderComponentProps,
FileExplorer,
FileExplorerProps,
} from '../FileExplorer/FileExplorer'
import { makeFileHierarchy } from '../FileExplorer/helpers/makeFileHierarchy'
import { InlineIcon } from '@iconify/react'
import javascriptIcon from '@iconify/icons-vscode-icons/file-type-js-official'
import typescriptIcon from '@iconify/icons-vscode-icons/file-type-typescript-official'
import reactJs from '@iconify/icons-vscode-icons/file-type-reactjs'
import reactTs from '@iconify/icons-vscode-icons/file-type-reactts'
import folderClosed from '@iconify/icons-vscode-icons/default-folder'
import folderOpen from '@iconify/icons-vscode-icons/default-folder-opened'
import styles from './SpecList.module.scss'
const icons: Record<string, any> = {
js: { icon: javascriptIcon },
ts: { icon: typescriptIcon },
tsx: { icon: reactTs },
jsx: { icon: reactJs },
folderOpen: { icon: folderOpen },
folderClosed: { icon: folderClosed },
}
const getExt = (path: string) => {
const extensionMatches = path.match(/(?:\.([^.]+))?$/)
return extensionMatches ? extensionMatches[1] : ''
}
const FileComponent: React.FC<FileComponentProps> = (props) => {
const ext = getExt(props.item.name)
const inlineIconProps = ext && icons[ext]
return (
<div
onClick={() => props.onClick(props.item)}
>
<InlineIcon {...inlineIconProps} />
{props.item.name}
</div>
)
}
const FolderComponent: React.FC<FolderComponentProps> = (props) => {
const inlineIconProps = props.isOpen ? icons.folderOpen : icons.folderClosed
return (
<div
onClick={props.onClick}
>
<InlineIcon {...inlineIconProps} />
{props.item.name}
</div>
)
}
interface SpecListProps extends Omit<
FileExplorerProps, 'files' | 'fileComponent' | 'folderComponent' | 'cssModule'
> {
specs: Cypress.Cypress['spec'][]
}
export const SpecList: React.FC<SpecListProps> = (props) => {
const files = React.useMemo(() => makeFileHierarchy(props.specs.map((spec) => spec.relative)), [props.specs])
return (
<>
<FileExplorer
{...props}
cssModule={styles}
files={files}
fileComponent={FileComponent}
folderComponent={FolderComponent}
/>
</>
)
}

View File

@@ -1,7 +1,4 @@
// Color map
@import '~@fortawesome/fontawesome-free/css/all.min.css';
@import '~normalize.css';
$colors: (
black: rgba(0, 0, 0, 1),
metal-90: rgba(33, 36, 38, 1),

View File

@@ -2,10 +2,6 @@ export * from './components/Button'
export * from './components/CypressLogo/CypressLogo'
export * from './components/FileExplorer/FileExplorer'
export * from './components/SpecList/SpecList'
export * from './components/SearchInput/SearchInput'
export * from './components/Nav'

View File

@@ -50,6 +50,8 @@ const baseRules = {
'error',
2,
{
// TODO: fix this, we shouldn't need to ignore TemplateLiterals
'ignoredNodes': ['TemplateLiteral'],
'SwitchCase': 1,
'MemberExpression': 0,
},
@@ -193,7 +195,8 @@ const baseRules = {
],
'space-infix-ops': 'error',
'space-unary-ops': 'error',
'template-curly-spacing': 'error',
// TODO: change this back to 'error'
'template-curly-spacing': 'off',
'use-isnan': 'error',
'valid-typeof': 'error',
}
@@ -214,6 +217,9 @@ module.exports = {
json: {
'sort-package-json': 'pro',
},
react: {
version: 'detect',
},
},
env: {
node: true,
@@ -252,12 +258,16 @@ module.exports = {
},
},
{
files: '*.ts',
files: [
'*.ts',
'*.tsx',
],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
rules: {
'no-undef': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',

View File

@@ -17,9 +17,9 @@
"shelljs": "^0.8.3"
},
"devDependencies": {
"eslint": "^6.1.0",
"eslint": "^7.22.0",
"eslint-plugin-json-format": "^2.0.0",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-mocha": "^8.1.0",
"eslint-plugin-promise": "^4.2.1",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
@@ -33,7 +33,7 @@
"eslint": ">= 3.2.1",
"eslint-plugin-json-format": ">= 2.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-react": "^7.2.1"
"eslint-plugin-react": "^7.22.0"
},
"bin": {
"lint-changed": "./lib/scripts/lint-changed.js",
@@ -54,4 +54,4 @@
"eslint",
"eslintplugin"
]
}
}

View File

@@ -12,6 +12,7 @@
"env": {
"cypress/globals": true
},
"root": true,
"globals": {
"jest": "readonly"
},

View File

@@ -1,3 +1,9 @@
/**
* NOTICE
* Disable the release for React until further notice.
* You may re-enable it by toggling publishConfig: restricted/public
* and uncommenting the following lines
*/
module.exports = {
...require('../../.releaserc.base'),
branches: [

View File

@@ -39,14 +39,6 @@
# [@cypress/react-v5.0.1](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.0.0...@cypress/react-v5.0.1) (2021-02-17)
### Bug Fixes
* trigger semantic release ([#15128](https://github.com/cypress-io/cypress/issues/15128)) ([3a6f3b1](https://github.com/cypress-io/cypress/commit/3a6f3b1928277f7086062b1107f424e5a0247e00))
# [@cypress/react-v5.0.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v4.16.4...@cypress/react-v5.0.0) (2021-02-17)
### Bug Fixes
* update dependencies of npm/react-vue ([#15095](https://github.com/cypress-io/cypress/issues/15095)) ([e028262](https://github.com/cypress-io/cypress/commit/e028262aed485865c4f40162c1f8102970ef91f8))
@@ -60,31 +52,11 @@
### BREAKING CHANGES
* change of architecture for
component testing
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
# [@cypress/react-v5.0.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v4.16.4...@cypress/react-v5.0.0) (2021-02-16)
### Bug Fixes
* update dependencies of npm/react-vue ([#15095](https://github.com/cypress-io/cypress/issues/15095)) ([e028262](https://github.com/cypress-io/cypress/commit/e028262aed485865c4f40162c1f8102970ef91f8))
* **component-testing:** make content adjust to size of window ([#14876](https://github.com/cypress-io/cypress/issues/14876)) ([4cf3896](https://github.com/cypress-io/cypress/commit/4cf3896ecbb074831709f73f22768457fdaf5779))
### Features
* component testing ([#14479](https://github.com/cypress-io/cypress/issues/14479)) ([af26fbe](https://github.com/cypress-io/cypress/commit/af26fbebe6bc609132013a0493a116cc78bb1bd4))
### BREAKING CHANGES
* change of architecture for
component testing
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
* Added the need to install a preprocessor or a dev-server plugin
* Removed the pre-instalation of test coverage
* Install it manually by following [the documentation](https://docs.cypress.io/guides/tooling/code-coverage.html#Introduction)
* removed the pre-installation of `cypress-react-selector`
* If you use `cy.react()` in your tests, the command will not work anymore. [Install it back in your support file](https://www.npmjs.com/package/cypress-react-selector)
# [@cypress/react-v4.16.4](https://github.com/cypress-io/cypress/compare/@cypress/react-v4.16.3...@cypress/react-v4.16.4) (2021-01-27)

View File

@@ -1,4 +1,4 @@
> A little helper to unit test React components in the open source [Cypress.io](https://www.cypress.io/) E2E test runner **v4.5.0+**
> A little helper to unit test React components in the open source [Cypress.io](https://www.cypress.io/) test runner **v7.0.0+**
**Jump to:** [Comparison](#comparison), [Blog posts](#blog-posts), [Install](#install), Examples: [basic](#basic-examples), [advanced](#advanced-examples), [full](#full-examples), [external](#external-examples), [Style options](#options), [Code coverage](#code-coverage), [Visual testing](#visual-testing), [Common problems](#common-problems), [Chat](#chat)
@@ -8,9 +8,9 @@
![Example component test](images/dynamic.gif)
- How is this different from [Enzyme](https://github.com/airbnb/enzyme) or [RTL](https://testing-library.com/docs/react-testing-library/intro)? It is similar in functionality BUT runs the component in the real browser with full power of Cypress E2E test runner: [live GUI, full API, screen recording, CI support, cross-platform](https://www.cypress.io/features/), and [visual testing](https://on.cypress.io/visual-testing). Ohh, and the code coverage is built-in!
- How is this different from [Enzyme](https://github.com/airbnb/enzyme) or [RTL](https://testing-library.com/docs/react-testing-library/intro)? It is similar in functionality BUT runs the component in the real browser with full power of Cypress E2E test runner: [live GUI, full API, screen recording, CI support, cross-platform](https://www.cypress.io/features/), and [visual testing](https://on.cypress.io/visual-testing).
- If you like using `@testing-library/react`, you can use `@testing-library/cypress` for the same `findBy`, `queryBy` commands, see one of the examples in the list below
- Read [My Vision for Component Tests in Cypress](https://glebbahmutov.com/blog/my-vision-for-component-tests/)
- Read [My Vision for Component Tests in Cypress](https://glebbahmutov.com/blog/my-vision-for-component-tests/) by Gleb Bahmutov
## Comparison
@@ -54,10 +54,10 @@ If you are coming from Enzyme world, check out the [enzyme](cypress/component/ba
## Install
Requires [Node](https://nodejs.org/en/) version 8 or above.
Requires [Node](https://nodejs.org/en/) version 12 or above.
```sh
npm install --save-dev cypress @cypress/react
npm install --save-dev cypress @cypress/react @cypress/webpack-dev-server
```
## Init
@@ -294,6 +294,20 @@ If your React and React DOM libraries are installed in non-standard paths (think
</details>
You may also specify the `ReactDOM` package to use. This can be useful in complex monorepo setups that have different versions of React and React DOM installed. If you see an error relating to [mismatching versions of React or React DOM](https://reactjs.org/warnings/invalid-hook-call-warning.html#mismatching-versions-of-react-and-react-dom), this may be the solution. You can do this using the `ReactDom` option:
```jsx
// if you have multiple versions of ReactDom in your monorepo
import ReactDom from 'react-dom'
mount(<Todo todo={todo} />, {
stylesheets: [
'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.css',
],
ReactDom
})
```
## Code coverage
In order to use code coverage you can follow the instructions from [docs](https://github.com/cypress-io/code-coverage). In most of cases you need to install 2 dependencies:
@@ -357,44 +371,13 @@ Finally, when running tests on the continuous integration service, the true test
</details>
<details id="speed">
<summary>Slow bundling</summary>
When you bundle spec file, you are now bundling React, Read DOM and other libraries, which is might be slow. For now, you can disable inline source maps by adding to your Webpack config settings (if available) the following:
```js
const webpackOptions = {
devtool: false,
}
```
Keep your eye on issue [#9663](https://github.com/cypress-io/cypress/issues/9663) for more information.
</details>
<details id="missing-code-coverage">
<summary>Missing code coverage</summary>
If you are using your custom Webpack, this plugin might be missing code coverage information because the code was not instrumented. We try to insert the `babel-plugin-istanbul` plugin automatically, but your bundling might not use Babel, or configure it differently, preventing plugin insertion. Please let us know by opening an issue with full reproducible details.
See related issue [#141](https://github.com/bahmutov/cypress-react-unit-test/react/issues/141). You can also debug the plugin's behavior by running it with `DEBUG` environment variable, see [#debugging](#debugging) section.
</details>
<details id="gatsby-not-supported">
<summary>Gatsby.js projects not supported</summary>
Currently, this project cannot find Webpack settings used by Gatsby.js, thus it cannot bundle specs and application code correctly. Keep an eye on [#307](https://github.com/cypress-io/cypress/issues/9671)
</details>
## Context Provider usage
React context provider usage and API described in [./docs/providers-and-composition.md](./docs/providers-and-composition.md)
## Chat
We have a chat workspace at [https://component-testing.slack.com/](https://component-testing.slack.com/), you are welcome to [join us](https://join.slack.com/t/component-testing/shared_invite/zt-h93lrgsl-8WzE8yNQlcZuZji_gA_mtg).
Come chat with us [on discord](https://discord.gg/7ZHYhZSW) in the #component-testing channel.
## Development
@@ -418,12 +401,9 @@ DEBUG=@cypress/react,find-webpack
Same feature for unit testing components from other frameworks using Cypress
- [cypress-vue-unit-test](https://github.com/bahmutov/cypress-vue-unit-test)
- [@cypress/vue](https://github.com/cypress-io/cypress/tree/develop/npm/vue)
- [cypress-cycle-unit-test](https://github.com/bahmutov/cypress-cycle-unit-test)
- [cypress-svelte-unit-test](https://github.com/bahmutov/cypress-svelte-unit-test)
- [cypress-angular-unit-test](https://github.com/bahmutov/cypress-angular-unit-test)
- [cypress-hyperapp-unit-test](https://github.com/bahmutov/cypress-hyperapp-unit-test)
- [cypress-angularjs-unit-test](https://github.com/bahmutov/cypress-angularjs-unit-test)
[renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg
[renovate-app]: https://renovateapp.com/

View File

@@ -11,6 +11,5 @@
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"experimentalComponentTesting": true,
"experimentalFetchPolyfill": true
}
}

View File

@@ -1,6 +1,9 @@
import * as React from 'react'
const LazyDog = React.lazy(() => import(/* webpackChunkName: "Dog" */ './Dog'))
const LazyDog = React.lazy(() => {
return import(/* webpackChunkName: "Dog" */ './Dog')
.then((comp) => new Promise((resolve) => setTimeout(() => resolve(comp), 10)))
})
interface LazyComponentProps {}

View File

@@ -9,3 +9,11 @@ it('renders a button', () => {
</Button>,
)
})
it('renders a button with an icon', () => {
mount(
<Button variant="contained" color="primary" startIcon="⛹️">
Hello World
</Button>,
)
})

View File

@@ -43,6 +43,7 @@ export default class MouseMovement extends React.Component {
clearTimeout(this.state.timer)
this.props.onMoved(false)
}
render () {
return null
}

View File

@@ -31,7 +31,13 @@ describe('Renderless component', () => {
cy.get('@log')
.invoke('getCalls')
.then((calls) => calls.map((call) => call.args[0]))
.then((calls) => {
return calls.map((call) => {
console.log('one', call.args[0])
return call.args[0]
})
})
.should('deep.equal', [
'MouseMovement constructor',
'MouseMovement componentWillMount',

View File

@@ -29,15 +29,20 @@ describe('Error Boundary', () => {
})
it('on error, display fallback UI', () => {
try {
mount(
<ErrorBoundary name="ChildWithError">
<ChildWithError />
</ErrorBoundary>,
)
} catch (e) {
// do nothing
}
// Error boundaries do not stop an uncaught error from propagating.
// Cypress will fail on uncaught exceptions by default, so we need to suppress that behavior.
cy.on('uncaught:exception', (err) => {
// Assert that we are only suppressing the default behavior for the error we expect.
expect(err.message.includes('I crashed!')).to.be.true
return false
})
mount(
<ErrorBoundary name="ChildWithError">
<ChildWithError />
</ErrorBoundary>,
)
cy.get('header h1').should('contain', 'Something went wrong.')
cy.get('header h3').should('contain', 'ChildWithError failed to load')

View File

@@ -61,6 +61,10 @@ const webpackConfig = {
* @type Cypress.PluginConfig
*/
module.exports = (on, config) => {
if (config.testingType !== 'component') {
throw Error(`This is a component testing project. testingType should be 'component'. Received ${config.testingType}`)
}
on('dev-server:start', (options) => {
return startDevServer({ options, webpackConfig, disableLazyCompilation: false })
})

View File

@@ -1,22 +1,28 @@
# Recipes
- [Recipes](#recipes)
- [Do nothing](#do-nothing)
- [React Scripts](#react-scripts)
- [Next.js](#nextjs)
- [Your webpack config](#your-webpack-config)
- [Your `.babelrc` file](#your-babelrc-file)
- [Add Babel plugins](#add-babel-plugins)
- [Configuration](#configuration)
- [Do nothing](#do-nothing)
- [React Scripts](#react-scripts)
- [Next.js](#nextjs)
- [Your Webpack config](#your-webpack-config)
- [Your `.babelrc` file](#your-babelrc-file)
- [Add Babel plugins](#add-babel-plugins)
- [Using rollup config](#using-rollup-config)
- [Usage](#usage)
- [Changing props](#changing-props)
## Do nothing
## Configuration
### Do nothing
Cypress Test Runner understands plain JSX by default, so for simple React applications it ... might just test components right out of the box!
But usually you want to point Cypress at your application's current Webpack configuration, so the specs can import your components correctly. The next recipes discuss common ways for doing this.
## React Scripts
### React Scripts
If you are using Create-React-App v3 or `react-scripts`, and want to reuse the built in webpack (even after ejecting), this module ships with Cypress preprocessor in [plugins](plugins) folder.
If you are using Create-React-App v3 or `react-scripts`, and want to reuse the built in Webpack (even after ejecting), this module ships with Cypress preprocessor in [plugins](plugins) folder.
```js
// cypress/plugins/index.js
@@ -32,7 +38,7 @@ See example repo [bahmutov/try-cra-with-unit-test](https://github.com/bahmutov/t
**Tip:** `plugins/react-scripts` is just loading `plugins/cra-v3`.
## Next.js
### Next.js
```js
// cypress/plugins/index.js
@@ -46,9 +52,9 @@ module.exports = (on, config) => {
See example in the folder [examples/nextjs](examples/nextjs).
## Your webpack config
### Your Webpack config
If you have your own webpack config, you can use included plugins file to load it. You can pass the webpack config file name (with respect to the root folder where `cypress.json` file sits) via plugins file or via an `env` variable in `cypress.json`
If you have your own Webpack config, you can use included plugins file to load it. You can pass the Webpack config file name (with respect to the root folder where `cypress.json` file sits) via plugins file or via an `env` variable in `cypress.json`
```js
// cypress/plugins/index.js
@@ -64,7 +70,7 @@ module.exports = (on, config) => {
See example in [bahmutov/Jscrambler-Webpack-React](https://github.com/bahmutov/Jscrambler-Webpack-React) or included example in the folder [examples/webpack-file](examples/webpack-file).
## Your `.babelrc` file
### Your `.babelrc` file
If you are using Babel without Webpack to transpile, you can use the plugin that tells Babel loader to use your `.babelrc` configuration file.
@@ -81,7 +87,7 @@ module.exports = (on, config) => {
See example in the folder [examples/using-babel](examples/using-babel) and [examples/using-babel-typescript](examples/using-babel-typescript).
### Add Babel plugins
#### Add Babel plugins
If you want to use code instrumentation, add the [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul) to your `.babelrc` setup. You do not even need to install it separately, as it is already included in `@cypress/react` as a dependency.
@@ -125,7 +131,7 @@ When loading your `.babelrc` settings, `@cypress/react` sets `BABEL_ENV` and `NO
See [examples/using-babel](examples/using-babel) folder for full example.
### Using rollup config
#### Using rollup config
If you are using rollup for bundling we can use it as well for the bundling. Check the example:
@@ -158,3 +164,83 @@ replace({ 'process.env.NODE_ENV': JSON.stringify('development') }),
```
See [examples/rollup](examples/rollup) folder for full example.
## Usage
### Changing props
Many components have some statefulness, whether explicitly through `useState`, or implicitly through `useEffect`. Therefore during testing it is useful to keep the component mounted, but change the props being passed to it in order to preserve its state. This is referred to in some testing frameworks as `rerender()`.
We recommend building a "wrapper" component that acts similarly to how your users will interact with the component under test. In isolation, you can add DOM controls to push new props to your component.
```js
const Accumulator = ({ value }) => {
const [storedValues, setStoredValues] = React.useState([])
React.useEffect(() => {
if (!value) {
return
}
setStoredValues((prev) => [...prev, value])
}, [value])
return (
<ul>
{storedValues.map((v) => (
<li key={v}>
{v}
</li>
))}
</ul>
)
}
```
This component is an accumulator that stores each `value` prop passed to it. We create a wrapper component that has an `input` and a `button` to push new values to the `value` prop.
```js
const TestAcc = () => {
const ref = React.useRef()
const [value, setValue] = React.useState()
return (
<div>
<input ref={ref} />
<button
onClick={() => {
setValue(ref.current.value)
}}
>
Add
</button>
<Acc value={value} />
</div>
)
}
```
With this, we can begin writing component tests to check the functionality of our `Accumulator` component.
```js
it('should store value', () => {
mount(<TestAcc />)
cy.get('input').type('Component testing is awesome!')
cy.get('button').click()
cy.get('li').eq(0).contains('Component testing is awesome!')
cy.get('input').clear().type('We are dynamically changing props')
cy.get('button').click()
cy.get('li').eq(1).contains('We are dynamically changing props')
cy.get('input').clear().type('to build a list of text')
cy.get('button').click()
cy.get('li').eq(0).contains('Component testing is awesome!')
cy.get('li').eq(1).contains('We are dynamically changing props')
cy.get('li').eq(2).contains('to build a list of text')
})
```

View File

@@ -3,6 +3,5 @@
"fixturesFolder": false,
"testFiles": "**/*spec.js",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true
}
"viewportHeight": 500
}

View File

@@ -3,10 +3,9 @@
"testFiles": "**/*.spec.{js,jsx}",
"viewportWidth": 500,
"viewportHeight": 800,
"experimentalComponentTesting": true,
"experimentalFetchPolyfill": true,
"componentFolder": "cypress/components",
"env": {
"coverage": true
}
}
}

View File

@@ -2,7 +2,6 @@
/// <reference types="cypress" />
import * as React from 'react'
import RouterPage from '../../pages/router'
import { createRouter } from 'next/router'
import { RouterContext } from 'next/dist/next-server/lib/router-context'
import { mount } from '@cypress/react'
@@ -22,6 +21,9 @@ describe('Component with router usage', () => {
reload: cy.spy(),
back: cy.spy(),
prefetch: cy.spy(),
isReady: true,
isPreview: false,
isLocaleDomain: false,
beforePopState: cy.spy(),
}
@@ -34,33 +36,6 @@ describe('Component with router usage', () => {
cy.contains('Next.js route /testPath')
})
it('renders the component that uses next.js with parsed query', () => {
// alternatively you can use next's internal `createRouter` function to create a real instance of NextRouter
const router = createRouter(
'/testPath',
{ param1: 'param1' },
'/asTestPath',
{
subscription: cy.spy(),
initialProps: {},
App: cy.spy(),
Component: cy.spy(),
pageLoader: cy.spy(),
initialStyleSheets: [],
wrapApp: cy.spy(),
isFallback: false,
},
)
mount(
<RouterContext.Provider value={router}>
<RouterPage />
</RouterContext.Provider>,
)
cy.contains('My query: {"param1":"param1"}')
})
it('pushes the new route', () => {
const router = {
pathname: '/router',
@@ -76,6 +51,9 @@ describe('Component with router usage', () => {
reload: cy.spy(),
back: cy.spy(),
prefetch: cy.spy(),
isReady: true,
isPreview: false,
isLocaleDomain: false,
beforePopState: cy.spy(),
}

View File

@@ -15,12 +15,11 @@
"devDependencies": {
"@cypress/react": "file:../../dist",
"@mdx-js/loader": "^1.6.16",
"@next/mdx": "10.0.7",
"@next/mdx": "10.1.2",
"@zeit/next-sass": "^1.0.1",
"check-code-coverage": "1.9.2",
"cypress-circleci-reporter": "0.2.0",
"next": "10.0.7",
"webpack": "^4.44.2"
"next": "10.1.2"
},
"license": "MIT"
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,5 @@
"testFiles": "**/*cy-spec.js",
"viewportWidth": 500,
"viewportHeight": 800,
"experimentalComponentTesting": true,
"componentFolder": "cypress/component"
}
}

View File

@@ -3,7 +3,6 @@
"testFiles": "**/*cy-spec.js",
"viewportWidth": 500,
"viewportHeight": 800,
"experimentalComponentTesting": true,
"experimentalFetchPolyfill": true,
"componentFolder": "src"
}
}

View File

@@ -23,7 +23,7 @@ describe('RemotePizza', () => {
it('stubs via prop (di)', () => {
const fetchIngredients = cy.stub().resolves({ args: { ingredients } })
mount(<RemotePizza fetchIngredients={fetchIngredients} />)
mount(<RemotePizza fetchIngredients={fetchIngredients} />, { ReactDom })
cy.contains('button', /cook/i).click()
for (const ingredient of ingredients) {

View File

@@ -1,8 +1,7 @@
{
"video": false,
"testFiles": "**/*spec.{ts,tsx}",
"experimentalComponentTesting": true,
"experimentalFetchPolyfill": true,
"componentFolder": "src",
"fixturesFolder": false
}
}

View File

@@ -4,10 +4,9 @@
"testFiles": "**/*spec.*",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true,
"componentFolder": "src",
"nodeVersion": "system",
"env": {
"coverage": true
}
}
}

View File

@@ -4,7 +4,6 @@
"testFiles": "**/*-spec.js",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true,
"ignoreTestFiles": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
@@ -14,4 +13,4 @@
"prettier": true
}
}
}
}

View File

@@ -4,9 +4,8 @@
"testFiles": "**/*cy-spec.js",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true,
"componentFolder": "src",
"env": {
"coverage": true
}
}
}

View File

@@ -4,6 +4,5 @@
"testFiles": "**/*spec.tsx",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true,
"componentFolder": "src"
}
}

View File

@@ -4,6 +4,5 @@
"testFiles": "**/*spec.js",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true,
"componentFolder": "src"
}
}

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