Merge branch '10.0-release' into develop-10.0-release

This commit is contained in:
Blue F
2022-04-14 10:32:05 -07:00
committed by GitHub
4377 changed files with 357024 additions and 107577 deletions

View File

@@ -6,7 +6,7 @@
**/build
**/cypress/fixtures
**/dist
**/dist-test
**/dist-*
**/node_modules
**/support/fixtures/*
!**/support/fixtures/projects
@@ -22,11 +22,14 @@ system-tests/projects/**/static/*
system-tests/projects/**/*.jsx
system-tests/projects/**/fail.js
system-tests/lib/scaffold/plugins/index.js
system-tests/lib/scaffold/support/index.js
system-tests/lib/scaffold/support/e2e.js
system-tests/lib/scaffold/support/component.js
system-tests/lib/scaffold/support/commands.js
system-tests/test/support/projects/e2e/cypress/
system-tests/projects/e2e/cypress/integration/stdout_exit_early_failing_spec.js
system-tests/projects/e2e/cypress/integration/typescript_syntax_error_spec.ts
system-tests/projects/e2e/cypress/e2e/stdout_exit_early_failing.cy.js
system-tests/projects/e2e/cypress/e2e/typescript_syntax_error.cy.ts
system-tests/projects/config-with-ts-syntax-error/**
system-tests/projects/config-with-ts-module-error/**
**/test/fixtures
@@ -35,14 +38,20 @@ system-tests/projects/e2e/cypress/integration/typescript_syntax_error_spec.ts
# cli/types is linted by tslint/dtslint
cli/types
# cli/react, cli/vue, and cli/mount-utils are all copied from dist'd builds
cli/react
cli/vue
cli/vue2
cli/mount-utils
# packages/example is not linted (think about changing this)
packages/example
packages/extension/test/helpers/background.js
integration/stdout_exit_early_failing_spec.js
e2e/stdout_exit_early_failing_spec.js
npm/webpack-preprocessor/cypress/tests/e2e/compile-error.js
npm/webpack-preprocessor/examples/use-babelrc/cypress/integration/spec.js
npm/webpack-preprocessor/examples/use-babelrc/cypress/e2e/spec.cy.js
npm/cypress-schematic/src/**/*.js
@@ -60,3 +69,41 @@ npm/cypress-schematic/src/**/*.js
**/.next/**
/npm/create-cypress-tests/initial-template
/npm/create-cypress-tests/**/*.template.*
# The global eslint configuration is not set up to parse vue@2 files
/npm/vue2/**/*.vue
packages/data-context/test/unit/codegen/files
# community templates we test against, no need to lint
system-tests/projects/cra-4/**/*
system-tests/projects/cra-5/**/*
system-tests/projects/cra-ejected/**/*
system-tests/projects/create-react-app-configured/**/*
system-tests/projects/create-react-app-unconfigured/**/*
system-tests/projects/create-react-app-custom-index-html
system-tests/projects/vueclivue2-unconfigured/**/*
system-tests/projects/vueclivue2-configured/**/*
system-tests/projects/vueclivue3-unconfigured/**/*
system-tests/projects/vueclivue3-configured/**/*
system-tests/projects/vueclivue3-custom-index-html
system-tests/projects/vuecli5vue3-unconfigured/**/*
system-tests/projects/vuecli5vue3-configured/**/*
system-tests/projects/vue3-vite-ts-unconfigured/**/*
system-tests/projects/vue3-vite-ts-configured/**/*
system-tests/projects/vue3-vite-ts-custom-index-html
system-tests/projects/nextjs-unconfigured/**/*
system-tests/projects/nextjs-configured/**/*
system-tests/projects/nuxtjs-vue2-unconfigured/**/*
system-tests/projects/nuxtjs-vue2-configured/**/*
system-tests/projects/react-app-webpack-5-unconfigured/**/*
system-tests/project-fixtures/**
system-tests/projects/**/*/expected-cypress*/**/*

71
.eslintrc.js Normal file
View File

@@ -0,0 +1,71 @@
const fs = require('fs')
const path = require('path')
const { specifiedRules } = require('graphql')
const graphqlOpts = {
env: 'literal',
tagName: 'gql',
schemaString: fs.readFileSync(
path.join(__dirname, 'packages/graphql/schemas/schema.graphql'),
'utf8',
),
}
const validators = specifiedRules
.map((rule) => rule.name)
.filter(
(ruleName) => {
return [
'NoUnusedFragmentsRule',
'KnownFragmentNamesRule',
'NoUnusedVariablesRule',
].findIndex((x) => x === ruleName) === -1
},
)
module.exports = {
plugins: [
'@cypress/dev',
'graphql',
],
extends: [
'plugin:@cypress/dev/general',
'plugin:@cypress/dev/tests',
],
parser: '@typescript-eslint/parser',
rules: {
'no-duplicate-imports': 'off',
'import/no-duplicates': 'off',
'@typescript-eslint/no-duplicate-imports': [
'error',
],
'prefer-spread': 'off',
'prefer-rest-params': 'off',
'no-useless-constructor': 'off',
'no-restricted-properties': [
'error',
{
object: 'process',
property: 'geteuid',
message: 'process.geteuid() will throw on Windows. Do not use it unless you catch any potential errors.',
},
{
object: 'os',
property: 'userInfo',
message: 'os.userInfo() will throw when there is not an `/etc/passwd` entry for the current user (like when running with --user 12345 in Docker). Do not use it unless you catch any potential errors.',
},
],
'graphql/capitalized-type-name': ['warn', graphqlOpts],
'graphql/no-deprecated-fields': ['error', graphqlOpts],
'graphql/template-strings': ['error', { ...graphqlOpts, validators }],
'graphql/required-fields': [
'error',
{ ...graphqlOpts, requiredFields: ['id'] },
],
},
settings: {
react: {
version: '16.8',
},
},
}

View File

@@ -1,32 +0,0 @@
{
"plugins": [
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
"plugin:@cypress/dev/tests"
],
"rules": {
"prefer-spread": "off",
"prefer-rest-params": "off",
"no-useless-constructor": "off",
"no-restricted-properties": [
"error",
{
"object": "process",
"property": "geteuid",
"message": "process.geteuid() will throw on Windows. Do not use it unless you catch any potential errors."
},
{
"object": "os",
"property": "userInfo",
"message": "os.userInfo() will throw when there is not an `/etc/passwd` entry for the current user (like when running with --user 12345 in Docker). Do not use it unless you catch any potential errors."
}
]
},
"settings": {
"react": {
"version": "16.8"
}
}
}

2
.github/CODEOWNERS vendored
View File

@@ -34,6 +34,8 @@
/packages/net-stubbing/ @cypress-io/end-to-end
/packages/network/ @cypress-io/end-to-end
/packages/proxy/ @cypress-io/end-to-end
/packages/data-context/ @tgriesser
/packages/graphql/ @tgriesser
/packages/reporter/ @cypress-io/end-to-end
/packages/resolve-dist/ @cypress-io/end-to-end
/packages/rewriter/ @cypress-io/end-to-end

View File

@@ -35,4 +35,3 @@ DO NOT DELETE the PR checklist.
- [ ] Has the original issue (or this PR, if no issue exists) been tagged with a release in ZenHub? (user-facing changes only)
- [ ] Has a PR for user-facing changes been opened in [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation)? <!-- Link to PR here -->
- [ ] Have API changes been updated in the [`type definitions`](https://github.com/cypress-io/cypress/blob/develop/cli/types/cypress.d.ts)?
- [ ] Have new configuration options been added to the [`cypress.schema.json`](https://github.com/cypress-io/cypress/blob/develop/cli/schema/cypress.schema.json)?

17
.gitignore vendored
View File

@@ -9,22 +9,18 @@ _test-output
cypress.zip
.babel-cache
# from config, compiled .js files
packages/config/src/*.js
# from extension
Cached Theme.pak
Cached Theme Material Design.pak
# from config, compiled .js files
packages/config/lib/*.js
# from data-context, compiled .js files
packages/data-context/src/**/*.js
packages/errors/src/**/*.js
packages/errors/test/**/*.js
# from desktop-gui
packages/desktop-gui/cypress/videos
packages/desktop-gui/src/jsconfig.json
# from driver
packages/driver/cypress/videos
packages/driver/cypress/screenshots
@@ -72,6 +68,7 @@ packages/socket/lib/*.js
system-tests/.projects
system-tests/.http-mitm-proxy
system-tests/fixtures/large-img
system-tests/lib/fixtureDirs.ts
# from npm/react
/npm/react/bin/*
@@ -83,6 +80,9 @@ system-tests/fixtures/large-img
/npm/design-system/cypress/videos
/npm/design-system/.babel-cache
# from npm/webpack-dev-server-fresh
/npm/webpack-dev-server-fresh/cypress/videos
# from runner-ct
/packages/runner-ct/cypress/screenshots
@@ -94,6 +94,8 @@ system-tests/fixtures/large-img
# graphql, auto-generated
/packages/launchpad/src/generated
/packages/app/src/generated
/packages/frontend-shared/src/generated
/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts
# from npm/create-cypress-tests
/npm/create-cypress-tests/initial-template
@@ -104,7 +106,6 @@ scripts/support
binary-url.json
# Allows us to dynamically create eslint rules that override the default for Decaffeinate scripts
.eslintrc.js
cli/visual-snapshots
# Created by https://www.gitignore.io/api/osx,git,node,windows,intellij,linux

View File

@@ -15,5 +15,6 @@ module.exports = {
],
extends: 'semantic-release-monorepo',
branches: [
'master',
],
}

4
.vscode/cspell.json vendored
View File

@@ -15,6 +15,7 @@
"OVERLIMIT",
"Pinia",
"pnpm",
"pseudoclass",
"Screenshotting",
"shiki",
"testid",
@@ -25,7 +26,8 @@
"urql",
"vite",
"vitejs",
"vueuse"
"vueuse",
"Windi"
],
"ignoreWords": [],
"import": []

View File

@@ -39,6 +39,27 @@
"cwd": "[cwd]/system-tests",
"command": "yarn test [fileBasename]"
},
{
"name": "packages/app cypress open",
"focus": true,
"onlySingle": true,
"cwd": "[cwd]/packages/app",
"command": "yarn cypress:open"
},
{
"name": "packages/app dev",
"focus": true,
"onlySingle": true,
"cwd": "[cwd]/packages/app",
"command": "yarn dev"
},
{
"name": "packages/app watch",
"focus": true,
"onlySingle": true,
"cwd": "[cwd]",
"command": "yarn watch"
},
{
"name": "packages/server test-watch",
"focus": true,
@@ -75,20 +96,6 @@
"onlySingle": true,
"cwd": "[cwd]/packages/driver",
"command": "yarn cypress:open"
},
{
"name": "packages/desktop-gui cypress open",
"focus": true,
"onlySingle": true,
"cwd": "[cwd]/packages/desktop-gui",
"command": "yarn cypress:open"
},
{
"name": "packages/desktop-gui watch",
"focus": true,
"onlySingle": true,
"cwd": "[cwd]/packages/desktop-gui",
"command": "yarn watch"
}
]
}

View File

@@ -259,7 +259,6 @@ Here is a list of the core packages in this repository with a short description,
| Folder Name | Package Name | Purpose |
| :------------------------------------ | :---------------------- | :--------------------------------------------------------------------------- |
| [cli](./cli) | `cypress` | The command-line tool that is packaged as an `npm` module. |
| [desktop-gui](./packages/desktop-gui) | `@packages/desktop-gui` | The front-end code for the Cypress Desktop GUI. |
| [driver](./packages/driver) | `@packages/driver` | The code that is used to drive the behavior of the API commands. |
| [electron](./packages/electron) | `@packages/electron` | The Cypress implementation of Electron. |
| [example](./packages/example) | `@packages/example` | Our example kitchen-sink application. |

14
apollo.config.js Normal file
View File

@@ -0,0 +1,14 @@
const path = require('path')
// For use with Apollo Extension for VSCode:
// https://www.apollographql.com/docs/devtools/editor-plugins/
module.exports = {
client: {
service: {
name: 'cypress-io',
localSchemaFile: path.join(__dirname, 'packages/graphql/schemas/schema.graphql'),
},
tagName: 'gql',
includes: [path.join(__dirname, 'packages/{launchpad,app,frontend-shared}/src/**/*.{vue,ts,js,tsx,jsx}')],
},
}

14
autobarrel.json Normal file
View File

@@ -0,0 +1,14 @@
{
"prefix": "/* eslint-disable padding-line-between-statements */",
"paths": [
"packages/graphql/src/**/*",
"packages/data-context/src/**/*",
"packages/scaffold-config/src/**"
],
"ignore": [
"packages/data-context/src/gen",
"packages/graphql/src/stitching",
"packages/graphql/src/testing",
"packages/graphql/src/gen"
]
}

File diff suppressed because it is too large Load Diff

7
cli/.gitignore vendored
View File

@@ -12,3 +12,10 @@ types/sinon-chai
types/net-stubbing.ts
# ignore CLI output build folder
build
# ignore packages synced at build-time via
# the sync-exported-npm-with-cli.js script
vue
vue2
react
mount-utils

View File

@@ -60,6 +60,47 @@ yarn add ~/{your-dirs}/cypress/cli/build/cypress-3.3.1.tgz --ignore-scripts
Which installs the `tgz` file we have just built from folder `Users/jane-lane/{your-dirs}/cypress/cli/build`.
#### Sub-package API
> How do deep imports from cypress/* get resolved?
The cypress npm package comes pre-assembled with mounting libraries for major front-end frameworks. These mounting libraries are the first examples of Cypress providing re-exported sub-packages. These sub-packages follow the same naming convention they do when they're published on **npm**, but without a leading **`@`** sign. For example:
##### An example of a sub-package: @cypress/vue, @cypress/react, @cypress/mount-utils
**Let's discuss the Vue mounting library that Cypress ships.**
If you'd installed the `@cypress/vue` package from NPM, you could write the following code.
This would be necessary when trying to use a version of Vue, React, or other library that may be newer or older than the current version of cypress itself.
```js
import { mount } from '@cypress/vue'
```
Now, with the sub-package API, you're able to import the latest APIs directly from Cypress without needing to install a separate dependency.
```js
import { mount } from 'cypress/vue'
```
The only difference is the import name, and if you still need to use a specific version of one of our external sub-packages, you may install it and import it directly.
##### Adding a new sub-package
There are a few steps when adding a new sub-package.
1. Make sure the sub-package's rollup build is _self-contained_ or that any dependencies are also declared in the CLI's **`package.json`**.
2. Now, in the **`postbuild`** script for the sub-package you'd like to embed, invoke `node ./scripts/sync-exported-npm-with-cli.js` (relative to the sub-package, see **`npm/vue`** for an example).
3. Add the sub-package's name to the following locations:
- **`cli/.gitignore`**
- **`cli/scripts/post-build.js`**
- **`.eslintignore`** (under cli/sub-package)
4. DO NOT manually update the **package.json** file. Running `yarn build` will automate this process.
5. Commit the changed files.
[Here is an example Pull Request](https://github.com/cypress-io/cypress/pull/20930/files#diff-21b1fe66043572c76c549a4fc5f186e9a69c330b186fc91116b9b70a4d047902)
#### Module API
The module API can be tested locally using something like:

View File

@@ -16,22 +16,27 @@ exports['shows help for open --foo 1'] = `
Opens Cypress in the interactive GUI.
Options:
-b, --browser <browser-path> path to a custom browser to be added to the
list of available browsers in Cypress
-b, --browser <browser-path> runs Cypress in the browser with the given
name. if a filesystem path is supplied,
Cypress will attempt to use the browser at
that path.
--component runs component tests
-c, --config <config> sets configuration values. separate multiple
values with a comma. overrides any value in
cypress.json.
-C, --config-file <config-file> path to JSON file where configuration values
are set. defaults to "cypress.json". pass
"false" to disable.
cypress.config.{ts|js}.
-C, --config-file <config-file> path to script file where configuration
values are set. defaults to
"cypress.config.{ts|js}".
-d, --detached [bool] runs Cypress application in detached mode
--e2e runs end to end tests
-e, --env <env> sets environment variables. separate
multiple values with a comma. overrides any
value in cypress.json or cypress.env.json
value in cypress.config.{ts|js} or
cypress.env.json
--global force Cypress into global mode as if its
globally installed
-p, --port <port> runs Cypress on a specific port. overrides
any value in cypress.json.
any value in cypress.config.{ts|js}.
-P, --project <project-path> path to the project
--dev runs cypress in development and bypasses
binary check
@@ -64,16 +69,18 @@ exports['shows help for run --foo 1'] = `
Options:
-b, --browser <browser-name-or-path> runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.
--ci-build-id <id> the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers
-c, --config <config> sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.
-C, --config-file <config-file> path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.
-e, --env <env> sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json
--component runs component tests
-c, --config <config> sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{ts|js}.
-C, --config-file <config-file> path to script file where configuration values are set. defaults to "cypress.config.{ts|js}".
--e2e runs end to end tests
-e, --env <env> sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{ts|js} or cypress.env.json
--group <name> a named group for recorded runs in the Cypress Dashboard
-k, --key <record-key> your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.
--headed displays the browser instead of running headlessly
--headless hide the browser instead of running headed (default for cypress run)
--no-exit keep the browser open after tests finish
--parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes
-p, --port <port> runs Cypress on a specific port. overrides any value in cypress.json.
-p, --port <port> runs Cypress on a specific port. overrides any value in cypress.config.{ts|js}.
-P, --project <project-path> path to the project
-q, --quiet run quietly, using only the configured reporter
--record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard.
@@ -214,7 +221,9 @@ exports['cli help command shows help 1'] = `
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
Deprecated: use "open --component"
run-ct [options] Runs all Cypress component testing suites. Deprecated:
use "run --component"
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -252,7 +261,9 @@ exports['cli help command shows help for -h 1'] = `
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
Deprecated: use "open --component"
run-ct [options] Runs all Cypress component testing suites. Deprecated:
use "run --component"
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -290,7 +301,9 @@ exports['cli help command shows help for --help 1'] = `
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
Deprecated: use "open --component"
run-ct [options] Runs all Cypress component testing suites. Deprecated:
use "run --component"
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -329,7 +342,9 @@ exports['cli unknown command shows usage and exits 1'] = `
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
Deprecated: use "open --component"
run-ct [options] Runs all Cypress component testing suites. Deprecated:
use "run --component"
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
@@ -454,7 +469,9 @@ exports['cli CYPRESS_INTERNAL_ENV allows and warns when staging environment 1']
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
Deprecated: use "open --component"
run-ct [options] Runs all Cypress component testing suites. Deprecated:
use "run --component"
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and

View File

@@ -26,31 +26,6 @@ Platform: test platform-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['errors individual has the following errors 1'] = [
"CYPRESS_RUN_BINARY",
"binaryNotExecutable",
"childProcessKilled",
"failedDownload",
"failedUnzip",
"failedUnzipWindowsMaxPathLength",
"incompatibleHeadlessFlags",
"invalidCacheDirectory",
"invalidCypressEnv",
"invalidOS",
"invalidRunProjectPath",
"invalidSmokeTestDisplayError",
"invalidTestingType",
"missingApp",
"missingDependency",
"missingXvfb",
"nonZeroExitCodeXvfb",
"notInstalledCI",
"smokeTestFailure",
"unexpected",
"unknownError",
"versionMismatch"
]
exports['invalid display error'] = `
Cypress verification failed.
@@ -97,3 +72,31 @@ Consider opening a new issue.
Platform: test platform-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['errors individual has the following errors 1'] = [
"CYPRESS_RUN_BINARY",
"binaryNotExecutable",
"childProcessKilled",
"failedDownload",
"failedUnzip",
"failedUnzipWindowsMaxPathLength",
"incompatibleHeadlessFlags",
"incompatibleTestTypeFlags",
"incompatibleTestingTypeAndFlag",
"invalidCacheDirectory",
"invalidConfigFile",
"invalidCypressEnv",
"invalidOS",
"invalidRunProjectPath",
"invalidSmokeTestDisplayError",
"invalidTestingType",
"missingApp",
"missingDependency",
"missingXvfb",
"nonZeroExitCodeXvfb",
"notInstalledCI",
"smokeTestFailure",
"unexpected",
"unknownError",
"versionMismatch"
]

View File

@@ -21,13 +21,6 @@ exports['exec run .processRunOptions passes --record option 1'] = [
"my record id"
]
exports['exec run .processRunOptions passes --config-file false option 1'] = [
"--run-project",
null,
"--config-file",
false
]
exports['exec run .processRunOptions defaults to e2e testingType 1'] = [
"--run-project",
null

15
cli/index.mjs Normal file
View File

@@ -0,0 +1,15 @@
import module from 'module'
const require = module.createRequire(import.meta.url)
const cypress = require('./lib/cypress')
export default cypress
export const defineConfig = cypress.defineConfig
export const run = cypress.run
export const open = cypress.open
export const cli = cypress.cli

View File

@@ -22,10 +22,6 @@ function unknownOption (flag, type = 'option') {
}
commander.Command.prototype.unknownOption = unknownOption
const coerceFalseOrString = (arg) => {
return arg !== 'false' ? arg : false
}
const coerceFalse = (arg) => {
return arg !== 'false'
}
@@ -97,19 +93,20 @@ const parseVariableOpts = (fnArgs, args) => {
}
const descriptions = {
browserOpenMode: 'path to a custom browser to be added to the list of available browsers in Cypress',
browserRunMode: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.',
browser: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.',
cacheClear: 'delete all cached binaries',
cachePrune: 'deletes all cached binaries except for the version currently in use',
cacheList: 'list cached binary versions',
cachePath: 'print the path to the binary cache',
cacheSize: 'Used with the list command to show the sizes of the cached folders',
ciBuildId: 'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers',
config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.',
configFile: 'path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.',
component: 'runs component tests',
config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{ts|js}.',
configFile: 'path to script file where configuration values are set. defaults to "cypress.config.{ts|js}".',
detached: 'runs Cypress application in detached mode',
dev: 'runs cypress in development and bypasses binary check',
env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json',
e2e: 'runs end to end tests',
env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{ts|js} or cypress.env.json',
exit: 'keep the browser open after tests finish',
forceInstall: 'force install the Cypress binary',
global: 'force Cypress into global mode as if its globally installed',
@@ -118,7 +115,7 @@ const descriptions = {
headless: 'hide the browser instead of running headed (default for cypress run)',
key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
port: 'runs Cypress on a specific port. overrides any value in cypress.json.',
port: 'runs Cypress on a specific port. overrides any value in cypress.config.{ts|js}.',
project: 'path to the project',
quiet: 'run quietly, using only the configured reporter',
record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.',
@@ -245,10 +242,12 @@ const addCypressRunCommand = (program) => {
.command('run')
.usage('[options]')
.description('Runs Cypress tests from the CLI without the GUI')
.option('-b, --browser <browser-name-or-path>', text('browserRunMode'))
.option('-b, --browser <browser-name-or-path>', text('browser'))
.option('--ci-build-id <id>', text('ciBuildId'))
.option('--component', text('component'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('--e2e', text('e2e'))
.option('-e, --env <env>', text('env'))
.option('--group <name>', text('group'))
.option('-k, --key <record-key>', text('key'))
@@ -272,10 +271,12 @@ const addCypressOpenCommand = (program) => {
.command('open')
.usage('[options]')
.description('Opens Cypress in the interactive GUI.')
.option('-b, --browser <browser-path>', text('browserOpenMode'))
.option('-b, --browser <browser-path>', text('browser'))
.option('--component', text('component'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('-d, --detached [bool]', text('detached'), coerceFalse)
.option('--e2e', text('e2e'))
.option('-e, --env <env>', text('env'))
.option('--global', text('global'))
.option('-p, --port <port>', text('port'))
@@ -283,6 +284,16 @@ const addCypressOpenCommand = (program) => {
.option('--dev', text('dev'), coerceFalse)
}
const maybeAddInspectFlags = (program) => {
if (process.argv.includes('--dev')) {
return program
.option('--inspect', 'Node option')
.option('--inspect-brk', 'Node option')
}
return program
}
/**
* Casts known command line options for "cypress run" to their intended type.
* For example if the user passes "--port 5005" the ".port" property should be
@@ -290,7 +301,7 @@ const addCypressOpenCommand = (program) => {
*
* Returns a clone of the original object.
*/
const castCypressRunOptions = (opts) => {
const castCypressOptions = (opts) => {
// only properties that have type "string | false" in our TS definition
// require special handling, because CLI parsing takes care of purely
// boolean arguments
@@ -300,10 +311,6 @@ const castCypressRunOptions = (opts) => {
castOpts.port = coerceAnyStringToInt(opts.port)
}
if (_.has(opts, 'configFile')) {
castOpts.configFile = coerceFalseOrString(opts.configFile)
}
return castOpts
}
@@ -331,14 +338,55 @@ module.exports = {
debug('creating program parser')
const program = createProgram()
addCypressRunCommand(program)
maybeAddInspectFlags(addCypressRunCommand(program))
.action((...fnArgs) => {
debug('parsed Cypress run %o', fnArgs)
const options = parseVariableOpts(fnArgs, cliArgs)
debug('parsed options %o', options)
const casted = castCypressRunOptions(options)
const casted = castCypressOptions(options)
debug('casted options %o', casted)
resolve(casted)
})
debug('parsing args: %o', cliArgs)
program.parse(cliArgs)
})
},
/**
* Parses `cypress open` command line option array into an object
* with options that you can feed into cy.openModeSystemTest test calls
* @example
* const options = parseOpenCommand(['cypress', 'open', '--browser', 'chrome'])
* // options is {browser: 'chrome'}
*/
parseOpenCommand (args) {
return new Promise((resolve, reject) => {
if (!Array.isArray(args)) {
return reject(new Error('Expected array of arguments'))
}
// make a copy of the input arguments array
// and add placeholders where "node ..." would usually be
// also remove "cypress" keyword at the start if present
const cliArgs = args[0] === 'cypress' ? [...args.slice(1)] : [...args]
cliArgs.unshift(null, null)
debug('creating program parser')
const program = createProgram()
maybeAddInspectFlags(addCypressOpenCommand(program))
.action((...fnArgs) => {
debug('parsed Cypress open %o', fnArgs)
const options = parseVariableOpts(fnArgs, cliArgs)
debug('parsed options %o', options)
const casted = castCypressOptions(options)
debug('casted options %o', casted)
resolve(casted)
@@ -400,7 +448,7 @@ module.exports = {
showVersions(args)
})
addCypressOpenCommand(program)
maybeAddInspectFlags(addCypressOpenCommand(program))
.action((opts) => {
debug('opening Cypress')
require('./exec/open')
@@ -409,7 +457,7 @@ module.exports = {
.catch(util.logErrorExit1)
})
addCypressRunCommand(program)
maybeAddInspectFlags(addCypressRunCommand(program))
.action((...fnArgs) => {
debug('running Cypress with args %o', fnArgs)
require('./exec/run')
@@ -421,8 +469,8 @@ module.exports = {
program
.command('open-ct')
.usage('[options]')
.description('Opens Cypress component testing interactive mode.')
.option('-b, --browser <browser-path>', text('browserOpenMode'))
.description('Opens Cypress component testing interactive mode. Deprecated: use "open --component"')
.option('-b, --browser <browser-path>', text('browser'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('-d, --detached [bool]', text('detached'), coerceFalse)
@@ -433,6 +481,17 @@ module.exports = {
.option('--dev', text('dev'), coerceFalse)
.action((opts) => {
debug('opening Cypress')
const msg = `
${logSymbols.warning} Warning: open-ct is deprecated and will be removed in a future release.
Use \`cypress open --component\` instead.
`
logger.warn()
logger.warn(stripIndent(msg))
logger.warn()
require('./exec/open')
.start({ ...util.parseOpts(opts), testingType: 'component' })
.then(util.exit)
@@ -442,8 +501,8 @@ module.exports = {
program
.command('run-ct')
.usage('[options]')
.description('Runs all Cypress Component Testing suites')
.option('-b, --browser <browser-name-or-path>', text('browserRunMode'))
.description('Runs all Cypress component testing suites. Deprecated: use "run --component"')
.option('-b, --browser <browser-name-or-path>', text('browser'))
.option('--ci-build-id <id>', text('ciBuildId'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
@@ -465,6 +524,16 @@ module.exports = {
.option('--dev', text('dev'), coerceFalse)
.action((opts) => {
debug('running Cypress run-ct')
const msg = `
${logSymbols.warning} Warning: run-ct is deprecated and will be removed in a future release.
Use \`cypress run --component\` instead.
`
logger.warn()
logger.warn(stripIndent(msg))
logger.warn()
require('./exec/run')
.start({ ...util.parseOpts(opts), testingType: 'component' })
.then(util.exit)

View File

@@ -227,6 +227,21 @@ const invalidTestingType = {
solution: `Please provide a valid testingType. Valid test types are ${chalk.cyan('\'e2e\'')} and ${chalk.cyan('\'component\'')}.`,
}
const incompatibleTestTypeFlags = {
description: '`--e2e` and `--component` cannot both be passed.',
solution: 'Either pass `--e2e` or `--component`, but not both.',
}
const incompatibleTestingTypeAndFlag = {
description: 'Set a `testingType` and also passed `--e2e` or `--component` flags.',
solution: 'Either set `testingType` or pass a testing type flag, but not both.',
}
const invalidConfigFile = {
description: '`--config-file` cannot be false.',
solution: 'Either pass a relative path to a valid Cypress config file or remove this option.',
}
/**
* This error happens when CLI detects that the child Test Runner process
* was killed with a signal, like SIGBUS
@@ -420,5 +435,8 @@ module.exports = {
incompatibleHeadlessFlags,
invalidRunProjectPath,
invalidTestingType,
incompatibleTestTypeFlags,
incompatibleTestingTypeAndFlag,
invalidConfigFile,
},
}

View File

@@ -2,9 +2,19 @@ const debug = require('debug')('cypress:cli')
const util = require('../util')
const spawn = require('./spawn')
const verify = require('../tasks/verify')
const { processTestingType } = require('./shared')
const { processTestingType, checkConfigFile } = require('./shared')
const { exitWithError } = require('../errors')
const processOpenOptions = (options) => {
/**
* Maps options collected by the CLI
* and forms list of CLI arguments to the server.
*
* Note: there is lightweight validation, with errors
* thrown synchronously.
*
* @returns {string[]} list of CLI arguments
*/
const processOpenOptions = (options = {}) => {
if (!util.isInstalledGlobally() && !options.global && !options.project) {
options.project = process.cwd()
}
@@ -16,6 +26,7 @@ const processOpenOptions = (options) => {
}
if (options.configFile !== undefined) {
checkConfigFile(options)
args.push('--config-file', options.configFile)
}
@@ -35,7 +46,19 @@ const processOpenOptions = (options) => {
args.push('--project', options.project)
}
args.push(...processTestingType(options.testingType))
if (options.global) {
args.push('--global', options.global)
}
if (options.inspect) {
args.push('--inspect')
}
if (options.inspectBrk) {
args.push('--inspectBrk')
}
args.push(...processTestingType(options))
debug('opening from options %j', options)
debug('command line arguments %j', args)
@@ -46,14 +69,22 @@ const processOpenOptions = (options) => {
module.exports = {
processOpenOptions,
start (options = {}) {
const args = processOpenOptions(options)
function open () {
return spawn.start(args, {
dev: options.dev,
detached: Boolean(options.detached),
stdio: 'inherit',
})
try {
const args = processOpenOptions(options)
return spawn.start(args, {
dev: options.dev,
detached: Boolean(options.detached),
stdio: 'inherit',
})
} catch (err) {
if (err.details) {
return exitWithError(err.details)()
}
throw err
}
}
if (options.dev) {

View File

@@ -5,7 +5,7 @@ const util = require('../util')
const spawn = require('./spawn')
const verify = require('../tasks/verify')
const { exitWithError, errors } = require('../errors')
const { processTestingType, throwInvalidOptionError } = require('./shared')
const { processTestingType, throwInvalidOptionError, checkConfigFile } = require('./shared')
/**
* Typically a user passes a string path to the project.
@@ -58,6 +58,7 @@ const processRunOptions = (options = {}) => {
}
if (options.configFile !== undefined) {
checkConfigFile(options)
args.push('--config-file', options.configFile)
}
@@ -137,7 +138,15 @@ const processRunOptions = (options = {}) => {
args.push('--tag', options.tag)
}
args.push(...processTestingType(options.testingType))
if (options.inspect) {
args.push('--inspect')
}
if (options.inspectBrk) {
args.push('--inspectBrk')
}
args.push(...processTestingType(options))
return args
}
@@ -156,10 +165,14 @@ module.exports = {
})
function run () {
let args
try {
args = processRunOptions(options)
const args = processRunOptions(options)
debug('run to spawn.start args %j', args)
return spawn.start(args, {
dev: options.dev,
})
} catch (err) {
if (err.details) {
return exitWithError(err.details)()
@@ -167,12 +180,6 @@ module.exports = {
throw err
}
debug('run to spawn.start args %j', args)
return spawn.start(args, {
dev: options.dev,
})
}
if (options.dev) {

View File

@@ -23,23 +23,43 @@ const throwInvalidOptionError = (details) => {
* @param {string} testingType The type of tests being executed
* @returns {string[]} The array of new exec arguments
*/
const processTestingType = (testingType) => {
if (testingType) {
if (testingType === 'e2e') {
return ['--testing-type', 'e2e']
}
const processTestingType = (options) => {
if (options.e2e && options.component) {
return throwInvalidOptionError(errors.incompatibleTestTypeFlags)
}
if (testingType === 'component') {
return ['--testing-type', 'component']
}
if (options.testingType && (options.component || options.e2e)) {
return throwInvalidOptionError(errors.incompatibleTestTypeFlags)
}
if (options.testingType === 'component' || options.component || options.ct) {
return ['--testing-type', 'component']
}
if (options.testingType === 'e2e' || options.e2e) {
return ['--testing-type', 'e2e']
}
if (options.testingType) {
return throwInvalidOptionError(errors.invalidTestingType)
}
return []
}
/**
* Throws an error if configFile is string 'false' or boolean false
* @param {*} options
*/
const checkConfigFile = (options) => {
// CLI will parse as string, module API can pass in boolean
if (options.configFile === 'false' || options.configFile === false) {
throwInvalidOptionError(errors.invalidConfigFile)
}
}
module.exports = {
throwInvalidOptionError,
processTestingType,
checkConfigFile,
}

View File

@@ -171,8 +171,13 @@ module.exports = {
args.unshift(startScriptPath)
}
if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) {
args.unshift(process.env.CYPRESS_INTERNAL_DEV_DEBUG)
}
debug('spawn args %o %o', args, _.omit(stdioOptions, 'env'))
debug('spawning Cypress with executable: %s', executable)
const child = cp.spawn(executable, args, stdioOptions)
function resolveOn (event) {

View File

@@ -12,6 +12,7 @@ const debugXvfb = Debug('cypress:xvfb')
debug.Debug = debugXvfb.Debug = Debug
const xvfbOptions = {
displayNum: process.env.XVFB_DISPLAY_NUM,
timeout: 30000, // milliseconds
// need to explicitly define screen otherwise electron will crash
// https://github.com/cypress-io/cypress/issues/6184

View File

@@ -198,12 +198,15 @@ const parseOpts = (opts) => {
'cacheClear',
'cachePrune',
'ciBuildId',
'ct',
'component',
'config',
'configFile',
'cypressVersion',
'destination',
'detached',
'dev',
'e2e',
'exit',
'env',
'force',
@@ -211,6 +214,8 @@ const parseOpts = (opts) => {
'group',
'headed',
'headless',
'inspect',
'inspectBrk',
'key',
'path',
'parallel',
@@ -252,13 +257,17 @@ const getApplicationDataFolder = (...paths) => {
const { env } = process
// allow overriding the app_data folder
const folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'
let folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'
const PRODUCT_NAME = pkg.productName || pkg.name
const OS_DATA_PATH = ospath.data()
const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME)
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
folder = `${folder}-e2e-test`
}
const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths)
return p

View File

@@ -101,8 +101,13 @@
"bin",
"lib",
"index.js",
"index.mjs",
"types/**/*.d.ts",
"types/net-stubbing.ts"
"types/net-stubbing.ts",
"mount-utils",
"vue",
"react",
"vue2"
],
"bin": {
"cypress": "bin/cypress"
@@ -110,5 +115,30 @@
"engines": {
"node": ">=12.0.0"
},
"types": "types"
"types": "types",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
},
"./vue": {
"import": "./vue/dist/cypress-vue.esm-bundler.js",
"require": "./vue/dist/cypress-vue.cjs.js"
},
"./vue2": {
"import": "./vue2/dist/cypress-vue2.esm-bundler.js",
"require": "./vue2/dist/cypress-vue2.cjs.js"
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
},
"./react": {
"import": "./react/dist/cypress-react.esm-bundler.js",
"require": "./react/dist/cypress-react.cjs.js"
},
"./mount-utils": {
"require": "./mount-utils/dist/index.js"
}
}
}

View File

@@ -1,11 +0,0 @@
# cypress.json file schema
This [schema file](cypress.schema.json) provides definitions for
[various Cypress configuration](https://on.cypress.io/configuration) properties
inside `cypress.json` file.
When editing `cypress.json` the schema file is used by
[Schemastore](http://schemastore.org/json/) to provide IntelliSense and
validation. Works in modern text editors like VSCode, WebStorm and Atom with help with the
help of [autocomplete-json plugin](https://atom.io/packages/autocomplete-json).
See [VSCode documentation](https://code.visualstudio.com/docs/languages/json)

View File

@@ -1,383 +0,0 @@
{
"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",
"definitions": {
"cypressConfig": {
"properties": {
"baseUrl": {
"type": "string",
"default": null,
"description": "Url used as prefix for cy.visit() or cy.request() commands url. Example http://localhost:3030 or https://test.my-domain.com"
},
"env": {
"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"
},
"slowTestThreshold": {
"type": "number",
"default": 10000,
"description": "Slow test threshold in milliseconds. Only affects the visual output of some reporters. For example, the spec reporter will display the test time in yellow if over the threshold. See https://on.cypress.io/configuration#Timeouts"
},
"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": "system",
"description": "DEPRECATED: If set to 'bundled', Cypress will use the Node version bundled with Cypress. Otherwise, Cypress will use the Node version that was used to launch the Cypress. This Node version is used when executing your plugins file and building spec files."
},
"experimentalInteractiveRunEvents": {
"type": "boolean",
"default": false,
"description": "Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode."
},
"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."
},
"experimentalSessionSupport": {
"type": "boolean",
"default": false,
"description": "Enable experimental session support. See https://on.cypress.io/session"
},
"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()"
},
"clientCertificates": {
"description": "Defines client certificates to use when sending requests to the specified URLs",
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"description": "Requests for URLs matching this minimatch pattern will use the supplied client certificate",
"type": "string"
},
"ca": {
"description": "Path(s) to CA file(s) to validate certs against, relative to project root",
"type": "array",
"items": {
"type": "string"
}
},
"certs": {
"type": "array",
"items": {
"anyOf": [
{
"description": "PEM file specific properties",
"type": "object",
"properties": {
"cert": {
"description": "Path to the certificate, relative to project root",
"type": "string"
},
"key": {
"description": "Path to the private key, relative to project root",
"type": "string"
},
"passphrase": {
"description": "(Optional) File path to a UTF-8 text file containing the passphrase for the key, relative to project root",
"type": "string"
}
},
"required": [
"cert",
"key"
]
},
{
"description": "PFX file specific properties",
"type": "object",
"properties": {
"pfx": {
"description": "Path to the certificate container, relative to project root",
"type": "string"
},
"passphrase": {
"description": "(Optional) File path to a UTF-8 text file containing the passphrase for the container, relative to project root",
"type": "string"
}
},
"required": [
"pfx"
]
}
]
}
}
},
"required": [
"url",
"certs"
]
}
}
}
}
},
"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

@@ -1,8 +1,13 @@
const shelljs = require('shelljs')
const fs = require('fs-extra')
const path = require('path')
const { includeTypes } = require('./utils')
shelljs.rm('-rf', 'build')
fs.removeSync(path.join(__dirname, '..', 'build'))
includeTypes.map((m) => {
shelljs.rm('-rf', `types/${m}`)
includeTypes.forEach((folder) => {
try {
fs.removeSync(path.join(__dirname, '..', 'types', folder))
} catch (e) {
//
}
})

22
cli/scripts/post-build.js Normal file
View File

@@ -0,0 +1,22 @@
const shell = require('shelljs')
const { resolve } = require('path')
shell.set('-v') // verbose
shell.set('-e') // any error is fatal
// For each npm package that is re-published via cypress/*
// make sure that it is also copied into the build directory
const npmModulesToCopy = [
'mount-utils',
'react',
'vue',
'vue2',
]
npmModulesToCopy.forEach((folder) => {
// cli/mount-utils => cli/build/mount-utils
const from = resolve(`${__dirname}/../${folder}`)
const to = resolve(`${__dirname}/../build/${folder}`)
shell.cp('-R', from, to)
})

View File

@@ -2,13 +2,14 @@
// @ts-check
/* eslint-disable no-console */
const fs = require('fs-extra')
const { includeTypes } = require('./utils')
const shell = require('shelljs')
const fs = require('fs')
const { join } = require('path')
const resolvePkg = require('resolve-pkg')
require('./clean')
shell.set('-v') // verbose
shell.set('-e') // any error is fatal
@@ -19,10 +20,12 @@ shell.set('-e') // any error is fatal
// yet we do not install "@types/.." packages with "npm install cypress"
// because they can conflict with user's own libraries
includeTypes.forEach((folder) => {
const source = resolvePkg(`@types/${folder}`, { cwd: join(__dirname, '..', '..') })
fs.ensureDirSync(join(__dirname, '..', 'types'))
shell.cp('-R', source, 'types')
includeTypes.forEach((folder) => {
const source = resolvePkg(`@types/${folder}`, { cwd: __dirname })
fs.copySync(source, join(__dirname, '..', 'types', folder))
})
// jQuery v3.3.x includes "dist" folder that just references back to itself
@@ -72,7 +75,7 @@ shell.sed('-i', 'from \'sinon\';', 'from \'../sinon\';', sinonChaiFilename)
// copy experimental network stubbing type definitions
// so users can import: `import 'cypress/types/net-stubbing'`
shell.cp(resolvePkg('@packages/net-stubbing/lib/external-types.ts'), 'types/net-stubbing.ts')
fs.copySync(resolvePkg('@packages/net-stubbing/lib/external-types.ts'), 'types/net-stubbing.ts')
// https://github.com/cypress-io/cypress/issues/18069
// To avoid type clashes, some files should be commented out entirely by patch-package
@@ -90,7 +93,7 @@ filesToUncomment.forEach((file) => {
const str = fs.readFileSync(filePath).toString()
const result = str.split('\n').map((line) => {
return line.startsWith('// ') ? line.substring(3) : line
return line.startsWith('//z ') ? line.substring(4) : line
}).join('\n')
fs.writeFileSync(filePath, result)

View File

@@ -22,5 +22,10 @@ includeTypes.forEach((folder) => {
shell.cp('-R', source, 'build/types')
})
// TODO: Add a typescript or rollup build step
// The only reason start-build.js exists
// is because the cli package does not have an actual
// build process to compile index.js and lib
shell.exec('babel lib -d build/lib')
shell.exec('babel index.js -o build/index.js')
shell.cp('index.mjs', 'build/index.mjs')

View File

@@ -630,12 +630,24 @@ describe('cli', () => {
})
it('spawns server with correct args for component-testing', () => {
this.exec('open --component --dev')
expect(spawn.start.firstCall.args[0]).to.include('--testing-type')
expect(spawn.start.firstCall.args[0]).to.include('component')
})
it('spawns server with correct args for depricated component-testing command', () => {
this.exec('open-ct --dev')
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 --component --dev')
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 depricated component-testing command', () => {
this.exec('run-ct --dev')
expect(spawn.start.firstCall.args[0]).to.include('--testing-type')
expect(spawn.start.firstCall.args[0]).to.include('component')

View File

@@ -53,18 +53,6 @@ describe('cypress', function () {
expect(args).to.deep.eq({ config: JSON.stringify(config) })
})
})
it('passes configFile: false', () => {
const opts = {
configFile: false,
}
return cypress.open(opts)
.then(getStartArgs)
.then((args) => {
expect(args).to.deep.eq(opts)
})
})
})
context('.run fails to write results file', function () {
@@ -152,18 +140,6 @@ describe('cypress', function () {
return cypress.run().then(snapshot)
})
it('passes configFile: false', () => {
const opts = {
configFile: false,
}
return cypress.run(opts)
.then(getStartArgs)
.then((args) => {
expect(args).to.deep.eq(opts)
})
})
it('rejects if project is an empty string', () => {
return expect(cypress.run({ project: '' })).to.be.rejected
})
@@ -223,30 +199,12 @@ describe('cypress', function () {
})
})
it('coerces --config-file false to boolean', async () => {
const args = 'cypress run --config-file false'.split(' ')
it('coerces --config-file cypress.config.js to string', async () => {
const args = 'cypress run --config-file cypress.config.js'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
configFile: false,
})
})
it('coerces --config-file cypress.json to string', async () => {
const args = 'cypress run --config-file cypress.json'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
configFile: 'cypress.json',
})
})
it('parses config file false', async () => {
const args = 'cypress run --config-file false'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
configFile: false,
configFile: 'cypress.config.js',
})
})

View File

@@ -56,20 +56,11 @@ describe('exec open', function () {
})
})
it('spawns with --config-file false', function () {
return open.start({ configFile: false })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--config-file', false],
)
})
})
it('spawns with --config-file set', function () {
return open.start({ configFile: 'special-cypress.json' })
return open.start({ configFile: 'special-cypress.config.js' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--config-file', 'special-cypress.json'],
['--config-file', 'special-cypress.config.js'],
)
})
})
@@ -142,7 +133,11 @@ describe('exec open', function () {
})
it('throws if --testing-type is invalid', () => {
expect(() => open.start({ testingType: 'randomTestingType' })).to.throw()
expect(() => open.processOpenOptions({ testingType: 'randomTestingType' })).to.throw()
})
it('throws if --config-file is false', () => {
expect(() => open.processOpenOptions({ configFile: 'false' })).to.throw()
})
})
})

View File

@@ -53,14 +53,6 @@ describe('exec run', function () {
snapshot(args)
})
it('passes --config-file false option', () => {
const args = run.processRunOptions({
configFile: false,
})
snapshot(args)
})
it('does not allow setting paradoxical --headed and --headless flags', () => {
os.platform.returns('linux')
process.exit.returns()
@@ -104,6 +96,18 @@ describe('exec run', function () {
it('throws if testingType is invalid', () => {
expect(() => run.processRunOptions({ testingType: 'randomTestingType' })).to.throw()
})
it('throws if both e2e and component are set', () => {
expect(() => run.processRunOptions({ e2e: true, component: true })).to.throw()
})
it('throws if both testingType and component are set', () => {
expect(() => run.processRunOptions({ testingType: 'component', component: true })).to.throw()
})
it('throws if --config-file is false', () => {
expect(() => run.processRunOptions({ configFile: 'false' })).to.throw()
})
})
context('.start', function () {
@@ -140,20 +144,11 @@ describe('exec run', function () {
})
})
it('spawns with --config-file false', function () {
return run.start({ configFile: false })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--run-project', process.cwd(), '--config-file', false],
)
})
})
it('spawns with --config-file set', function () {
return run.start({ configFile: 'special-cypress.json' })
return run.start({ configFile: 'special-cypress.config.js' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--run-project', process.cwd(), '--config-file', 'special-cypress.json'],
['--run-project', process.cwd(), '--config-file', 'special-cypress.config.js'],
)
})
})
@@ -190,6 +185,20 @@ describe('exec run', function () {
})
})
it('spawns with --testing-type e2e when given --e2e', function () {
return run.start({ e2e: true })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--testing-type', 'e2e'])
})
})
it('spawns with --testing-type component when given --component', function () {
return run.start({ component: true })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--testing-type', 'component'])
})
})
it('spawns with --tag value', function () {
return run.start({ tag: 'nightly' })
.then(() => {

View File

@@ -37,7 +37,7 @@ declare namespace CypressCommandLine {
interface CypressRunOptions extends CypressCommonOptions {
/**
* Specify browser to run tests in, either by name or by filesystem path
*/
*/
browser: string
/**
* Specify a unique identifier for a run to enable grouping or parallelization
@@ -91,10 +91,6 @@ declare namespace CypressCommandLine {
* Specify mocha reporter options
*/
reporterOptions: any
/**
* Slow test threshold in milliseconds. Only affects the visual output of some reporters. For example, the spec reporter will display the test time in yellow if over the threshold.
*/
slowTestThreshold: number
/**
* Specify the specs to run
*/
@@ -146,11 +142,9 @@ declare namespace CypressCommandLine {
/**
* Path to the config file to be used.
*
* If `false` is passed, no config file will be used.
*
* @default "cypress.json"
* @default "cypress.config.{ts|js}"
*/
configFile: string | false
configFile: string
/**
* Specify environment variables.
* TODO: isn't this duplicate of config.env?!
@@ -395,7 +389,7 @@ declare module 'cypress' {
* @param {Cypress.ConfigOptions} config
* @returns {Cypress.ConfigOptions} the configuration passed in parameter
*/
defineConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions
defineConfig<ComponentDevServerOpts = any>(config: Cypress.ConfigOptions<ComponentDevServerOpts>): Cypress.ConfigOptions
}
// export Cypress NPM module interface

280
cli/types/cypress.d.ts vendored
View File

@@ -174,7 +174,7 @@ declare namespace Cypress {
/**
* Spec type for the given test. "integration" is the default, but
* tests run using `open-ct` will be "component"
* tests run using `open --component` will be "component"
*
* @see https://on.cypress.io/experiments
*/
@@ -204,7 +204,7 @@ declare namespace Cypress {
/**
* The configuration for Cypress.
*/
type Config = ResolvedConfigOptions & RuntimeConfigOptions
type Config = ResolvedConfigOptions & RuntimeConfigOptions & RuntimeServerConfigOptions
/**
* Several libraries are bundled with Cypress by default.
@@ -398,7 +398,7 @@ declare namespace Cypress {
// no real way to type without generics
/**
* Returns all environment variables set with CYPRESS_ prefix or in "env" object in "cypress.json"
* Returns all environment variables set with CYPRESS_ prefix or in "env" object in "cypress.config.{ts|js}"
*
* @see https://on.cypress.io/env
*/
@@ -407,7 +407,7 @@ declare namespace Cypress {
* Returns specific environment variable or undefined
* @see https://on.cypress.io/env
* @example
* // cypress.json
* // cypress.config.js
* { "env": { "foo": "bar" } }
* Cypress.env("foo") // => bar
*/
@@ -676,7 +676,7 @@ declare namespace Cypress {
* @see https://on.cypress.io/get
* @example
```
// Get the aliased todos elements
// Get the aliased 'todos' elements
cy.get('ul#todos').as('todos')
//...hack hack hack...
// later retrieve the todos
@@ -724,9 +724,9 @@ declare namespace Cypress {
*
* @see https://on.cypress.io/check
* @example
* // Select the radio with the value of US
* // Select the radio with the value of 'US'
* cy.get('[type="radio"]').check('US')
* // Check the checkboxes with the values ga and ca
* // Check the checkboxes with the values 'ga' and 'ca'
* cy.get('[type="checkbox"]').check(['ga', 'ca'])
*/
check(value: string | string[], options?: Partial<CheckOptions>): Chainable<Subject>
@@ -750,7 +750,7 @@ declare namespace Cypress {
/**
* Clear a specific browser cookie.
* Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldnt need to use this command unless youre using it to clear a specific cookie inside a single test.
* Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear a specific cookie inside a single test.
*
* @see https://on.cypress.io/clearcookie
*/
@@ -758,7 +758,7 @@ declare namespace Cypress {
/**
* Clear all browser cookies.
* Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldnt need to use this command unless youre using it to clear a specific cookie inside a single test.
* Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear a specific cookie inside a single test.
*
* @see https://on.cypress.io/clearcookies
*/
@@ -767,7 +767,7 @@ declare namespace Cypress {
/**
* Clear data in local storage.
* Cypress automatically runs this command before each test to prevent state from being
* shared across tests. You shouldnt need to use this command unless youre using it
* shared across tests. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test. Yields `localStorage` object.
*
* @see https://on.cypress.io/clearlocalstorage
@@ -799,7 +799,7 @@ declare namespace Cypress {
/**
* Clear data in local storage.
* Cypress automatically runs this command before each test to prevent state from being
* shared across tests. You shouldnt need to use this command unless youre using it
* shared across tests. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test. Yields `localStorage` object.
*
* @see https://on.cypress.io/clearlocalstorage
@@ -814,7 +814,7 @@ declare namespace Cypress {
/**
* Clear data in local storage.
* Cypress automatically runs this command before each test to prevent state from being
* shared across tests. You shouldnt need to use this command unless youre using it
* shared across tests. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test. Yields `localStorage` object.
*
* @see https://on.cypress.io/clearlocalstorage
@@ -851,8 +851,8 @@ declare namespace Cypress {
/**
* Click a DOM element at specific coordinates
*
* @param {number} x The distance in pixels from the elements left to issue the click.
* @param {number} y The distance in pixels from the elements top to issue the click.
* @param {number} x The distance in pixels from the element's left to issue the click.
* @param {number} y The distance in pixels from the element's top to issue the click.
* @see https://on.cypress.io/click
* @example
```
@@ -1007,8 +1007,8 @@ declare namespace Cypress {
/**
* Double-click a DOM element at specific coordinates
*
* @param {number} x The distance in pixels from the elements left to issue the click.
* @param {number} y The distance in pixels from the elements top to issue the click.
* @param {number} x The distance in pixels from the element's left to issue the click.
* @param {number} y The distance in pixels from the element's top to issue the click.
* @see https://on.cypress.io/dblclick
* @example
```
@@ -1037,8 +1037,8 @@ declare namespace Cypress {
/**
* Right-click a DOM element at specific coordinates
*
* @param {number} x The distance in pixels from the elements left to issue the click.
* @param {number} y The distance in pixels from the elements top to issue the click.
* @param {number} x The distance in pixels from the element's left to issue the click.
* @param {number} y The distance in pixels from the element's top to issue the click.
* @see https://on.cypress.io/rightclick
* @example
```
@@ -1141,7 +1141,7 @@ declare namespace Cypress {
*
* @see https://on.cypress.io/find
* @example
* // Find the lis within the nav
* // Find the li's within the nav
* cy.get('.left-nav>.nav').find('>li')
*/
find<E extends Node = HTMLElement>(selector: string, options?: Partial<Loggable & Timeoutable & Shadow>): Chainable<JQuery<E>>
@@ -1213,7 +1213,7 @@ declare namespace Cypress {
* Get one or more DOM elements by alias.
* @see https://on.cypress.io/get#Alias
* @example
* // Get the aliased todos elements
* // Get the aliased 'todos' elements
* cy.get('ul#todos').as('todos')
* //...hack hack hack...
* //later retrieve the todos
@@ -1236,7 +1236,7 @@ declare namespace Cypress {
getCookies(options?: Partial<Loggable & Timeoutable>): Chainable<Cookie[]>
/**
* Navigate back or forward to the previous or next URL in the browsers history.
* Navigate back or forward to the previous or next URL in the browser's history.
*
* @see https://on.cypress.io/go
*/
@@ -1281,7 +1281,7 @@ declare namespace Cypress {
invoke(propertyPath: string, ...args: any[]): Chainable
/**
* Get a propertys value on the previously yielded subject.
* Get a property's value on the previously yielded subject.
*
* @see https://on.cypress.io/its
* @example
@@ -1855,7 +1855,7 @@ declare namespace Cypress {
*/
stub(): Agent<sinon.SinonStub>
/**
* Stubs all the objects methods.
* Stubs all the object's methods.
*
* @see https://on.cypress.io/stub
* @example
@@ -2113,11 +2113,11 @@ declare namespace Cypress {
* @example
* // Unchecks checkbox element
* cy.get('[type="checkbox"]').uncheck()
* // Uncheck element with the id saveUserName
* // Uncheck element with the id 'saveUserName'
* cy.get('#saveUserName').uncheck()
* // Uncheck all checkboxes
* cy.get(':checkbox').uncheck()
* // Uncheck the checkbox with the value of ga
* // Uncheck the checkbox with the value of 'ga'
* cy.get('input[type="checkbox"]').uncheck(['ga'])
*/
uncheck(options?: Partial<CheckOptions>): Chainable<Subject>
@@ -2126,7 +2126,7 @@ declare namespace Cypress {
*
* @see https://on.cypress.io/uncheck
* @example
* // Uncheck the checkbox with the value of ga
* // Uncheck the checkbox with the value of 'ga'
* cy.get('input[type="checkbox"]').uncheck('ga')
*/
uncheck(value: string, options?: Partial<CheckOptions>): Chainable<Subject>
@@ -2135,7 +2135,7 @@ declare namespace Cypress {
*
* @see https://on.cypress.io/uncheck
* @example
* // Uncheck the checkbox with the value of ga, 'ma'
* // Uncheck the checkbox with the value of 'ga', 'ma'
* cy.get('input[type="checkbox"]').uncheck(['ga', 'ma'])
*/
uncheck(values: string[], options?: Partial<CheckOptions>): Chainable<Subject>
@@ -2625,9 +2625,9 @@ declare namespace Cypress {
certs: PEMCert[] | PFXCert[]
}
interface ResolvedConfigOptions {
interface ResolvedConfigOptions<ComponentDevServerOpts = any> {
/**
* Url used as prefix for [cy.visit()](https://on.cypress.io/visit) or [cy.request()](https://on.cypress.io/request) commands url
* Url used as prefix for [cy.visit()](https://on.cypress.io/visit) or [cy.request()](https://on.cypress.io/request) command's url
* @default null
*/
baseUrl: string | null
@@ -2640,7 +2640,7 @@ declare namespace Cypress {
* 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.
* @default "*.hot-update.js"
*/
ignoreTestFiles: string | string[]
excludeSpecPattern: string | string[]
/**
* 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.
* @default 50
@@ -2711,11 +2711,6 @@ declare namespace Cypress {
* @default "cypress/fixtures"
*/
fixturesFolder: string | false
/**
* Path to folder containing integration test files
* @default "cypress/integration"
*/
integrationFolder: string
/**
* Path to folder where files downloaded during a test are saved
* @default "cypress/downloads"
@@ -2726,11 +2721,6 @@ declare namespace Cypress {
* @default "bundled"
*/
nodeVersion: 'system' | 'bundled'
/**
* Path to plugins file. (Pass false to disable)
* @default "cypress/plugins/index.js"
*/
pluginsFile: string | false
/**
* The application under test cannot redirect more than this limit.
* @default 20
@@ -2752,13 +2742,13 @@ declare namespace Cypress {
*/
screenshotOnRunFailure: boolean
/**
* Path to folder where screenshots will be saved from [cy.screenshot()](https://on.cypress.io/screenshot) command or after a headless or CI runs test failure
* Path to folder where screenshots will be saved from [cy.screenshot()](https://on.cypress.io/screenshot) command or after a headless or CI run's test failure
* @default "cypress/screenshots"
*/
screenshotsFolder: string | false
/**
* Path to file to load before test files load. This file is compiled and bundled. (Pass false to disable)
* @default "cypress/support/index.js"
* @default "cypress/support/{e2e|component}.js"
*/
supportFile: string | false
/**
@@ -2782,7 +2772,7 @@ declare namespace Cypress {
*/
video: boolean
/**
* Whether Cypress will upload the video to the Dashboard even if all tests are passing. This applies only when recording your runs to the Dashboard. Turn this off if youd like the video uploaded only when there are failing tests.
* Whether Cypress will upload the video to the Dashboard even if all tests are passing. This applies only when recording your runs to the Dashboard. Turn this off if you'd like the video uploaded only when there are failing tests.
* @default true
*/
videoUploadOnPasses: boolean
@@ -2792,12 +2782,12 @@ declare namespace Cypress {
*/
chromeWebSecurity: boolean
/**
* Default height in pixels for the application under tests viewport (Override with [cy.viewport()](https://on.cypress.io/viewport) command)
* Default height in pixels for the application under tests' viewport (Override with [cy.viewport()](https://on.cypress.io/viewport) command)
* @default 660
*/
viewportHeight: number
/**
* Default width in pixels for the application under tests viewport. (Override with [cy.viewport()](https://on.cypress.io/viewport) command)
* Default width in pixels for the application under tests' viewport. (Override with [cy.viewport()](https://on.cypress.io/viewport) command)
* @default 1000
*/
viewportWidth: number
@@ -2831,11 +2821,6 @@ declare namespace Cypress {
* @default false
*/
experimentalSourceRewriting: boolean
/**
* Generate and save commands directly to your test suite by interacting with your app as an end user would.
* @default false
*/
experimentalStudio: boolean
/**
* Number of times to retry a failed test.
* If a number is set, tests will retry in both runMode and openMode.
@@ -2845,7 +2830,7 @@ declare namespace Cypress {
retries: Nullable<number | { runMode?: Nullable<number>, openMode?: Nullable<number> }>
/**
* Enables including elements within the shadow DOM when using querying
* commands (e.g. cy.get(), cy.find()). Can be set globally in cypress.json,
* commands (e.g. cy.get(), cy.find()). Can be set globally in cypress.config.{ts|js},
* per-suite or per-test in the test configuration object, or programmatically
* with Cypress.config()
* @default false
@@ -2856,10 +2841,6 @@ declare namespace Cypress {
* The list of hosts to be blocked
*/
blockHosts: null | string | string[]
/**
* Path to folder containing component test files.
*/
componentFolder: false | string
/**
* A unique ID for the project used for recording
*/
@@ -2871,7 +2852,7 @@ declare namespace Cypress {
/**
* Glob pattern to determine what test files to load.
*/
testFiles: string | string[]
specPattern: string | string[]
/**
* The user agent the browser sends in all request headers.
*/
@@ -2885,34 +2866,41 @@ declare namespace Cypress {
* Override default config options for Component Testing runner.
* @default {}
*/
component: Omit<ResolvedConfigOptions, TestingType>
component: ComponentConfigOptions<ComponentDevServerOpts>
/**
* Override default config options for E2E Testing runner.
* @default {}
*/
e2e: Omit<ResolvedConfigOptions, TestingType>
e2e: Omit<CoreConfigOptions, 'indexHtmlFile'>
/**
* An array of objects defining the certificates
*/
clientCertificates: ClientCertificate[]
/**
* Handle Cypress plugins
*/
setupNodeEvents: (on: PluginEvents, config: PluginConfigOptions) => Promise<PluginConfigOptions | void> | PluginConfigOptions | void
indexHtmlFile: string
}
/**
* Options appended to config object on runtime.
*/
interface RuntimeConfigOptions {
interface RuntimeConfigOptions extends Partial<RuntimeServerConfigOptions> {
/**
* Absolute path to the config file (default: <projectRoot>/cypress.config.{ts|js})
*/
configFile: string
/**
* CPU architecture, from Node `os.arch()`
*
* @see https://nodejs.org/api/os.html#os_os_arch
*/
arch: string
/**
* The browser Cypress is running on.
*/
browser: Browser
/**
* Available browsers found on your system.
*/
@@ -2941,21 +2929,32 @@ declare namespace Cypress {
*/
version: string
// Internal or Unlisted at server/lib/config_options
namespace: string
projectRoot: string
devServerPublicPathRoute: string
cypressBinaryRoot: string
}
/**
* Optional options added before the server starts
*/
interface RuntimeServerConfigOptions {
/**
* The browser Cypress is running on.
*/
browser: Browser
// Internal or Unlisted at server/lib/config_options
autoOpen: boolean
browserUrl: string
clientRoute: string
configFile: string
cypressEnv: string
devServerPublicPathRoute: string
isNewProject: boolean
isTextTerminal: boolean
morgan: boolean
namespace: string
parentTestsFolder: string
parentTestsFolderDisplay: string
projectName: string
projectRoot: string
proxyUrl: string
remote: RemoteState
report: boolean
@@ -2970,7 +2969,7 @@ declare namespace Cypress {
xhrUrl: string
}
interface TestConfigOverrides extends Partial<Pick<ConfigOptions, 'animationDistanceThreshold' | 'baseUrl' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>> {
interface TestConfigOverrides extends Partial<Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>> {
browser?: IsBrowserMatcher | IsBrowserMatcher[]
keystrokeDelay?: number
}
@@ -2979,13 +2978,54 @@ declare namespace Cypress {
* All configuration items are optional.
*/
type CoreConfigOptions = Partial<Omit<ResolvedConfigOptions, TestingType>>
type ConfigOptions = CoreConfigOptions & { e2e?: CoreConfigOptions, component?: CoreConfigOptions }
interface PluginConfigOptions extends ResolvedConfigOptions {
/**
* Absolute path to the config file (default: <projectRoot>/cypress.json) or false
*/
configFile: string | false
interface DefineDevServerConfig {
// This interface can be extended by the user, to inject the types for their
// preferred bundler: e.g.
//
// import type * as webpack from 'webpack'
//
// declare global {
// namespace Cypress {
// interface DefineDevServerConfig {
// webpackConfig?: webpack.Configuration
// }
// }
// }
[key: string]: any
}
type PickConfigOpt<T> = T extends keyof DefineDevServerConfig ? DefineDevServerConfig[T] : any
type DevServerFn<ComponentDevServerOpts = any> = (cypressDevServerConfig: DevServerConfig, devServerConfig: ComponentDevServerOpts) => ResolvedDevServerConfig | Promise<ResolvedDevServerConfig>
type DevServerConfigObject = {
bundler: 'webpack'
framework: 'react' | 'vue' | 'vue-cli' | 'nuxt' | 'create-react-app'
webpackConfig?: PickConfigOpt<'webpackConfig'>
} | {
bundler: 'vite'
framework: 'react' | 'vue'
viteConfig?: Omit<Exclude<PickConfigOpt<'viteConfig'>, undefined>, 'base' | 'root'>
}
interface ComponentConfigOptions<ComponentDevServerOpts = any> extends Omit<CoreConfigOptions, 'baseUrl'> {
devServer: DevServerFn<ComponentDevServerOpts> | DevServerConfigObject
devServerConfig?: ComponentDevServerOpts
}
/**
* Config options that can be assigned on cypress.config.{ts|js} file
*/
type UserConfigOptions<ComponentDevServerOpts = any> = Omit<ResolvedConfigOptions<ComponentDevServerOpts>, 'baseUrl' | 'excludeSpecPattern' | 'supportFile' | 'specPattern' | 'indexHtmlFile'>
/**
* Takes ComponentDevServerOpts to track the signature of the devServerConfig for the provided `devServer`,
* so we have proper completion for `devServerConfig`
*/
type ConfigOptions<ComponentDevServerOpts = any> = Partial<UserConfigOptions<ComponentDevServerOpts>>
interface PluginConfigOptions extends ResolvedConfigOptions, RuntimeConfigOptions {
/**
* Absolute path to the root of the project
*/
@@ -3279,7 +3319,7 @@ declare namespace Cypress {
/**
* Cypress will automatically apply the right authorization headers
* if youre attempting to visit an application that requires
* if you're attempting to visit an application that requires
* Basic Authentication.
*
* @example
@@ -3350,7 +3390,7 @@ declare namespace Cypress {
interface Chainer<Subject> {
// chai
/**
* Asserts that the targets `type` is equal to the given string type.
* Asserts that the target's `type` is equal to the given string type.
* Types are case insensitive. See the `type-detect` project page for info on the type detection algorithm:
* https://github.com/chaijs/type-detect.
* @example
@@ -3369,7 +3409,7 @@ declare namespace Cypress {
*/
(chainer: 'be.above', value: number | Date): Chainable<Subject>
/**
* Asserts that the targets `type` is equal to the given string type.
* Asserts that the target's `type` is equal to the given string type.
* Types are case insensitive. See the `type-detect` project page for info on the type detection algorithm:
* https://github.com/chaijs/type-detect.
* @example
@@ -3406,7 +3446,7 @@ declare namespace Cypress {
*/
(chainer: 'be.arguments'): Chainable<Subject>
/**
* Asserts that the target is a number thats within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* Asserts that the target is a number that's within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* @example
* cy.wrap(5.1).should('be.approximately', 5, 0.5)
* @alias closeTo
@@ -3415,7 +3455,7 @@ declare namespace Cypress {
*/
(chainer: 'be.approximately', value: number, delta: number): Chainable<Subject>
/**
* Asserts that the target is a number thats within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* Asserts that the target is a number that's within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* @example
* cy.wrap(5.1).should('be.closeTo', 5, 0.5)
* @see http://chaijs.com/api/bdd/#method_closeto
@@ -3423,7 +3463,7 @@ declare namespace Cypress {
*/
(chainer: 'be.closeTo', value: number, delta: number): Chainable<Subject>
/**
* When the target is a string or array, .empty asserts that the targets length property is strictly (===) equal to 0
* When the target is a string or array, .empty asserts that the target's length property is strictly (===) equal to 0
* @example
* cy.wrap([]).should('be.empty')
* cy.wrap('').should('be.empty')
@@ -3590,7 +3630,7 @@ declare namespace Cypress {
(chainer: 'contain', value: any): Chainable<Subject>
/**
* When one argument is provided, `.decrease` asserts that the given function `subject` returns a lesser number when it's invoked after invoking the target function compared to when it's invoked beforehand.
* `.decrease` also causes all `.by` assertions that follow in the chain to assert how much lesser of a number is returned. it's often best to assert that the return value decreased by the expected amount, rather than asserting it decreased by any amount.
* `.decrease` also causes all `.by` assertions that follow in the chain to assert how much lesser of a number is returned. It's often best to assert that the return value decreased by the expected amount, rather than asserting it decreased by any amount.
* @example
* let val = 1
* function subtractTwo() { val -= 2 }
@@ -3701,7 +3741,7 @@ declare namespace Cypress {
*/
(chainer: 'have.deep.property', value: string, obj: object): Chainable<Subject>
/**
* Asserts that the targets `length` property is equal to the given number `n`.
* Asserts that the target's `length` property is equal to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length', 3)
* cy.wrap('foo').should('have.length', 3)
@@ -3711,7 +3751,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length' | 'have.lengthOf', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is greater than to the given number `n`.
* Asserts that the target's `length` property is greater than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.greaterThan', 2)
* cy.wrap('foo').should('have.length.greaterThan', 2)
@@ -3720,7 +3760,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length.greaterThan' | 'have.lengthOf.greaterThan', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is greater than to the given number `n`.
* Asserts that the target's `length` property is greater than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.gt', 2)
* cy.wrap('foo').should('have.length.gt', 2)
@@ -3729,7 +3769,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length.gt' | 'have.lengthOf.gt' | 'have.length.above' | 'have.lengthOf.above', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is greater than or equal to the given number `n`.
* Asserts that the target's `length` property is greater than or equal to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.gte', 2)
* cy.wrap('foo').should('have.length.gte', 2)
@@ -3738,7 +3778,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length.gte' | 'have.lengthOf.gte' | 'have.length.at.least' | 'have.lengthOf.at.least', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is less than to the given number `n`.
* Asserts that the target's `length` property is less than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.lessThan', 4)
* cy.wrap('foo').should('have.length.lessThan', 4)
@@ -3747,7 +3787,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length.lessThan' | 'have.lengthOf.lessThan', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is less than to the given number `n`.
* Asserts that the target's `length` property is less than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.lt', 4)
* cy.wrap('foo').should('have.length.lt', 4)
@@ -3756,7 +3796,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length.lt' | 'have.lengthOf.lt' | 'have.length.below' | 'have.lengthOf.below', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is less than or equal to the given number `n`.
* Asserts that the target's `length` property is less than or equal to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.lte', 4)
* cy.wrap('foo').should('have.length.lte', 4)
@@ -3765,7 +3805,7 @@ declare namespace Cypress {
*/
(chainer: 'have.length.lte' | 'have.lengthOf.lte' | 'have.length.at.most' | 'have.lengthOf.at.most', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is within `start` and `finish`.
* Asserts that the target's `length` property is within `start` and `finish`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.within', 1, 5)
* @see http://chaijs.com/api/bdd/#method_lengthof
@@ -3842,9 +3882,9 @@ declare namespace Cypress {
* When one argument is provided, `.increase` asserts that the given function `subject` returns a greater number when it's
* invoked after invoking the target function compared to when it's invoked beforehand.
* `.increase` also causes all `.by` assertions that follow in the chain to assert how much greater of a number is returned.
* it's often best to assert that the return value increased by the expected amount, rather than asserting it increased by any amount.
* It's often best to assert that the return value increased by the expected amount, rather than asserting it increased by any amount.
*
* When two arguments are provided, `.increase` asserts that the value of the given object `subject`s `prop` property is greater after
* When two arguments are provided, `.increase` asserts that the value of the given object `subject`'s `prop` property is greater after
* invoking the target function compared to beforehand.
*
* @example
@@ -3929,7 +3969,7 @@ declare namespace Cypress {
*/
(chainer: 'be.extensible'): Chainable<Subject>
/**
* Asserts that the target is sealed, which means that new properties cant be added to it, and its existing properties cant be reconfigured or deleted.
* Asserts that the target is sealed, which means that new properties can't be added to it, and its existing properties can't be reconfigured or deleted.
* @example
* let sealedObject = Object.seal({})
* let frozenObject = Object.freeze({})
@@ -3940,7 +3980,7 @@ declare namespace Cypress {
*/
(chainer: 'be.sealed'): Chainable<Subject>
/**
* Asserts that the target is frozen, which means that new properties cant be added to it, and its existing properties cant be reassigned to different values, reconfigured, or deleted.
* Asserts that the target is frozen, which means that new properties can't be added to it, and its existing properties can't be reassigned to different values, reconfigured, or deleted.
* @example
* let frozenObject = Object.freeze({})
* cy.wrap(frozenObject).should('be.frozen')
@@ -3949,7 +3989,7 @@ declare namespace Cypress {
*/
(chainer: 'be.frozen'): Chainable<Subject>
/**
* Asserts that the target is a number, and isnt `NaN` or positive/negative `Infinity`.
* Asserts that the target is a number, and isn't `NaN` or positive/negative `Infinity`.
* @example
* cy.wrap(1).should('be.finite')
* @see http://chaijs.com/api/bdd/#method_finite
@@ -3959,7 +3999,7 @@ declare namespace Cypress {
// chai.not
/**
* Asserts that the targets `type` is not equal to the given string type.
* Asserts that the target's `type` is not equal to the given string type.
* Types are case insensitive. See the `type-detect` project page for info on the type detection algorithm:
* https://github.com/chaijs/type-detect.
* @example
@@ -3978,7 +4018,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.above', value: number | Date): Chainable<Subject>
/**
* Asserts that the targets `type` is not equal to the given string type.
* Asserts that the target's `type` is not equal to the given string type.
* Types are case insensitive. See the `type-detect` project page for info on the type detection algorithm:
* https://github.com/chaijs/type-detect.
* @example
@@ -4015,7 +4055,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.arguments'): Chainable<Subject>
/**
* Asserts that the target is a not number thats within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* Asserts that the target is a not number that's within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* @example
* cy.wrap(5.1).should('not.be.approximately', 6, 0.5)
* @alias closeTo
@@ -4024,7 +4064,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.approximately', value: number, delta: number): Chainable<Subject>
/**
* Asserts that the target is not a number thats within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* Asserts that the target is not a number that's within a given +/- `delta` range of the given number `expected`. However, it's often best to assert that the target is equal to its expected value.
* @example
* cy.wrap(5.1).should('not.be.closeTo', 6, 0.5)
* @see http://chaijs.com/api/bdd/#method_closeto
@@ -4032,7 +4072,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.closeTo', value: number, delta: number): Chainable<Subject>
/**
* When the target is a not string or array, .empty asserts that the targets length property is strictly (===) equal to 0
* When the target is a not string or array, .empty asserts that the target's length property is strictly (===) equal to 0
* @example
* cy.wrap([1]).should('not.be.empty')
* cy.wrap('foo').should('not.be.empty')
@@ -4199,7 +4239,7 @@ declare namespace Cypress {
(chainer: 'not.contain', value: any): Chainable<Subject>
/**
* When one argument is provided, `.decrease` asserts that the given function `subject` does not returns a lesser number when it's invoked after invoking the target function compared to when it's invoked beforehand.
* `.decrease` also causes all `.by` assertions that follow in the chain to assert how much lesser of a number is returned. it's often best to assert that the return value decreased by the expected amount, rather than asserting it decreased by any amount.
* `.decrease` also causes all `.by` assertions that follow in the chain to assert how much lesser of a number is returned. It's often best to assert that the return value decreased by the expected amount, rather than asserting it decreased by any amount.
* @example
* let val = 1
* function subtractTwo() { val -= 2 }
@@ -4285,7 +4325,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.deep.property', value: string, obj: object): Chainable<Subject>
/**
* Asserts that the targets `length` property is not equal to the given number `n`.
* Asserts that the target's `length` property is not equal to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length', 2)
* cy.wrap('foo').should('not.have.length', 2)
@@ -4295,7 +4335,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length' | 'not.have.lengthOf', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is not greater than to the given number `n`.
* Asserts that the target's `length` property is not greater than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length.greaterThan', 4)
* cy.wrap('foo').should('not.have.length.greaterThan', 4)
@@ -4304,7 +4344,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length.greaterThan' | 'not.have.lengthOf.greaterThan', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is not greater than to the given number `n`.
* Asserts that the target's `length` property is not greater than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length.gt', 4)
* cy.wrap('foo').should('not.have.length.gt', 4)
@@ -4313,7 +4353,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length.gt' | 'not.have.lengthOf.gt' | 'not.have.length.above' | 'not.have.lengthOf.above', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is not greater than or equal to the given number `n`.
* Asserts that the target's `length` property is not greater than or equal to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length.gte', 4)
* cy.wrap('foo').should('not.have.length.gte', 4)
@@ -4322,7 +4362,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length.gte' | 'not.have.lengthOf.gte' | 'not.have.length.at.least' | 'not.have.lengthOf.at.least', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is less than to the given number `n`.
* Asserts that the target's `length` property is less than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('have.length.lessThan', 2)
* cy.wrap('foo').should('have.length.lessThan', 2)
@@ -4331,7 +4371,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length.lessThan' | 'not.have.lengthOf.lessThan', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is not less than to the given number `n`.
* Asserts that the target's `length` property is not less than to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length.lt', 2)
* cy.wrap('foo').should('not.have.length.lt', 2)
@@ -4340,7 +4380,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length.lt' | 'not.have.lengthOf.lt' | 'not.have.length.below' | 'not.have.lengthOf.below', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is not less than or equal to the given number `n`.
* Asserts that the target's `length` property is not less than or equal to the given number `n`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length.lte', 2)
* cy.wrap('foo').should('not.have.length.lte', 2)
@@ -4349,7 +4389,7 @@ declare namespace Cypress {
*/
(chainer: 'not.have.length.lte' | 'not.have.lengthOf.lte' | 'not.have.length.at.most' | 'not.have.lengthOf.at.most', value: number): Chainable<Subject>
/**
* Asserts that the targets `length` property is within `start` and `finish`.
* Asserts that the target's `length` property is within `start` and `finish`.
* @example
* cy.wrap([1, 2, 3]).should('not.have.length.within', 6, 12)
* @see http://chaijs.com/api/bdd/#method_lengthof
@@ -4426,9 +4466,9 @@ declare namespace Cypress {
* When one argument is provided, `.increase` asserts that the given function `subject` returns a greater number when it's
* invoked after invoking the target function compared to when it's invoked beforehand.
* `.increase` also causes all `.by` assertions that follow in the chain to assert how much greater of a number is returned.
* it's often best to assert that the return value increased by the expected amount, rather than asserting it increased by any amount.
* It's often best to assert that the return value increased by the expected amount, rather than asserting it increased by any amount.
*
* When two arguments are provided, `.increase` asserts that the value of the given object `subject`s `prop` property is greater after
* When two arguments are provided, `.increase` asserts that the value of the given object `subject`'s `prop` property is greater after
* invoking the target function compared to beforehand.
*
* @example
@@ -4514,7 +4554,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.extensible'): Chainable<Subject>
/**
* Asserts that the target is sealed, which means that new properties cant be added to it, and its existing properties cant be reconfigured or deleted.
* Asserts that the target is sealed, which means that new properties can't be added to it, and its existing properties can't be reconfigured or deleted.
* @example
* cy.wrap({a: 1}).should('be.sealed')
* cy.wrap({a: 1}).should('be.sealed')
@@ -4523,7 +4563,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.sealed'): Chainable<Subject>
/**
* Asserts that the target is frozen, which means that new properties cant be added to it, and its existing properties cant be reassigned to different values, reconfigured, or deleted.
* Asserts that the target is frozen, which means that new properties can't be added to it, and its existing properties can't be reassigned to different values, reconfigured, or deleted.
* @example
* cy.wrap({a: 1}).should('not.be.frozen')
* @see http://chaijs.com/api/bdd/#method_frozen
@@ -4531,7 +4571,7 @@ declare namespace Cypress {
*/
(chainer: 'not.be.frozen'): Chainable<Subject>
/**
* Asserts that the target is a number, and isnt `NaN` or positive/negative `Infinity`.
* Asserts that the target is a number, and isn't `NaN` or positive/negative `Infinity`.
* @example
* cy.wrap(NaN).should('not.be.finite')
* cy.wrap(Infinity).should('not.be.finite')
@@ -4543,7 +4583,7 @@ declare namespace Cypress {
// sinon-chai
/**
* Assert spy/stub was called the `new` operator.
* Beware that this is inferred based on the value of the this object and the spy functions prototype, so it may give false positives if you actively return the right kind of object.
* Beware that this is inferred based on the value of the this object and the spy function's prototype, so it may give false positives if you actively return the right kind of object.
* @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew
* @see https://on.cypress.io/assertions
*/
@@ -4616,7 +4656,7 @@ declare namespace Cypress {
(chainer: 'be.calledWithMatch' | 'have.been.calledWithMatch', ...args: any[]): Chainable<Subject>
/**
* Assert spy/stub was called the `new` operator.
* Beware that this is inferred based on the value of the this object and the spy functions prototype, so it may give false positives if you actively return the right kind of object.
* Beware that this is inferred based on the value of the this object and the spy function's prototype, so it may give false positives if you actively return the right kind of object.
* @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew
* @see https://on.cypress.io/assertions
*/
@@ -4703,7 +4743,7 @@ declare namespace Cypress {
// sinon-chai.not
/**
* Assert spy/stub was not called the `new` operator.
* Beware that this is inferred based on the value of the this object and the spy functions prototype, so it may give false positives if you actively return the right kind of object.
* Beware that this is inferred based on the value of the this object and the spy function's prototype, so it may give false positives if you actively return the right kind of object.
* @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew
* @see https://on.cypress.io/assertions
*/
@@ -4776,7 +4816,7 @@ declare namespace Cypress {
(chainer: 'not.be.calledWithMatch' | 'not.have.been.calledWithMatch', ...args: any[]): Chainable<Subject>
/**
* Assert spy/stub was not called the `new` operator.
* Beware that this is inferred based on the value of the this object and the spy functions prototype, so it may give false positives if you actively return the right kind of object.
* Beware that this is inferred based on the value of the this object and the spy function's prototype, so it may give false positives if you actively return the right kind of object.
* @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew
* @see https://on.cypress.io/assertions
*/
@@ -5327,7 +5367,7 @@ declare namespace Cypress {
/**
* Create an assertion. Assertions are automatically retried until they pass or time out.
* Passing a function to `.should()` enables you to make multiple assertions on the yielded subject. This also gives you the opportunity to massage what youd like to assert on.
* Passing a function to `.should()` enables you to make multiple assertions on the yielded subject. This also gives you the opportunity to massage what you'd like to assert on.
* Just be sure _not_ to include any code that has side effects in your callback function. The callback function will be retried over and over again until no assertions within it throw.
* @example
* cy
@@ -5432,7 +5472,7 @@ declare namespace Cypress {
interface ResolvedDevServerConfig {
port: number
close: (done?: () => any) => void
close: (done?: (err?: Error) => any) => void
}
interface PluginEvents {
@@ -5465,7 +5505,7 @@ declare namespace Cypress {
```
// likely want to do this in a support file
// so it's applied to all spec files
// cypress/support/index.js
// cypress/support/{e2e|component}.js
Cypress.on('uncaught:exception', (err, runnable) => {
// returning false here prevents Cypress from

View File

@@ -22,17 +22,15 @@ cypress.run().then(results => {
cypress.open() // $ExpectType Promise<void>
cypress.run() // $ExpectType Promise<CypressRunResult | CypressFailedRunResult>
cypress.open({
configFile: false
})
cypress.run({
configFile: "abc123"
})
// provide only some config options
const runConfig: Cypress.ConfigOptions = {
baseUrl: 'http://localhost:8080',
e2e: {
baseUrl: 'http://localhost:8080',
},
env: {
login: false
},

View File

@@ -33,9 +33,11 @@ namespace CypressConfigTests {
Cypress.config().baseUrl // $ExpectType string | null
// setters
Cypress.config('baseUrl', '.') // $ExpectType void
Cypress.config('baseUrl', null) // $ExpectType void
Cypress.config({ baseUrl: '.', }) // $ExpectType void
Cypress.config({ e2e: { baseUrl: '.' }}) // $ExpectType void
Cypress.config({ e2e: { baseUrl: null }}) // $ExpectType void
Cypress.config({ e2e: { baseUrl: '.', }}) // $ExpectType void
Cypress.config({ component: { baseUrl: '.', devServer: () => ({} as any) } }) // $ExpectError
Cypress.config({ e2e: { indexHtmlFile: 'index.html' } }) // $ExpectError
Cypress.config('taskTimeout') // $ExpectType number
Cypress.config('includeShadowDom') // $ExpectType boolean
@@ -798,7 +800,6 @@ namespace CypressTestConfigOverridesTests {
// set config on a per-test basis
it('test', {
animationDistanceThreshold: 10,
baseUrl: 'www.foobar.com',
defaultCommandTimeout: 6000,
env: {},
execTimeout: 6000,
@@ -859,7 +860,6 @@ namespace CypressTestConfigOverridesTests {
// set config on a per-suite basis
describe('suite', {
browser: {family: 'firefox'},
baseUrl: 'www.example.com',
keystrokeDelay: 0
}, () => {})
@@ -867,7 +867,6 @@ namespace CypressTestConfigOverridesTests {
describe('suite', {
browser: {family: 'firefox'},
baseUrl: 'www.example.com',
keystrokeDelay: false // $ExpectError
foo: 'foo' // $ExpectError
}, () => {})

View File

@@ -6,15 +6,14 @@ const pluginConfig: Cypress.PluginConfig = (on, config) => {}
// allows synchronous returns
const pluginConfig2: Cypress.PluginConfig = (on, config) => {
config // $ExpectType PluginConfigOptions
config.baseUrl // $ExpectType string | null
config.configFile // $ExpectType string | false
config.configFile // $ExpectType string
config.fixturesFolder // $ExpectType string | false
config.pluginsFile // $ExpectType string | false
config.screenshotsFolder // $ExpectType string | false
config.videoCompression // $ExpectType number | false
config.projectRoot // $ExpectType string
config.version // $ExpectType string
config.testingType // $ExpectType TestingType
config.browsers // $ExpectType Browser[]
on('before:browser:launch', (browser, options) => {
browser.displayName // $ExpectType string
@@ -66,7 +65,9 @@ const pluginConfig2: Cypress.PluginConfig = (on, config) => {
})
return {
baseUrl: 'http://localhost:3000'
e2e: {
baseUrl: 'http://localhost:3000'
}
}
}
@@ -105,7 +106,9 @@ const pluginConfig4: Cypress.PluginConfig = (on, config) => {
})
return Promise.resolve({
baseUrl: 'http://localhost:3000'
e2e: {
baseUrl: 'http://localhost:3000'
}
})
}

127
graphql-codegen.yml Normal file
View File

@@ -0,0 +1,127 @@
# https://www.graphql-code-generator.com/docs/getting-started/index
documentFilters: &documentFilters
immutableTypes: true
useTypeImports: true
preResolveTypes: true
onlyOperationTypes: true
avoidOptionals: true
vueOperations: &vueOperations
schema: './packages/graphql/schemas/schema.graphql'
config:
<<: *documentFilters
plugins:
- add:
content: '/* eslint-disable */'
- 'typescript'
- 'typescript-operations'
- 'typed-document-node':
# Intentionally specified under typed-document-node rather than top level config,
# becuase we don't want it flattening the types for the operations
flattenGeneratedTypes: true
vueTesting: &vueTesting
schema: './packages/graphql/schemas/schema.graphql'
config:
<<: *documentFilters
plugins:
- add:
content: '/* eslint-disable */'
- 'typescript'
- 'typescript-operations':
# For modifying in mountFragment
immutableTypes: false
- 'typed-document-node'
overwrite: true
config:
enumsAsTypes: true
declarationKind: 'interface'
strictScalars: true
scalars:
Date: string
DateTime: string
JSON: any
generates:
###
# Generates types for us to infer the correct "source types" when we mock out on the frontend
# This ensures we have proper type checking when we're using cy.mountFragment in component tests
###
'./packages/frontend-shared/cypress/support/generated/test-graphql-types.gen.ts':
schema: 'packages/graphql/schemas/schema.graphql'
plugins:
- add:
content: '/* eslint-disable */'
- 'typescript':
nonOptionalTypename: true
- 'packages/frontend-shared/script/codegen-type-map.js'
'./packages/frontend-shared/cypress/support/generated/test-cloud-graphql-types.gen.ts':
schema: 'packages/graphql/schemas/cloud.graphql'
plugins:
- add:
content: '/* eslint-disable */'
- 'typescript':
nonOptionalTypename: true
- 'packages/frontend-shared/script/codegen-type-map.js'
'./packages/graphql/src/gen/cloud-source-types.gen.ts':
schema: 'packages/graphql/schemas/cloud.graphql'
plugins:
- add:
content: '/* eslint-disable */'
- 'typescript'
###
# Generates types for us to infer the correct keys for graphcache
###
'./packages/data-context/src/gen/graphcache-config.gen.ts':
config:
useTypeImports: true
schema: 'packages/graphql/schemas/schema.graphql'
plugins:
- add:
content: '/* eslint-disable */'
- typescript
- typescript-urql-graphcache
###
# All of the GraphQL Query/Mutation documents we import for use in the .{vue,ts,tsx,js,jsx}
# files for useQuery / useMutation, as well as types associated with the fragments
###
'./packages/launchpad/src/generated/graphql.ts':
documents:
- './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}'
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
<<: *vueOperations
'./packages/app/src/generated/graphql.ts':
documents:
- './packages/app/src/**/*.{vue,ts,tsx,js,jsx}'
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
<<: *vueOperations
'./packages/frontend-shared/src/generated/graphql.ts':
documents: './packages/frontend-shared/src/gql-components/**/*.{vue,ts,tsx,js,jsx}'
<<: *vueOperations
###
# All GraphQL documents imported into the .spec.tsx files for component testing.
# Similar to generated/graphql.ts, except it doesn't include the flattening for the document nodes,
# so we can actually use the document in cy.mountFragment
###
'./packages/launchpad/src/generated/graphql-test.ts':
documents:
- './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}'
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
<<: *vueTesting
'./packages/app/src/generated/graphql-test.ts':
documents:
- './packages/app/src/**/*.{vue,ts,tsx,js,jsx}'
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
<<: *vueTesting
'./packages/frontend-shared/src/generated/graphql-test.ts':
documents: './packages/frontend-shared/src/gql-components/**/*.{vue,ts,tsx,js,jsx}'
<<: *vueTesting

80
guides/app-lifecycle.md Normal file
View File

@@ -0,0 +1,80 @@
## App Lifecycle
This documents the lifecycle of the application, specifically related to managing the current project,
and the various states & inputs that can feed into state changes, and how they are managed
1. Application starts via `cypress open | run --flags`
1. The input is run through `cli/lib/cli.js` for normalization
1. The normalized input is passed into the server, eventually getting to `server/lib/modes/index.ts`
1. The `DataContext` class receives the testing mode (`run` | `open`), and the `modeOptions` (CLI Flags)
1. We call `ctx.initialize`, which based on the `mode` returns a promise for series of steps needed
1. The `DataContext` should act as the global source of truth for all state in the application. It should be passed along where possible. In the `server` package, we can import/use `getCtx` so we don't need to pass it down the chain.
1. The CLI flags & environment variables are used set the initial state of the `coreData`
1. TODO: rename to `appState`?
1. In `open` mode, if the `--global` flag is passed, we start in "global" mode, which allows us to select multiple projects
1. Once a project is selected, either via the CLI being run within a project, or via the `--project` flag, we launch into project mode
## Project Lifecycle
1. Once a project is selected, we source the config from `cypress.config.js`, or wherever the config is specified via the `--configFile` CLI flag:
1. Read the `globalBrowsers`
1. Execute the `configFile` in a child process & reply back with the config, and the require.cache files in the child process
1. If there is an error sourcing the config file, we set an error on the `currentProject` in the root state
1. We source `cypress.env.json` and validate (if it exists)
## **Config Precedence:**
1. Runtime, inline: `it('should do the thing', { retries: { run: 3 } }`
2. `port` from spawned server
3. Returned from `setupNodeEvents` (as these get the options from the CLI)
4. Sourced from CLI
5. Sourced from `cypress.env.json`
6. Sourced from `cypress.config.{js|ts}`
7. Default config options
## **Merging**
Config options are deeply merged:
```bash
# CLI:
cypress run --env FOO=bar
# cypress.config.js
env: {
FOO: 'test'
},
e2e: {
setupNodeEvents (on, config) {
return require('@cypress/code-coverage')(on, config)
},
env: {
e2eRunner: true
}
}
# Would Result in
{
env: { FOO: 'bar', e2eRunner: true }
}
```
## Steps of Sourcing / Execution
1. **Application Start**
1. CLI args & environment are parsed into an "options" object, which is passed along to create the initial application config
2. Browsers are sourced from the machine at startup
3. CLI options `--config baseUrl=http://example.com`, `--env` are gathered for merging later
1. [https://gist.github.com/tgriesser/5111edc0e31b9db61755b0bddbf93e78](https://gist.github.com/tgriesser/5111edc0e31b9db61755b0bddbf93e78)
2. **Project Initialization**
1. When we have a "projectRoot", we execute the `cypress.config.{js|ts}`, and read the `cypress.env.json` - this will be persisted on the state object, so we can compare the diff as we detect/watch changes to these files
1. The child process will also send back a list of files that have been sourced so we can watch them for changes to re-execute the config. *We may want to warn against importing things top-level, so as to minimize the work done in child-process blocking the config*
2. We also pull the "saved state" for the user from the FS App data
1. We only do this in "open mode"
3. At this point, we do a first-pass at creating a known config shape, merging the info together into a single object, picking out the "allowed" list of properties to pass to the `setupNodeEvents`
3. **setupNodeEvents**
1. Once we have selected a `testingType`, we execute the `setupNodeEvents`, passing an "allowed" list of options as the second argument to the function. At this point, we have merged in any CLI options, env vars,
1. If they return a new options object, we merge it with the one we passed in
4. **config → FullConfig**
1. At this point we have the entire config, and we can set the `resolved` property which includes the origin of where the config property was resolved from

View File

@@ -0,0 +1,60 @@
# E2E Open Mode Testing
The "Open Mode" E2E tests in `packages/app` & `packages/launchpad` are the "system tests" for the open mode. This means that they hit the actual node server and act as a realistic "real world" scenario as much as possible. The main idea of these tests is to act as comprehensive "end-to-end" test for the actual server execution, and act as a "black box", testing for functional behavior as high-level as possible - only tapping into lower-level details when it's necessary.
### Example Test:
```ts
beforeEach(() => {
// Scaffold a project, from system-tests/fixtures
cy.scaffoldProject('todos')
// "Open" the project, as though you did cypress open --project 'path/to/todos'
cy.openProject('todos')
// Open the project, passing '--e2e' in the argv for cypress open
cy.openProject('todos', ['--e2e'])
// cypress open --global
cy.openGlobalMode()
})
it('should onboard a todos project', () => {
cy.visitLaunchpad()
cy.get('[e2e-project]').click()
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('cypress.config.ts', 'export default {}') // Adds a file
})
})
```
### Testing the App:
```ts
it('should open todos in the app', () => {
cy.startAppServer() // starts the express server used to run the "app"
cy.visitApp() // visits the app page, without launching the browser
cy.get('[href=#/runs]').click()
cy.get('[href=#/settings]').click()
})
```
### Remote GraphQL Schema Mocking
When we hit the remote GraphQL server, we mock against the same mocked schema we use client-side in the component tests. If we want to modify these responses we can use `cy.remoteGraphQLIntercept` to tap in and modify the mocked payloads to simulate different states:
```ts
cy.remoteGraphQLIntercept(async (obj) => {
// Currently, all remote requests go through here, we want to use this to modify the
// remote request before it's used and avoid touching the login query
if (obj.result.data?.cloudProjectsBySlugs) {
for (const proj of obj.result.data.cloudProjectsBySlugs) {
if (proj.runs?.nodes) {
proj.runs.nodes = []
}
}
}
return obj.result
})
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

View File

@@ -6,11 +6,38 @@ Clear, consistent, errors are one of the important parts of the Cypress experien
All error related logic for the server should be added to `@packages/errors`. This logic has been separated out from the `@packages/server` to enable strict type checking & use in other packages we have added in the `10.0-release` branch.
Summary of the Errors package:
### Errors Development Workflow
- `errors.ts`: A key/value mapping of known errors to functions returning "ErrorTemplates", described below, also includes/re-exports several helper utilities:
Adding and editing errors is best done with the help of the Error Comparison tool.
Start off by launching the Error Comparison tool from `packages/errors` via `yarn comparison`. This will launch a mini-webapp at http://localhost:5555.
The Error Comparison app has three pages: Ansi Compare, Ansi Base List, and Markdown.
1. Ansi Compare - Used to compare and accept changes made during development
2. Ansi Base List - Used to preview errors as they will be rendered to users in the Terminal.
3. Markdown - Used to preview errors as they will be rendered to users within the App, Launchpad, and Reporter.
#### Editing or Adding New Errors and Updating Snapshots
<img src="./error-handling-accept-snapshot.png" width="1200" />
1. Add (or update) errors in `packages/errors/src/errors.ts`
2. Add test cases to `visualSnapshotErrors_spec.ts`
3. Run `yarn test` in the `packages/errors` directory
4. Run `yarn comparison` in the `packages/errors` directory
5. Open http://localhost:5555/
6. Search for the error you're working on by the error key. (e.g. `AUTOMATION_SERVER_DISCONNECTED`)
7. Click "Looks Good" if it looks good.
8. To make edits, re-run the `yarn test` command and do a full refresh of the webapp.
9. Run `yarn test` after updating the snapshot to validate the changes were applied.
10. Commit the files changed in `__snapshot-html__`
### Technical Overview
- `errors.ts`: A key/value mapping of known errors to functions returning "ErrorTemplates", described below, also includes/re-exports several helper utilities:
- `get` / `getError`: builds & retrieves the error as a `CypressError`, should be the main way we retrieve errors throughout Cypress. Aliased as `errors.get` for existing use in the server package
- `throw` / `throwErr`: Get & throw the error, so we can spy/stub it in a test. Aliased as `errors.throw` for existing use in the server package
- `throw` / `throwErr`: Get & throw the error, so we can spy/stub it in a test. Aliased as `errors.throwErr` for existing use in the server package
- `logWarning`: Logs the error as a warning to the console, aliased as `errors.log` for existing use in the server package
- `errTemplate.ts`: Tagged template literal formatting the error as described below
- `stackUtils.ts`: Utilities for working with a stack trace, extended by the driver package
@@ -26,9 +53,9 @@ Return Value of `errTemplate` (`ErrTemplateResult`):
```ts
{
// Will always exist, this is the terminal-formatted error message
message: string,
message: string,
// Will always exist, this is the browser-formatted error message
messageMarkdown: string,
messageMarkdown: string,
details?: string, // Exists if there is `details()` call in the errTemplate
originalError?: ErrorLike // Exists if an error was passed into the `details()`
}
@@ -44,7 +71,7 @@ CANNOT_TRASH_ASSETS: (arg1: string) => {
This error will not alter the exit code.
${details(arg1)}`
},
},
```
In this case, `arg1` will be highlighted in yellow when printed to the terminal.
@@ -70,7 +97,7 @@ PLUGINS_FILE_ERROR: (arg1: string, arg2: Error) => {
### Error Wrapping
Any time we know about an edge case that is an error, we should define an error in `errors.ts`. This error should be retrieved by `getError`, which converts it to a `CypressError`.
Any time we know about an edge case that is an error, we should define an error in `errors.ts`. This error should be retrieved by `getError`, which converts it to a `CypressError`.
The `CypressError` is an `Error` containing the message returned from the `errTemplate`. The `stack` is set to that of the `originalError` if it exists (i.e. the error object passed into `details`), otherwise it's the `stack` from where the `getError` / `throwError` is called.
@@ -79,7 +106,6 @@ The `CypressError` has an `isCypressErr` prop which we use as a duck-type guard
### Child-Process Errors
All errors that occur in a child process spawned by Cypress should be sent over the `ipc` bridge using `util.serializeError`.
This ensures the `name`, `message`, `stack`, and any other relevant details are preserved and can be handled by the standard process of Cypress' error standardization / wrapping.
All errors that occur in a child process spawned by Cypress should be sent over the `ipc` bridge using `util.serializeError`.
This ensures the `name`, `message`, `stack`, and any other relevant details are preserved and can be handled by the standard process of Cypress' error standardization / wrapping.

View File

@@ -0,0 +1,119 @@
## GraphQL Subscriptions Overview & Test Guide:
In GraphQL there are currently 3 types of Operations: `query`, `mutation`, `subscription`
Query is similar to a rest-ful `GET` request:
```graphql
query MyAppData {
cloudUser { # CloudUser type
id
email
fullName
}
currentProject { # Project type
id
name
...CurrentProjectCard
}
app { # App type
localSettings {
...LocalSettingsView
}
}
}
```
Whereas a mutation is like a `POST` request, where you ask for what you want back (what changed b/c of the mutation):
```graphql
mutation MyAppMutation($testingType: TestingTypeEnum) {
chooseTestingType(testingType: $testingType) { # Project type
id
currentTestingType
}
}
```
A subscription is used when you want to receive information about data that has changed scoped to an individual event:
```graphql
subscription MyAppSubscription {
projectUpdated { # Project type
id
isLoadingConfig
isLoadingSetupNodeEvents
}
}
```
The distinction between subscriptions & queries is that subscriptions can only have a single top-level field. You can think of the subscription like an "event name" in socket.io, except that it's strongly typed & part of the overall GraphQL schema, meaning that you don't need to do any extra work for it to merge into the normalized cache & refresh affected parts of the view.
```ts
// will merge & update any views that depend on `isLoadingConfig` / `isLoadingSetupNodeEvents`
useSubscription({ query: MyAppSubscriptionDocument })
```
We can also use subscriptions more granularly as an event emitter, basically as a strongly typed socket.io emitter:
```graphql
subscription OnSpecChange {
onSpecUpdate {
specPath
reason
}
}
```
```ts
useSubscription({ query: OnSpecChangeDocument }, (prev, next) => {
if (data.specPath === currentSpecPath && reason === 'DELETED') {
// navigate to another page
} else {
// Rerun spec
}
return next
})
```
### Client Details:
- [API Docs for useSubscription](https://formidable.com/open-source/urql/docs/api/vue/#usesubscription)
- [Subscriptions overview](https://formidable.com/open-source/urql/docs/advanced/subscriptions/)
One thing to be aware of, is the subscription is only mounted/responded to when the containing component is mounted on the page. Therefore, one rule of thumb when using subscriptions is to ensure that they are declared in the subscription dependent view that is highest in the hierarchy so that the subscription can be active when it needs to be. This location is often alongside a query rather than a fragment.
### Server Docs
Subscriptions are implemented on the server as an `AsyncIterator`. This is handled for us by the [graphql-ws](https://github.com/enisdenjo/graphql-ws) package.
To add a new Subscription field, add a new entry in the [`gql-Subscriptions`](../packages/graphql/src/schemaTypes/objectTypes/gql-Subscription.ts):
```ts
t.field('browserStatusChange', {
type: CurrentProject,
description: 'Status of the currently opened browser',
subscribe: (source, args, ctx) => ctx.emitter.subscribeTo('browserStatusChange'),
resolve: (source, args, ctx) => ({}),
})
```
And then add the corresponding method in [DataEmitterActions](../packages/data-context/src/actions/DataEmitterActions.ts)
```ts
browserStatusChange () {
this._emit('browserStatusChange')
}
```
- [API Docs](https://github.com/enisdenjo/graphql-ws/tree/master/docs)
- [Transport layer protcol specification](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)
### Testing
If you want to TDD the subscription being added & working directly in isolation, one recommended approach is to add one spec file per-subscription in the `/app/cypress/e2e/subscription` directory. This file can cover both the app & launchpad handling of a given subscription.
Example: [authChange-subscription.cy.ts](../packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts)

View File

@@ -0,0 +1,154 @@
# DRAFT - CYPRESS TESTING STRATEGY + STYLE GUIDE
## Status of this document
This draft is a starting point to help us converge on some common practices in terms of how we write tests, and a reference point for how we can review test PRs with each other. There are many valid style choices to make in writing tests, and some of them are contradictory, so a consistent approach is useful.
I'm starting by documenting the practices that I see us currently doing in the new parts of the codebase, or things we seem to have agreed we'd like to do. Not exhaustive, and not prescriptive. The Testing Strategy section existed already in Google Docs.
## Testing Strategy
The purpose of this document is to detail the various ways to test the cypress repo including the Cypress App.
### Testing Goals
The goal of testing is to instill confidence in the features being built and the bugs being fixed as well as save development hours.
Test driven development (TDD) is a core tenet to how we write tests at Cypress. Our approach is to Shift-left testing whenever possible.
### Testing Types
- Unit - Tests the functionality of a single section of code, like a single function.
- Integration - Tests the functionality of a feature, with some portions (like a backend) mocked.
- End-to-end (E2E) - Tests the functionality of a feature with no mocking. It tests all pieces of the feature working together.
- Component - Tests a single component or group of components.
- System - A type of E2E test that tests an entire project in the context of Cypress.
- Snapshot - Captures text output and compares them to previously captured text output (like testing output for a CLI).
- Screenshot - Captures screenshots and compares them to previously captured screenshots.
- Performance - Tests the speed, response time, stability, reliability, scalability and resource usage of the application.
### Testing Environments
Tests can run in different environments:
- Different browsers
- Different OSs
- Node.js
### Testing Tools
- Mocha
- Cypress
- Snapshot tests using [snap-shot-it](https://github.com/bahmutov/snap-shot-it) - Use `SNAPSHOT_UPDATE=1 <test command>` to update them, but make sure to check the diff to ensure it's the right update.
- Percy snapshot tests - These only run in CI and can be managed from the GitHub status check on a PR
### Testing Workflow
Developing features and fixing bugs should be test driven.
An example of building out a front-end facing feature may go like this:
1. Check user requirements in the ticket/issue youre assigned to.
1. Write a failing component test for a features required in the user story.
1. Build the piece of the component needed so that the failing test passes.
1. Repeat steps 2-3 until the component satisfies the user requirements.
1. Write out all CSS required for the feature to be design complete.
1. Add a screenshot test.
1. Write an E2E test to ensure all pieces and integrations work together.
1. Add performance tests if necessary to ensure the feature doesnt introduce regressions on performance.
An example of building out a feature/change to the CLI may go like this:
1. Check user requirements in the ticket/issue youre assigned to.
1. Write a failing unit/integration test for the feature required in the user story.1. Write the logic for the new feature/change.
1. Capture/update a snapshot test for any changes to the printed console output.
## Component and E2E Tests in App, Launchpad, and Frontend-Shared Packages
### Component Tests
Component tests should assert all expected behaviors of a component from a user perspective, as well as the "contract" of the component from a developer perspective. It is useful to write component tests as the primary driver of component development. This helps with development speed (no need to keep getting the whole app into the state you want), and test completeness.
For user-facing behavior, we interact with (or remount) the component to get it into all desired states and confirm they are valid. Each state should have a Percy snapshot in addition to regular Cypress assertions.
For developer-facing behavior - props received, events emitted, any other side effects, anything not covered in the UI tests can be explicitly asserted on its own. Props are often fully covered by UI assertions, but events emitted by the component to be used in parent components are not. So we can stub event handlers and verify that they are called in response to particular interactions.
**Many components** in the unified App and Launchpad define the data they require from GraphQL with a Fragment inside the component's `script` section, and receive the data from that Fragment in a prop called `gql`, so mocking out scenarios usually involves tweaking the mocked GraphQL data before the component mounts using `mountFragment()`.
### E2E Tests
Certain side effects, like GraphQL mutations, do not fire from component tests, but can be monitored from an E2E test with `cy.intercept`. And some entire packages, like `reporter` are independent apps that can be mounted in an E2E test and tested for interactions with other parts of the system.
## Testing Style Guide
### String constants
Strings used in tests for locating elements or asserting content in other ways are imported from the i18n constants. All strings present in the App or Launchpad UI should be found in some form in `en-US.json`. If a plain string is found in the UI code (we have a few of them), it should be moved into the constants file and then imported to both the component and the test code.
### Element Locators
When relevant, element locators can assert something useful about the nature or context of the element in the DOM. Often for accessibility reasons it is important what elements are rendered. If there is nothing in particular that matters about the DOM, or if needed to disambiguate, then `data-cy` attributes can be used to locate elements.
### Testing Interactive Elements
If the test will interact with an element, or assert the state of an interactive element, **always prefer to locate that element with its accessible name** (and, usually, an assertion about the element itself). Without an accessible name, the element will not be described correctly by a screen reader, which means certain disabled users would not know what the control or form field is for. Since we want every user to be able to interact with our app, a test that interacts with something should fail if there is no label for it.
Examples:
```js
cy.contains('button', 'Log In').click()
```
```js
cy.findByLabelText('open navigation', {
selector: 'button',
}).click()
```
#### Label-like locators that are not labels
We should be cautious with a locator like `cy.findByPlaceholderText('My placeholder')` to target a form input, as an input that only has a `placeholder`, but no `label`, is not fully accessible. Even though it may be useful to assert the placeholder contents for its own sake, we should prefer to locate an input by `label` when interacting.
### Non-interactive elements
When assertions are made against non-interactive elements, if the surrounding HTML is relevant from an accessibility perspective, assert the relevant parts of the HTML:
```js
cy.contains('h1', 'Page Title!').should('be.visible')
```
```js
cy.contains('h2', 'Modal Title').should('be.visible')
```
This is a judgement call, there is a spectrum between asserting useful things about the DOM and writing a brittle assertion like this that adds no value to the test:
```js
cy.contains('div > div > .special-list > li > span', 'Home Link')`
```
In general, prefer to limit assertions about the nature of the DOM to a small surface area near the target element. Even then, only assert the DOM if there is a reason that changing the DOM in that area would harm the use experience - for example by breaking accessibility.
### Data-cy attributes
Sometimes the specific details of an element don't matter so we don't assert what kind of element it is. Or the contents are dynamic and can't be controlled in the test, so we can't use the content to locate the element. In these cases `data-cy` attributes can be used:
```
cy.get('[data-cy="success-toast"]').should('be.visible') // just make sure it exists
```
`data-cy` can also be combined with other selectors to help target a specific element we've identified in our code, while continuing to assert the appropriate DOM and content:
```
cy.contains('[data-cy="success-toast"] h2', 'Success!').should('be.visible') // toast with correct heading element and test exists
```
Be cautious when using _only_ `data-cy` to locate an element, because doing so specifically tells the test not to care about what kind of element it is, or what it contains. So we should be sure that's what is intended. This means we should rarely, if ever, rely on `data-cy` alone to locate an element we will interact with. It should always be combined with an assertion about the label/name of that element.
An exception would be when testing a something like card in the UI, if the whole card is supposed to be clickable. While there should be some focusable element inside the card for keyboard and screenreader users, the card itself does not need a label but should still be tested, alongside the properly labelled accessible control for the same function:
```
cy.get('[data-cy="ui-card"]').click() // test clicking at the level of the card itself
// ... assert expected state ...
cy.contains('[data-cy="ui-card"] button', 'Activate').click() // test the accessible trigger for that card's functionality
// ... assert expected state ...
```
### Visibility Checks
Asserting `should('be.visible')` is useful when testing elements that the test won't interact with, as in certain situations it is possible for elements to be found in the DOM with the content we expect, but to still be unexpectedly hidden from the user with CSS or covered by another element. When interacting, that visibility check is already built in before `cy.click` or `cy.type` for example.
### Cypress-Testing-Library
Feel free to use this often in tests if it makes the test easier to write or understand, except where using it provides less confidence than a plain Cypress selector. For example `cy.contains('button', 'Log In')` is slightly preferred to `cy.findByRole('button', {name: 'Log In' })`, because the ARIA role of `button` could be added to an element that does not have the expected keyboard behaviors implemented, and the test might still pass. It is also a good accessibility practice to not use ARIA to recreate the existing functionality of HTML elements, but instead use the elements directly. So using `findByRole` should not be necessary except for certain UI interactions like tabs or carousels, if we have those.
### Visual Appearance
Avoid specifying specific CSS color values in tests. Prefer Percy snapshots to validate that the approved appearance isn't changing unexpectedly.

2
gulpfile.js Normal file
View File

@@ -0,0 +1,2 @@
require('@packages/ts/register')
require('./scripts/gulp/gulpfile')

View File

@@ -14,7 +14,7 @@ Ensure you have a version of Cypress > 7.
Add the following to your support file:
```js
// cypress/support/index.js
// cypress/support/component.js
// core-js 3.*
require('core-js/es/reflect');
// core-js 2.*
@@ -22,13 +22,12 @@ require('core-js/es7/reflect');
require('@cypress/angular/support');
```
Enable component testing in `cypress.json`.
Enable component testing in `cypress.config.js`.
```json
{
```js
module.exports = {
"component": {
"componentFolder": "src/app",
"testFiles": "**/*cy-spec.ts"
"specPattern": "src/**/*.cy.ts"
}
}
```
@@ -156,15 +155,21 @@ module.exports = {
`npm install -D @cypress/code-coverage`
- Then add the code below to your supportFile and pluginsFile
- Then add the code below to your component support file
```javascript
// cypress/support/index.js
import '@cypress/code-coverage/support';
// cypress/plugins/index.js
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config);
return config;
```
- Then add the code below to your cypress configuration
```js
{
...
component: {
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config);
return config;
}
}
};
```

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import * as webpackConfig from './cypress/plugins/webpack.config'
export default defineConfig({
'experimentalFetchPolyfill': true,
'fixturesFolder': false,
'includeShadowDom': true,
'fileServerFolder': 'src',
'projectId': 'nf7zag',
'component': {
setupNodeEvents (on, config) {
return require('./cypress/plugins')(on, config)
},
devServer,
devServerConfig: { webpackConfig },
},
})

View File

@@ -1,11 +0,0 @@
{
"experimentalFetchPolyfill": true,
"fixturesFolder": false,
"includeShadowDom": true,
"fileServerFolder": "src",
"projectId": "nf7zag",
"component": {
"componentFolder": "src/app",
"testFiles": "**/*cy-spec.ts"
}
}

View File

@@ -1,16 +1,7 @@
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin'
import * as webpackConfig from './webpack.config'
module.exports = (on, config) => {
addMatchImageSnapshotPlugin(on, config)
const { startDevServer } = require('@cypress/webpack-dev-server')
on('dev-server:start', (options) => {
return startDevServer({
options,
webpackConfig,
})
})
require('@cypress/code-coverage/task')(on, config)

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<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>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{
"include": ["integration/*.ts"],
"include": ["e2e/*.ts"],
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

View File

@@ -7,13 +7,14 @@
"scripts": {
"build": "tsc --project tsconfig.lib.json",
"watch": "tsc --project tsconfig.lib.json -w",
"cy:open": "node ../../scripts/cypress.js open-ct",
"cy:run": "node ../../scripts/cypress.js run-ct",
"cy:open": "node ../../scripts/cypress.js open --component",
"cy:run": "node ../../scripts/cypress.js run --component",
"ng": "ng",
"app-start": "ng serve",
"app-build": "ng build",
"test": "yarn cy:run",
"test-ci": "yarn cy:run"
"test-ci": "yarn cy:run",
"postinstall": "patch-package"
},
"dependencies": {
"@cypress/mount-utils": "0.0.0-development",
@@ -59,7 +60,7 @@
"semantic-release": "17.4.2",
"to-string-loader": "1.1.6",
"ts-loader": "8.1.0",
"ts-node": "9.1.1",
"ts-node": "^10.2.1",
"tslib": "^2.2.0",
"tslint": "5.20.1",
"typescript": "4.2.4",

View File

@@ -0,0 +1,45 @@
diff --git a/node_modules/@cypress/code-coverage/support-utils.js b/node_modules/@cypress/code-coverage/support-utils.js
index 31e00ee..0c56908 100644
--- a/node_modules/@cypress/code-coverage/support-utils.js
+++ b/node_modules/@cypress/code-coverage/support-utils.js
@@ -10,7 +10,7 @@ const filterSpecsFromCoverage = (totalCoverage, config = Cypress.config) => {
const integrationFolder = config('integrationFolder')
/** @type {string} Cypress run-time config has test files string pattern */
// @ts-ignore
- const testFilePattern = config('testFiles')
+ const testFilePattern = config('specPattern')
// test files chould be:
// wild card string "**/*.*" (default)
diff --git a/node_modules/@cypress/code-coverage/support.js b/node_modules/@cypress/code-coverage/support.js
index c99ceb2..f51ce4e 100644
--- a/node_modules/@cypress/code-coverage/support.js
+++ b/node_modules/@cypress/code-coverage/support.js
@@ -37,7 +37,6 @@ const logMessage = (s) => {
* If there are more files loaded from support folder, also removes them
*/
const filterSupportFilesFromCoverage = (totalCoverage) => {
- const integrationFolder = Cypress.config('integrationFolder')
const supportFile = Cypress.config('supportFile')
/** @type {string} Cypress run-time config has the support folder string */
@@ -50,16 +49,9 @@ const filterSupportFilesFromCoverage = (totalCoverage) => {
isSupportFile(filename)
)
- // check the edge case
- // if we have files from support folder AND the support folder is not same
- // as the integration, or its prefix (this might remove all app source files)
- // then remove all files from the support folder
- if (!integrationFolder.startsWith(supportFolder)) {
- // remove all covered files from support folder
- coverage = Cypress._.omitBy(totalCoverage, (fileCoverage, filename) =>
- filename.startsWith(supportFolder)
- )
- }
+ coverage = Cypress._.omitBy(totalCoverage, (fileCoverage, filename) =>
+ filename.startsWith(supportFolder)
+ )
return coverage
}

View File

@@ -1,49 +0,0 @@
import { initEnv, mount } from '@cypress/angular'
import { AppModule } from '../app.module'
import { AssetsImageComponent } from './assets-image.component'
describe('AssetsImageComponent', () => {
// FIXME: Find out why this fails and fix it.
it.skip('should create', () => {
initEnv(AssetsImageComponent)
mount(AssetsImageComponent)
// add "fileServerFolder": "src" in cypress.json
cy.get('img#noSlash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
cy.get('img#slash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
})
// FIXME: Find out why this fails and fix it.
it.skip('should create with AppModule', () => {
initEnv({ imports: [AppModule] })
mount(AssetsImageComponent)
// add "fileServerFolder": "src" in cypress.json
cy.get('img#noSlash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
cy.get('img#slash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
})
})

View File

@@ -0,0 +1,49 @@
import { initEnv, mount } from '@cypress/angular'
import { AppModule } from '../app.module'
import { AssetsImageComponent } from './assets-image.component'
describe('AssetsImageComponent', () => {
// FIXME: Find out why this fails and fix it.
it.skip('should create', () => {
initEnv(AssetsImageComponent)
mount(AssetsImageComponent)
// add "fileServerFolder": "src" in cypress.config.{ts|js}
cy.get('img#noSlash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
cy.get('img#slash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
})
// FIXME: Find out why this fails and fix it.
it.skip('should create with AppModule', () => {
initEnv({ imports: [AppModule] })
mount(AssetsImageComponent)
// add "fileServerFolder": "src" in cypress.config.{ts|js}
cy.get('img#noSlash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
cy.get('img#slash')
.should('be.visible')
.and(($img) => {
const img = $img[0] as HTMLImageElement
expect(img.naturalWidth).to.be.greaterThan(0)
})
})
})

View File

@@ -1,19 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { initEnv, mount } from '@cypress/angular'
// You have to import your custom element
// And in cypress.json activate "includeShadowDom" configuration
import '../my-custom-element'
import { UseCustomElementComponent } from './use-custom-element.component'
describe('AddStyleComponent', () => {
it('component with custom element shadow dom', () => {
initEnv(UseCustomElementComponent, { schemas: [CUSTOM_ELEMENTS_SCHEMA] })
mount(UseCustomElementComponent)
cy.contains('use-custom-element works!')
cy.get('my-custom-element')
.shadow()
.get('button')
.should('have.text', 'Custom element button')
.should('have.css', 'color', 'rgb(255, 0, 0)')
})
})

View File

@@ -0,0 +1,19 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { initEnv, mount } from '@cypress/angular'
// You have to import your custom element
// And in cypress.config.{ts|js} activate "includeShadowDom" configuration
import '../my-custom-element'
import { UseCustomElementComponent } from './use-custom-element.component'
describe('AddStyleComponent', () => {
it('component with custom element shadow dom', () => {
initEnv(UseCustomElementComponent, { schemas: [CUSTOM_ELEMENTS_SCHEMA] })
mount(UseCustomElementComponent)
cy.contains('use-custom-element works!')
cy.get('my-custom-element')
.shadow()
.get('button')
.should('have.text', 'Custom element button')
.should('have.css', 'color', 'rgb(255, 0, 0)')
})
})

View File

@@ -1,8 +1,8 @@
exports['injects guessed next.js template cypress.json'] = `
{
"componentFolder": "src",
"testFiles": "**/*.spec.{js,ts,jsx,tsx}"
}
exports['injects guessed next.js template cypress.config.ts'] = `
export default {
specPattern: "src/**/*.spec.{js,ts,jsx,tsx}"
};
`
exports['injects guessed next.js template plugins/index.js'] = `
@@ -18,11 +18,11 @@ module.exports = (on, config) => {
`
exports['Injected overridden webpack template cypress.json'] = `
{
"componentFolder": "cypress/component",
"testFiles": "**/*.spec.{js,ts,jsx,tsx}"
}
exports['Injected overridden webpack template cypress.config.ts'] = `
export default {
specPattern: "cypress/component/**/*.spec.{js,ts,jsx,tsx}"
};
`
exports['Injected overridden webpack template plugins/index.js'] = `
@@ -38,6 +38,6 @@ module.exports = (on, config) => {
`
exports['Injected overridden webpack template support/index.js'] = `
exports['Injected overridden webpack template support/component.js'] = `
import "./commands.js";
`

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -3,7 +3,7 @@
"version": "0.0.0-development",
"description": "Cypress smart installation wizard",
"private": false,
"main": "index.js",
"main": "dist/src/main.js",
"scripts": {
"build": "yarn prepare-example && tsc -p ./tsconfig.json && node scripts/example copy-to ./dist/initial-template && yarn prepare-copy-templates",
"build-prod": "yarn build",
@@ -26,7 +26,8 @@
"fs-extra": "^9.1.0",
"glob": "^7.1.6",
"inquirer": "7.3.3",
"ora": "^5.1.0"
"ora": "^5.1.0",
"recast": "0.20.4"
},
"devDependencies": {
"@types/babel__core": "^7.1.2",
@@ -41,6 +42,10 @@
"snap-shot-it": "7.9.3",
"typescript": "^4.2.3"
},
"files": [
"dist",
"bin"
],
"bin": {
"create-cypress-tests": "dist/src/index.js"
},

View File

@@ -2,6 +2,7 @@ import path from 'path'
import * as fs from 'fs-extra'
import * as babel from '@babel/core'
import * as babelTypes from '@babel/types'
import { prettifyCode } from '../../utils'
type AST = ReturnType<typeof babel.template.ast>
@@ -11,13 +12,6 @@ export type PluginsConfigAst = {
requiresReturnConfig?: true
}
function tryRequirePrettier () {
try {
return require('prettier')
} catch (e) {
return null
}
}
const sharedBabelOptions = {
// disable user config
configFile: false,
@@ -47,11 +41,7 @@ async function transformFileViaPlugin (filePath: string, babelPlugin: babel.Plug
return false
}
const maybePrettier = tryRequirePrettier()
if (maybePrettier && maybePrettier.format) {
finalCode = maybePrettier.format(finalCode, { parser: 'babel' })
}
finalCode = await prettifyCode(finalCode)
await fs.writeFile(filePath, finalCode)

View File

@@ -0,0 +1,409 @@
/// <reference path="../../../../../cli/types/mocha/index.d.ts" />
import * as path from 'path'
import { expect } from 'chai'
import * as fs from 'fs-extra'
import { insertValueInJSString, insertValuesInConfigFile } from './configFileUpdater'
const projectRoot = process.cwd()
// Test util - if needed outside the tests we can move it to utils
const stripIndent = (strings: any, ...args: any) => {
const parts = []
for (let i = 0; i < strings.length; i++) {
parts.push(strings[i])
if (i < strings.length - 1) {
parts.push(`<<${i}>>`)
}
}
const lines = parts.join('').split('\n')
const firstLine = lines[0].length === 0 ? lines[1] : lines[0]
let indentSize = 0
for (let i = 0; i < firstLine.length; i++) {
if (firstLine[i] === ' ') {
indentSize++
continue
}
break
}
const strippedLines = lines.map((line) => line.substring(indentSize))
let result = strippedLines.join('\n').trimLeft()
args.forEach((arg: any, i: any) => {
result = result.replace(`<<${i}>>`, `${arg}`)
})
return result
}
describe('lib/util/config-file-updater', () => {
context('with js files', () => {
describe('#insertValueInJSString', () => {
describe('es6 vs es5', () => {
it('finds the object litteral and adds the values to it es6', async () => {
const src = stripIndent`\
export default {
foo: 42,
}
`
const expectedOutput = stripIndent`\
export default {
projectId: "id1234",
viewportWidth: 400,
foo: 42,
}
`
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 })
expect(output).to.equal(expectedOutput)
})
it('finds the object litteral and adds the values to it es5', async () => {
const src = stripIndent`\
module.exports = {
foo: 42,
}
`
const expectedOutput = stripIndent`\
module.exports = {
projectId: "id1234",
viewportWidth: 400,
foo: 42,
}
`
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 })
expect(output).to.equal(expectedOutput)
})
it('works with and without the quotes around keys', async () => {
const src = stripIndent`\
export default {
"foo": 42,
}
`
const expectedOutput = stripIndent`\
export default {
projectId: "id1234",
viewportWidth: 400,
"foo": 42,
}
`
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 })
expect(output).to.equal(expectedOutput)
})
})
describe('defineConfig', () => {
it('skips defineConfig and add to the object inside', async () => {
const src = stripIndent`\
import { defineConfig } from "cypress"
export default defineConfig({
foo: 42,
})
`
const expectedOutput = stripIndent`\
import { defineConfig } from "cypress"
export default defineConfig({
projectId: "id1234",
viewportWidth: 400,
foo: 42,
})
`
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 })
expect(output).to.equal(expectedOutput)
})
it('skips defineConfig even if it renamed in an import (es6)', async () => {
const src = stripIndent`\
import { defineConfig as cy_defineConfig } from "cypress"
export default cy_defineConfig({
foo: 42,
})
`
const expectedOutput = stripIndent`\
import { defineConfig as cy_defineConfig } from "cypress"
export default cy_defineConfig({
projectId: "id1234",
viewportWidth: 400,
foo: 42,
})
`
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 })
expect(output).to.equal(expectedOutput)
})
it('skips defineConfig even if it renamed in a require (es5)', async () => {
const src = stripIndent`\
const { defineConfig: cy_defineConfig } = require("cypress")
module.exports = cy_defineConfig({
foo: 42,
})
`
const expectedOutput = stripIndent`\
const { defineConfig: cy_defineConfig } = require("cypress")
module.exports = cy_defineConfig({
projectId: "id1234",
viewportWidth: 400,
foo: 42,
})
`
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 })
expect(output).to.equal(expectedOutput)
})
})
describe('updates', () => {
it('updates a value if the same value is found in resolved config', async () => {
const src = stripIndent`\
export default {
foo: 42,
}
`
const expectedOutput = stripIndent`\
export default {
foo: 1000,
}
`
const output = await insertValueInJSString(src, { foo: 1000 })
expect(output).to.equal(expectedOutput)
})
it('accepts inline comments', async () => {
const src = stripIndent`\
export default {
foo: 12, // will do this later
viewportWidth: 800,
}
`
const expectedOutput = stripIndent`\
export default {
foo: 1000, // will do this later
viewportWidth: 800,
}
`
const output = await insertValueInJSString(src, { foo: 1000 })
expect(output).to.equal(expectedOutput)
})
it('updates a value even when this value is explicitely undefined', async () => {
const src = stripIndent`\
export default {
foo: undefined, // will do this later
viewportWidth: 800,
}
`
const expectedOutput = stripIndent`\
export default {
foo: 1000, // will do this later
viewportWidth: 800,
}
`
const output = await insertValueInJSString(src, { foo: 1000 })
expect(output).to.equal(expectedOutput)
})
it('updates values and inserts config', async () => {
const src = stripIndent`\
export default {
foo: 42,
bar: 84,
component: {
devServer() {
return null
}
}
}
`
const expectedOutput = stripIndent`\
export default {
projectId: "id1234",
foo: 1000,
bar: 3000,
component: {
devServer() {
return null
}
}
}
`
const output = await insertValueInJSString(src, { foo: 1000, bar: 3000, projectId: 'id1234' })
expect(output).to.equal(expectedOutput)
})
})
describe('subkeys', () => {
it('inserts nested values', async () => {
const src = stripIndent`\
module.exports = {
foo: 42
}
`
const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } })
const expectedOutput = stripIndent`\
module.exports = {
component: {
specPattern: "src/**/*.spec.cy.js",
},
foo: 42
}
`
expect(output).to.equal(expectedOutput)
})
it('inserts nested values into existing keys', async () => {
const src = stripIndent`\
module.exports = {
component: {
viewportWidth: 800
},
foo: 42
}
`
const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } })
const expectedOutput = stripIndent`\
module.exports = {
component: {
specPattern: "src/**/*.spec.cy.js",
viewportWidth: 800
},
foo: 42
}
`
expect(output).to.equal(expectedOutput)
})
it('updates nested values', async () => {
const src = stripIndent`\
module.exports = {
foo: 42,
component: {
specPattern: 'components/**/*.spec.cy.js',
foo: 82
}
}`
const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } })
const expectedOutput = stripIndent`\
module.exports = {
foo: 42,
component: {
specPattern: "src/**/*.spec.cy.js",
foo: 82
}
}`
expect(output).to.equal(expectedOutput)
})
})
describe('failures', () => {
it('fails if not an object litteral', () => {
const src = [
'const foo = {}',
'export default foo',
].join('\n')
return insertValueInJSString(src, { bar: 10 })
.then(() => {
throw Error('this should not succeed')
})
.catch((err) => {
expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.')
})
})
it('fails if one of the values to update is not a literal', () => {
const src = [
'const bar = 12',
'export default {',
' foo: bar',
'}',
].join('\n')
return insertValueInJSString(src, { foo: 10 })
.then(() => {
throw Error('this should not succeed')
})
.catch((err) => {
expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.')
})
})
it('fails with inlined values', () => {
const src = stripIndent`\
const foo = 12
export default {
foo
}
`
return insertValueInJSString(src, { foo: 10 })
.then(() => {
throw Error('this should not succeed')
})
.catch((err) => {
expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.')
})
})
it('fails if there is a spread', () => {
const src = stripIndent`\
const foo = { bar: 12 }
export default {
bar: 8,
...foo
}
`
return insertValueInJSString(src, { bar: 10 })
.then(() => {
throw Error('this should not succeed')
})
.catch((err) => {
expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.')
})
})
})
})
})
})

View File

@@ -0,0 +1,295 @@
import _ from 'lodash'
import { parse } from '@babel/parser'
import type { File } from '@babel/types'
import type { NodePath } from 'ast-types/lib/node-path'
import { visit } from 'recast'
import type { namedTypes } from 'ast-types'
import * as fs from 'fs-extra'
import { prettifyCode } from '../../utils'
export async function insertValuesInConfigFile (filePath: string, obj: Record<string, any> = {}) {
await insertValuesInJavaScript(filePath, obj)
return true
}
export async function insertValuesInJavaScript (filePath: string, obj: Record<string, any>) {
const fileContents = await fs.readFile(filePath, { encoding: 'utf8' })
let finalCode = await insertValueInJSString(fileContents, obj)
const prettifiedCode = await prettifyCode(finalCode)
if (prettifiedCode) {
finalCode = prettifiedCode
}
await fs.writeFile(filePath, finalCode)
}
export async function insertValueInJSString (fileContents: string, obj: Record<string, any>): Promise<string> {
const ast = parse(fileContents, { plugins: ['typescript'], sourceType: 'module' })
let objectLiteralNode: namedTypes.ObjectExpression | undefined
function handleExport (nodePath: NodePath<namedTypes.CallExpression, any> | NodePath<namedTypes.ObjectExpression, any>): void {
if (nodePath.node.type === 'CallExpression'
&& nodePath.node.callee.type === 'Identifier') {
const functionName = nodePath.node.callee.name
if (isDefineConfigFunction(ast, functionName)) {
return handleExport(nodePath.get('arguments', 0))
}
}
if (nodePath.node.type === 'ObjectExpression' && !nodePath.node.properties.find((prop) => prop.type !== 'ObjectProperty')) {
objectLiteralNode = nodePath.node
return
}
throw new Error('Cypress was unable to add/update values in your configuration file.')
}
visit(ast, {
visitAssignmentExpression (nodePath) {
if (nodePath.node.left.type === 'MemberExpression') {
if (nodePath.node.left.object.type === 'Identifier' && nodePath.node.left.object.name === 'module'
&& nodePath.node.left.property.type === 'Identifier' && nodePath.node.left.property.name === 'exports') {
handleExport(nodePath.get('right'))
}
}
return false
},
visitExportDefaultDeclaration (nodePath) {
handleExport(nodePath.get('declaration'))
return false
},
})
const splicers: Splicer[] = []
if (!objectLiteralNode) {
// if the export is no object litteral
throw new Error('Cypress was unable to add/update values in your configuration file.')
}
setRootKeysSplicers(splicers, obj, objectLiteralNode!, ' ')
setSubKeysSplicers(splicers, obj, objectLiteralNode!, ' ', ' ')
// sort splicers to keep the order of the original file
const sortedSplicers = splicers.sort((a, b) => a.start === b.start ? 0 : a.start > b.start ? 1 : -1)
if (!sortedSplicers.length) return fileContents
let nextStartingIndex = 0
let resultCode = ''
sortedSplicers.forEach((splicer) => {
resultCode += fileContents.slice(nextStartingIndex, splicer.start) + splicer.replaceString
nextStartingIndex = splicer.end
})
return resultCode + fileContents.slice(nextStartingIndex)
}
export function isDefineConfigFunction (ast: File, functionName: string): boolean {
let value = false
visit(ast, {
visitVariableDeclarator (nodePath) {
// if this is a require of cypress
if (nodePath.node.init?.type === 'CallExpression'
&& nodePath.node.init.callee.type === 'Identifier'
&& nodePath.node.init.callee.name === 'require'
&& nodePath.node.init.arguments[0].type === 'StringLiteral'
&& nodePath.node.init.arguments[0].value === 'cypress') {
if (nodePath.node.id?.type === 'ObjectPattern') {
const defineConfigFunctionNode = nodePath.node.id.properties.find((prop) => {
return prop.type === 'ObjectProperty'
&& prop.key.type === 'Identifier'
&& prop.key.name === 'defineConfig'
})
if (defineConfigFunctionNode) {
value = (defineConfigFunctionNode as any).value?.name === functionName
}
}
}
return false
},
visitImportDeclaration (nodePath) {
if (nodePath.node.source.type === 'StringLiteral'
&& nodePath.node.source.value === 'cypress') {
const defineConfigFunctionNode = nodePath.node.specifiers?.find((specifier) => {
return specifier.type === 'ImportSpecifier'
&& specifier.imported.type === 'Identifier'
&& specifier.imported.name === 'defineConfig'
})
if (defineConfigFunctionNode) {
value = (defineConfigFunctionNode as any).local?.name === functionName
}
}
return false
},
})
return value
}
function setRootKeysSplicers (
splicers: Splicer[],
obj: Record<string, any>,
objectLiteralNode: namedTypes.ObjectExpression,
lineStartSpacer: string,
) {
const objectLiteralStartIndex = (objectLiteralNode as any).start + 1
// add values
const objKeys = Object.keys(obj).filter((key) => ['boolean', 'number', 'string'].includes(typeof obj[key]))
// update values
const keysToUpdate = objKeys.filter((key) => {
return objectLiteralNode.properties.find((prop) => {
return prop.type === 'ObjectProperty'
&& prop.key.type === 'Identifier'
&& prop.key.name === key
})
})
keysToUpdate.forEach(
(key) => {
const propertyToUpdate = propertyFromKey(objectLiteralNode, key)
if (propertyToUpdate) {
setSplicerToUpdateProperty(splicers, propertyToUpdate, obj[key], key, obj)
}
},
)
const keysToInsert = objKeys.filter((key) => !keysToUpdate.includes(key))
if (keysToInsert.length) {
const valuesInserted = `\n${lineStartSpacer}${ keysToInsert.map((key) => `${key}: ${JSON.stringify(obj[key])},`).join(`\n${lineStartSpacer}`)}`
splicers.push({
start: objectLiteralStartIndex,
end: objectLiteralStartIndex,
replaceString: valuesInserted,
})
}
}
function setSubKeysSplicers (
splicers: Splicer[],
obj: Record<string, any>,
objectLiteralNode: namedTypes.ObjectExpression,
lineStartSpacer: string,
parentLineStartSpacer: string,
) {
const objectLiteralStartIndex = (objectLiteralNode as any).start + 1
const keysToUpdateWithObjects: string[] = []
const objSubkeys = Object.keys(obj).filter((key) => typeof obj[key] === 'object').reduce((acc: Array<{parent: string, subkey: string}>, key) => {
keysToUpdateWithObjects.push(key)
Object.entries(obj[key]).forEach(([subkey, value]) => {
if (['boolean', 'number', 'string'].includes(typeof value)) {
acc.push({ parent: key, subkey })
}
})
return acc
}, [])
// add values where the parent key needs to be created
const subkeysToInsertWithoutKey = objSubkeys.filter(({ parent }) => {
return !objectLiteralNode.properties.find((prop) => {
return prop.type === 'ObjectProperty'
&& prop.key.type === 'Identifier'
&& prop.key.name === parent
})
})
const keysToInsertForSubKeys: Record<string, string[]> = {}
subkeysToInsertWithoutKey.forEach((keyTuple) => {
const subkeyList = keysToInsertForSubKeys[keyTuple.parent] || []
subkeyList.push(keyTuple.subkey)
keysToInsertForSubKeys[keyTuple.parent] = subkeyList
})
let subvaluesInserted = ''
for (const key in keysToInsertForSubKeys) {
subvaluesInserted += `\n${parentLineStartSpacer}${key}: {`
keysToInsertForSubKeys[key].forEach((subkey) => {
subvaluesInserted += `\n${parentLineStartSpacer}${lineStartSpacer}${subkey}: ${JSON.stringify(obj[key][subkey])},`
})
subvaluesInserted += `\n${parentLineStartSpacer}},`
}
if (subkeysToInsertWithoutKey.length) {
splicers.push({
start: objectLiteralStartIndex,
end: objectLiteralStartIndex,
replaceString: subvaluesInserted,
})
}
// add/update values where parent key already exists
keysToUpdateWithObjects.filter((parent) => {
return objectLiteralNode.properties.find((prop) => {
return prop.type === 'ObjectProperty'
&& prop.key.type === 'Identifier'
&& prop.key.name === parent
})
}).forEach((key) => {
const propertyToUpdate = propertyFromKey(objectLiteralNode, key)
if (propertyToUpdate?.value.type === 'ObjectExpression') {
setRootKeysSplicers(splicers, obj[key], propertyToUpdate.value, parentLineStartSpacer + lineStartSpacer)
}
})
}
function setSplicerToUpdateProperty (splicers: Splicer[],
propertyToUpdate: namedTypes.ObjectProperty,
updatedValue: any,
key: string,
obj: Record<string, any>) {
if (propertyToUpdate && (isPrimitive(propertyToUpdate.value) || isUndefinedOrNull(propertyToUpdate.value))) {
splicers.push({
start: (propertyToUpdate.value as any).start,
end: (propertyToUpdate.value as any).end,
replaceString: JSON.stringify(updatedValue),
})
} else {
throw new Error('Cypress was unable to add/update values in your configuration file.')
}
}
function propertyFromKey (objectLiteralNode: namedTypes.ObjectExpression | undefined, key: string): namedTypes.ObjectProperty | undefined {
return objectLiteralNode?.properties.find((prop) => {
return prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === key
}) as namedTypes.ObjectProperty
}
function isPrimitive (value: NodePath['node']): value is namedTypes.NumericLiteral | namedTypes.StringLiteral | namedTypes.BooleanLiteral {
return value.type === 'NumericLiteral' || value.type === 'StringLiteral' || value.type === 'BooleanLiteral'
}
function isUndefinedOrNull (value: NodePath['node']): value is namedTypes.Identifier {
return value.type === 'Identifier' && ['undefined', 'null'].includes(value.name)
}
interface Splicer{
start: number
end: number
replaceString: string
}

View File

@@ -20,7 +20,7 @@ describe('init component tests script', () => {
let execStub: SinonStub | null = null
const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output')
const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.json')
const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.config.ts')
beforeEach(async () => {
logSpy = sinon.spy(global.console, 'log')
@@ -55,9 +55,9 @@ describe('init component tests script', () => {
function snapshotGeneratedFiles (name: string) {
snapshot(
`${name} cypress.json`,
`${name} cypress.config.ts`,
fs.readFileSync(
path.join(e2eTestOutputPath, 'cypress.json'),
path.join(e2eTestOutputPath, 'cypress.config.ts'),
{ encoding: 'utf-8' },
),
)
@@ -71,7 +71,7 @@ describe('init component tests script', () => {
)
const supportFile = fs.readFileSync(
path.join(e2eTestOutputPath, 'cypress', 'support', 'index.js'),
path.join(e2eTestOutputPath, 'cypress', 'support', 'component.js'),
{ encoding: 'utf-8' },
)
@@ -81,9 +81,9 @@ describe('init component tests script', () => {
}
snapshot(
`${name} support/index.js`,
`${name} support/component.js`,
fs.readFileSync(
path.join(e2eTestOutputPath, 'cypress', 'support', 'index.js'),
path.join(e2eTestOutputPath, 'cypress', 'support', 'component.js'),
{ encoding: 'utf-8' },
),
)
@@ -91,8 +91,8 @@ describe('init component tests script', () => {
it('determines more presumable configuration to suggest', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress/support/index.js': '',
'/cypress.config.ts': 'export default {}',
'/cypress/support/component.js': '',
'/cypress/plugins/index.js': 'module.exports = (on, config) => {}',
// For next.js user will have babel config, but we want to suggest to use the closest config for the application code
'/babel.config.js': 'module.exports = { }',
@@ -114,8 +114,8 @@ describe('init component tests script', () => {
it('automatically suggests to the user which config to use', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress/support/index.js': 'import "./commands.js";',
'/cypress.config.ts': 'export default {}',
'/cypress/support/component.js': 'import "./commands.js";',
'/cypress/plugins/index.js': 'module.exports = () => {}',
'/package.json': JSON.stringify({
dependencies: {
@@ -145,7 +145,7 @@ describe('init component tests script', () => {
it('Asks for preferred bundling tool if can not determine the right one', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
'/webpack.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { } }),
})
@@ -170,7 +170,7 @@ describe('init component tests script', () => {
it('Asks for framework if more than 1 option was auto detected', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
'/webpack.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { react: '*', vue: '^2.4.5' } }),
})
@@ -195,7 +195,7 @@ describe('init component tests script', () => {
it('installs the right adapter', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
'/webpack.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { react: '16.4.5' } }),
})
@@ -213,7 +213,7 @@ describe('init component tests script', () => {
it('installs the right adapter for vue 3', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
'/vite.config.js': 'module.exports = { }',
'/package.json': JSON.stringify({ dependencies: { vue: '^3.0.0' } }),
})
@@ -226,7 +226,7 @@ describe('init component tests script', () => {
}) as any)
await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true })
expect(execStub).to.be.calledWith('yarn add @cypress/vue@3 --dev')
expect(execStub).to.be.calledWith('yarn add @cypress/vue --dev')
})
it('suggest the right instruction based on user template choice', async () => {
@@ -236,7 +236,7 @@ describe('init component tests script', () => {
react: '^16.0.0',
},
}),
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
})
promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({
@@ -253,9 +253,9 @@ describe('init component tests script', () => {
).to.be.true
})
it('suggests right docs example and cypress.json config based on the `componentFolder` answer', async () => {
it('suggests right docs example and cypress.config.ts config based on the `componentFolder` answer', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
'/package.json': JSON.stringify({
dependencies: {
react: '^16.0.0',
@@ -270,12 +270,11 @@ describe('init component tests script', () => {
await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true })
const injectedCode = fs.readFileSync(path.join(e2eTestOutputPath, 'cypress.json'), { encoding: 'utf-8' })
const injectedCode = require(path.join(e2eTestOutputPath, 'cypress.config.ts'))
expect(injectedCode).to.equal(JSON.stringify(
expect(JSON.stringify(injectedCode.default, null, 2)).to.equal(JSON.stringify(
{
componentFolder: 'cypress/component',
testFiles: '**/*.spec.{js,ts,jsx,tsx}',
specPattern: 'cypress/component/**/*.spec.{js,ts,jsx,tsx}',
},
null,
2,
@@ -284,7 +283,7 @@ describe('init component tests script', () => {
it('Shows help message if cypress files are not created', async () => {
createTempFiles({
'/cypress.json': '{}',
'/cypress.config.ts': 'export default {}',
'/package.json': JSON.stringify({
dependencies: {
react: '^16.0.0',
@@ -307,10 +306,10 @@ describe('init component tests script', () => {
).to.be.true
})
it('Doesn\'t affect injected code if user has custom babel.config.js', async () => {
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': '{}',
'/cypress.config.ts': 'export default {}',
'babel.config.js': `module.exports = ${JSON.stringify({
presets: [
'@babel/preset-env',

View File

@@ -8,6 +8,7 @@ import { guessTemplate } from './templates/guessTemplate'
import { installFrameworkAdapter } from './installFrameworkAdapter'
import { injectPluginsCode, getPluginsSourceExample } from './babel/babelTransform'
import { installDependency } from '../utils'
import { insertValuesInConfigFile } from './config-file-updater/configFileUpdater'
async function injectOrShowConfigCode (injectFn: () => Promise<boolean>, {
code,
@@ -51,27 +52,15 @@ async function injectOrShowConfigCode (injectFn: () => Promise<boolean>, {
injected ? printSuccess() : printFailure()
}
async function injectAndShowCypressJsonConfig (
async function injectAndShowCypressConfig (
cypressJsonPath: string,
componentFolder: string,
) {
const configToInject = {
componentFolder,
testFiles: '**/*.spec.{js,ts,jsx,tsx}',
specPattern: `${componentFolder}/**/*.spec.{js,ts,jsx,tsx}`,
}
async function autoInjectCypressJson () {
const currentConfig = JSON.parse(await fs.readFile(cypressJsonPath, { encoding: 'utf-8' }))
await fs.writeFile(cypressJsonPath, JSON.stringify({
...currentConfig,
...configToInject,
}, null, 2))
return true
}
await injectOrShowConfigCode(autoInjectCypressJson, {
await injectOrShowConfigCode(() => insertValuesInConfigFile(cypressJsonPath, configToInject), {
code: JSON.stringify(configToInject, null, 2),
language: 'js',
filePath: cypressJsonPath,
@@ -94,7 +83,7 @@ async function injectAndShowPluginConfig<T> (template: Template<T>, {
code: await getPluginsSourceExample(ast),
language: 'js',
filePath: pluginsFilePath,
fallbackFileMessage: 'plugins file (https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Plugin-files)',
fallbackFileMessage: 'plugins file (https://on.cypress.io/plugins-file)',
})
}
@@ -171,7 +160,7 @@ export async function initComponentTesting<T> ({ config, useYarn, cypressConfigP
console.log(`Let's setup everything for component testing with ${chalk.cyan(chosenTemplateName)}:`)
console.log()
await injectAndShowCypressJsonConfig(cypressConfigPath, componentFolder)
await injectAndShowCypressConfig(cypressConfigPath, componentFolder)
await injectAndShowPluginConfig(chosenTemplate, {
templatePayload,
pluginsFilePath,

View File

@@ -48,10 +48,16 @@ type InstallAdapterOptions = {
useYarn: boolean
}
const frameworkDependencies = {
react: '@cypress/react',
'vue@2': '@cypress/vue2',
'vue@3': '@cypress/vue',
}
export async function installFrameworkAdapter (cwd: string, options: InstallAdapterOptions) {
const framework = await guessOrAskForFramework(cwd)
await installDependency(`@cypress/${framework}`, options)
await installDependency(frameworkDependencies[framework], options)
return framework
}

View File

@@ -104,7 +104,7 @@ export function scanFSForAvailableDependency (cwd: string, lookingForDeps: Recor
.some(([dependency, version]) => {
return (
Boolean(lookingForDeps[dependency])
&& validateSemverVersion(version, lookingForDeps[dependency], dependency)
&& validateSemverVersion(version, lookingForDeps[dependency] as string, dependency)
)
}),
}

View File

@@ -15,7 +15,7 @@ type InstallCypressOpts = {
async function copyFiles ({ ignoreExamples, useTypescript }: InstallCypressOpts) {
let fileSpinner = ora('Creating config files').start()
await fs.outputFile(path.resolve(process.cwd(), 'cypress.json'), '{}\n')
await fs.outputFile(path.resolve(process.cwd(), useTypescript ? 'cypress.config.ts' : 'cypress.config.js'), useTypescript ? `export default {}` : `module.exports = {}\n`)
await fs.copy(
initialTemplate.getInitialPluginsFilePath(),
path.resolve('cypress', 'plugins/index.js'),
@@ -44,9 +44,10 @@ async function copyFiles ({ ignoreExamples, useTypescript }: InstallCypressOpts)
'',
].join('\n')
const specFileToCreate = path.resolve('cypress', 'integration', useTypescript ? 'spec.ts' : 'spec.js')
const specFileName = useTypescript ? 'spec.cy.ts' : 'spec.cy.js'
const specFileToCreate = path.resolve('cypress', 'e2e', specFileName)
await fs.outputFile(path.resolve('cypress', 'integration', useTypescript ? 'spec.js' : 'spec.ts'), dummySpec)
await fs.outputFile(specFileToCreate, dummySpec)
console.log(`In order to ignore examples a spec file ${chalk.green(path.relative(process.cwd(), specFileToCreate))}.`)
}
@@ -54,23 +55,24 @@ async function copyFiles ({ ignoreExamples, useTypescript }: InstallCypressOpts)
}
export async function findInstalledOrInstallCypress (options: InstallCypressOpts) {
let cypressJsonPath = await findUp('cypress.json')
const configFile = options.useTypescript ? 'cypress.config.ts' : 'cypress.config.js'
let cypressConfigPath = await findUp(configFile)
if (!cypressJsonPath) {
if (!cypressConfigPath) {
await installDependency('cypress', options)
await copyFiles(options)
cypressJsonPath = await findUp('cypress.json')
cypressConfigPath = await findUp(configFile)
}
if (!cypressJsonPath) {
if (!cypressConfigPath) {
throw new Error('Unexpected error during cypress installation.')
}
const config = await import(cypressConfigPath)
return {
cypressConfigPath: cypressJsonPath,
config: JSON.parse(
fs.readFileSync(cypressJsonPath, { encoding: 'utf-8' }).toString(),
) as Record<string, string>,
cypressConfigPath,
config: config.default,
}
}

View File

@@ -148,9 +148,9 @@ describe('create-cypress-tests', () => {
await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false })
expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'))).to.equal(true)
expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'index.js'))).to.equal(true)
expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'e2e.js'))).to.equal(true)
expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'commands.js'))).to.equal(true)
expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress.json'))).to.equal(true)
expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress.config.ts'))).to.equal(true)
})
it('Copies tsconfig if typescript is installed', async () => {

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