Merge branch 'develop' into feature-multidomain

This commit is contained in:
Chris Breiding
2022-01-03 10:20:39 -05:00
84 changed files with 2030 additions and 459 deletions

56
.github/CODEOWNERS vendored
View File

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

View File

@@ -281,7 +281,7 @@ Here is a list of the core packages in this repository with a short description,
| [extension](./packages/extension) | `@packages/extension` | The Cypress Chrome browser extension |
| [https-proxy](./packages/https-proxy) | `@packages/https-proxy` | This does https proxy for handling http certs and traffic. |
| [net-stubbing](./packages/net-stubbing) | `@packages/net-stubbing` | Contains server side code for Cypress' network stubbing features. |
| [network](./packages/networ ) | `@packages/network` | Various utilities related to networking. |
| [network](./packages/network) | `@packages/network` | Various utilities related to networking. |
| [proxy](./packages/proxy) | `@packages/proxy` | Code for Cypress' network proxy layer. |
| [launcher](./packages/launcher) | `@packages/launcher` | Finds and launches browsers installed on your system. |
| [reporter](./packages/reporter) | `@packages/reporter` | The reporter shows the running results of the tests (The Command Log UI). |

View File

@@ -1,4 +1,4 @@
{
"chrome:beta": "96.0.4664.45",
"chrome:stable": "96.0.4664.45"
"chrome:beta": "97.0.4692.56",
"chrome:stable": "96.0.4664.110"
}

View File

@@ -29,6 +29,7 @@ mainBuildFilters: &mainBuildFilters
only:
- develop
- 10.0-release
- test-binary-downstream-windows
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -37,7 +38,6 @@ macWorkflowFilters: &mac-workflow-filters
when:
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ tgriesser/fix/patch-resolutions, << pipeline.git.branch >> ]
- equal: [ renovate/cypress-request-2.x, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
@@ -48,7 +48,7 @@ windowsWorkflowFilters: &windows-workflow-filters
or:
- equal: [ master, << pipeline.git.branch >> ]
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ tgriesser/fix/patch-resolutions, << pipeline.git.branch >> ]
- equal: [ test-binary-downstream-windows, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -1588,7 +1588,7 @@ jobs:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "tgriesser/fix/patch-resolutions" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "test-binary-downstream-windows" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
@@ -1664,7 +1664,12 @@ jobs:
test-binary-and-npm-against-other-projects:
<<: *defaults
resource_class: medium
parameters:
<<: *defaultsParameters
resource_class:
type: string
default: medium
resource_class: << parameters.resource_class >>
steps:
# needs uploaded NPM and test binary
- restore_cached_workspace
@@ -1673,23 +1678,14 @@ jobs:
- run: ls -la binary-url.json npm-package-url.json
- run: cat binary-url.json
- run: cat npm-package-url.json
- run: mkdir /tmp/testing
- run:
name: create dummy package
working_directory: /tmp/testing
command: npm init -y
- run:
# install NPM from unique urls
name: Install Cypress
name: Install Cypress Binary in Dummy Package
command: |
node scripts/test-unique-npm-and-binary.js \
--npm npm-package-url.json \
--binary binary-url.json \
--cwd /tmp/testing
- run:
name: Verify Cypress binary
working_directory: /tmp/testing
command: $(yarn bin)/cypress verify
- run:
name: Running other test projects with new NPM package and binary
command: |
@@ -1720,19 +1716,19 @@ jobs:
- run:
name: Cypress version
working_directory: test-binary
command: $(yarn bin)/cypress version
command: $(yarn bin cypress) version
- run:
name: Verify Cypress binary
working_directory: test-binary
command: $(yarn bin)/cypress verify
command: $(yarn bin cypress) verify
- run:
name: Cypress help
working_directory: test-binary
command: $(yarn bin)/cypress help
command: $(yarn bin cypress) help
- run:
name: Cypress info
working_directory: test-binary
command: $(yarn bin)/cypress info
command: $(yarn bin cypress) info
- store-npm-logs
test-npm-module-on-minimum-node-version:
@@ -1891,7 +1887,7 @@ jobs:
CYPRESS_PROJECT_ID=$TEST_TINY_PROJECT_ID \
CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \
CYPRESS_INTERNAL_ENV=staging \
$(yarn bin)/cypress run --record
$(yarn bin cypress) run --record
- store-npm-logs
test-binary-against-recipes-firefox:
@@ -2026,11 +2022,11 @@ jobs:
- run:
name: Cypress help
working_directory: test-binary
command: $(yarn bin)/cypress help
command: $(yarn bin cypress) help
- run:
name: Cypress info
working_directory: test-binary
command: $(yarn bin)/cypress info
command: $(yarn bin cypress) info
- run:
name: Add Cypress demo
working_directory: test-binary
@@ -2038,11 +2034,11 @@ jobs:
- run:
name: Verify Cypress binary
working_directory: test-binary
command: DEBUG=cypress:cli $(yarn bin)/cypress verify
command: DEBUG=cypress:cli $(yarn bin cypress) verify
- run:
name: Run Cypress binary
working_directory: test-binary
command: DEBUG=cypress:cli $(yarn bin)/cypress run
command: DEBUG=cypress:cli $(yarn bin cypress) run
- store-npm-logs
linux-workflow: &linux-workflow
@@ -2432,6 +2428,14 @@ windows-workflow: &windows-workflow
requires:
- windows-build
- test-binary-and-npm-against-other-projects:
context: test-runner:trigger-test-jobs
name: windows-test-binary-and-npm-against-other-projects
executor: windows
resource_class: windows.medium
requires:
- windows-create-build-artifacts
workflows:
linux:
<<: *linux-workflow

View File

@@ -15,7 +15,7 @@ const logger = require('../logger')
const xvfb = require('../exec/xvfb')
const state = require('./state')
const VERIFY_TEST_RUNNER_TIMEOUT_MS = 30000
const VERIFY_TEST_RUNNER_TIMEOUT_MS = process.env.CYPRESS_VERIFY_TIMEOUT || 30000
const checkExecutable = (binaryDir) => {
const executable = state.getPathToExecutable(binaryDir)

View File

@@ -72,6 +72,14 @@ context('lib/tasks/verify', () => {
expect(verify.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.be.gt(10000)
})
it('accepts custom verify task timeout', () => {
process.env.CYPRESS_VERIFY_TIMEOUT = '500000'
delete require.cache[require.resolve(`${lib}/tasks/verify`)]
const newVerifyInstance = require(`${lib}/tasks/verify`)
expect(newVerifyInstance.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.be.gte(500000)
})
it('logs error and exits when no version of Cypress is installed', () => {
return verify
.start()

View File

@@ -7,13 +7,38 @@ declare namespace Cypress {
type HttpMethod = string
type RequestBody = string | object
type ViewportOrientation = 'portrait' | 'landscape'
type PrevSubject = 'optional' | 'element' | 'document' | 'window'
type PrevSubject = keyof PrevSubjectMap
type TestingType = 'e2e' | 'component'
type PluginConfig = (on: PluginEvents, config: PluginConfigOptions) => void | ConfigOptions | Promise<ConfigOptions>
interface PrevSubjectMap<O = unknown> {
optional: O
element: JQuery
document: Document
window: Window
}
interface CommandOptions {
prevSubject: boolean | PrevSubject | PrevSubject[]
}
interface CommandFn<T extends keyof ChainableMethods> {
(this: Mocha.Context, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
}
interface CommandFnWithSubject<T extends keyof ChainableMethods, S> {
(this: Mocha.Context, prevSubject: S, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
}
interface CommandOriginalFn<T extends keyof ChainableMethods> extends CallableFunction {
(...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]>
}
interface CommandOriginalFnWithSubject<T extends keyof ChainableMethods, S> extends CallableFunction {
(prevSubject: S, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]>
}
interface CommandFnWithOriginalFn<T extends keyof Chainable> {
(this: Mocha.Context, originalFn: CommandOriginalFn<T>, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
}
interface CommandFnWithOriginalFnAndSubject<T extends keyof Chainable, S> {
(this: Mocha.Context, originalFn: CommandOriginalFnWithSubject<T, S>, prevSubject: S, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
}
interface ObjectLike {
[key: string]: any
}
@@ -294,6 +319,11 @@ declare namespace Cypress {
*/
LocalStorage: LocalStorage
/**
* Internal class for session management.
*/
session: Session
/**
* Current testing type, determined by the Test Runner chosen to run.
*/
@@ -328,7 +358,7 @@ declare namespace Cypress {
// 60000
```
*/
config<K extends keyof ConfigOptions>(key: K): ResolvedConfigOptions[K]
config<K extends keyof Config>(key: K): Config[K]
/**
* Sets one configuration value.
* @see https://on.cypress.io/config
@@ -337,7 +367,7 @@ declare namespace Cypress {
Cypress.config('viewportWidth', 800)
```
*/
config<K extends keyof ConfigOptions>(key: K, value: ResolvedConfigOptions[K]): void
config<K extends keyof TestConfigOverrides>(key: K, value: TestConfigOverrides[K]): void
/**
* Sets multiple configuration values at once.
* @see https://on.cypress.io/config
@@ -420,9 +450,16 @@ declare namespace Cypress {
* @see https://on.cypress.io/api/commands
*/
Commands: {
add<T extends keyof Chainable>(name: T, fn: Chainable[T]): void
add<T extends keyof Chainable>(name: T, options: CommandOptions, fn: Chainable[T]): void
overwrite<T extends keyof Chainable>(name: T, fn: Chainable[T]): void
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
add<T extends keyof Chainable>(name: T, options: CommandOptions & {prevSubject: false}, fn: CommandFn<T>): void
add<T extends keyof Chainable, S extends PrevSubject>(
name: T, options: CommandOptions & { prevSubject: true | S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
): void
add<T extends keyof Chainable, S extends PrevSubject>(
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
): void
overwrite<T extends keyof Chainable>(name: T, fn: CommandFnWithOriginalFn<T>): void
overwrite<T extends keyof Chainable, S extends PrevSubject>(name: T, fn: CommandFnWithOriginalFnAndSubject<T, PrevSubjectMap[S]>): void
}
/**
@@ -2248,6 +2285,12 @@ declare namespace Cypress {
$$<TElement extends Element = HTMLElement>(selector: JQuery.Selector, context?: Element | Document | JQuery): JQuery<TElement>
}
type ChainableMethods<Subject = any> = {
[P in keyof Chainable<Subject>]: Chainable<Subject>[P] extends ((...args: any[]) => any)
? Chainable<Subject>[P]
: never
}
interface SinonSpyAgent<A extends sinon.SinonSpy> {
log(shouldOutput?: boolean): Omit<A, 'withArgs'> & Agent<A>
@@ -2879,7 +2922,7 @@ declare namespace Cypress {
xhrUrl: string
}
interface TestConfigOverrides extends Partial<Pick<ConfigOptions, 'animationDistanceThreshold' | 'baseUrl' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>> {
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'>> {
browser?: IsBrowserMatcher | IsBrowserMatcher[]
keystrokeDelay?: number
}
@@ -3079,6 +3122,11 @@ declare namespace Cypress {
onAnyAbort(route: RouteOptions, proxy: any): void
}
interface Session {
// Clear all saved sessions and re-run the current spec file.
clearAllSavedSessions: () => Promise<void>
}
type SameSiteStatus = 'no_restriction' | 'strict' | 'lax'
interface SetCookieOptions extends Loggable, Timeoutable {

View File

@@ -64,7 +64,7 @@ namespace CypressIsCyTests {
declare namespace Cypress {
interface Chainable {
newCommand: (arg: string) => void
newCommand: (arg: string) => Chainable<number>
}
}
@@ -74,20 +74,106 @@ namespace CypressCommandsTests {
arg
return
})
Cypress.Commands.add('newCommand', { prevSubject: true }, (arg) => {
Cypress.Commands.add('newCommand', (arg) => {
// $ExpectType string
arg
})
Cypress.Commands.add('newCommand', function(arg) {
this // $ExpectType Context
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: true }, (subject, arg) => {
subject // $ExpectType unknown
arg // $ExpectType string
return
})
Cypress.Commands.add('newCommand', { prevSubject: false }, (arg) => {
arg // $ExpectType string
return
})
Cypress.Commands.add('newCommand', { prevSubject: 'optional' }, (subject, arg) => {
subject // $ExpectType unknown
arg // $ExpectType string
return
})
Cypress.Commands.add('newCommand', { prevSubject: 'optional' }, (subject, arg) => {
subject // $ExpectType unknown
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: ['optional'] }, (subject, arg) => {
subject // $ExpectType unknown
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: 'document' }, (subject, arg) => {
subject // $ExpectType Document
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: 'window' }, (subject, arg) => {
subject // $ExpectType Window
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: 'element' }, (subject, arg) => {
subject // $ExpectType JQuery<HTMLElement>
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: ['element'] }, (subject, arg) => {
subject // $ExpectType JQuery<HTMLElement>
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: ['element', 'document', 'window'] }, (subject, arg) => {
if (subject instanceof Window) {
subject // $ExpectType Window
} else if (subject instanceof Document) {
subject // $ExpectType Document
} else {
subject // $ExpectType JQuery<HTMLElement>
}
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: ['window', 'document', 'optional', 'element'] }, (subject, arg) => {
if (subject instanceof Window) {
subject // $ExpectType Window
} else if (subject instanceof Document) {
subject // $ExpectType Document
} else if (subject) {
subject // $ExpectType JQuery<HTMLElement>
} else {
subject // $ExpectType void
}
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', (arg) => {
// $ExpectType string
arg
return new Promise((resolve) => {})
return cy.wrap(new Promise<number>((resolve) => { resolve(5) }))
})
Cypress.Commands.overwrite('newCommand', (arg) => {
// $ExpectType string
arg
return
Cypress.Commands.overwrite('newCommand', (originalFn, arg) => {
arg // $ExpectType string
originalFn // $ExpectedType Chainable['newCommand']
originalFn(arg) // $ExpectType Chainable<number>
})
Cypress.Commands.overwrite('newCommand', function(originalFn, arg) {
this // $ExpectType Context
arg // $ExpectType string
originalFn // $ExpectedType Chainable['newCommand']
originalFn.apply(this, [arg]) // $ExpectType Chainable<number>
})
Cypress.Commands.overwrite<'type', 'element'>('type', (originalFn, element, text, options?: Partial<Cypress.TypeOptions & {sensitive: boolean}>) => {
element // $ExpectType JQuery<HTMLElement>
text // $ExpectType string
if (options && options.sensitive) {
// turn off original log
options.log = false
// create our own log with masked message
Cypress.log({
$el: element,
name: 'type',
message: '*'.repeat(text.length),
})
}
return originalFn(element, text, options)
})
}

View File

@@ -11,36 +11,46 @@ The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) di
### Prerequisites
- Ensure you have the following permissions set up:
- An AWS account with permission to create AWS access keys for the Cypress CDN.
- Permissions for your npm account to publish the `cypress` package.
- Permissions to update releases in ZenHub.
- An AWS account with permission to create AWS access keys for the Cypress CDN.
- Permissions for your npm account to publish the `cypress` package.
- Permissions to update releases in ZenHub.
- Set up the following environment variables:
- Cypress AWS access key and secret in `aws_credentials_json`, which looks like this:
```text
aws_credentials_json={"bucket":"cdn.cypress.io","folder":"desktop","key":"...","secret":"..."}
```
- Cypress AWS access key and secret in `aws_credentials_json`, which looks like this:
```text
aws_credentials_json='{"bucket":"cdn.cypress.io","folder":"desktop","key":"...","secret":"..."}'
```
- A [GitHub token](https://github.com/settings/tokens) and a [CircleCI token](https://circleci.com/account/api) in `ci_json`:
```text
ci_json={"githubToken":"...","circleToken":"..."}
```
- You'll also need to put the GitHub token under its own variable and get a [ZenHub API token](https://app.zenhub.com/dashboard/tokens) for the `release-automations` step.
```text
GITHUB_TOKEN="..."
ZENHUB_API_TOKEN="..."
```
- The `cypress-bot` GitHub app credentials are also needed. Ask another team member who has done a deploy for those.
```text
GITHUB_APP_CYPRESS_INSTALLATION_ID=
GITHUB_APP_ID=
GITHUB_PRIVATE_KEY=
```
- For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. If you don't have access, ask a team member who has done a deploy.
```text
CF_ZONEID="..."
CF_TOKEN="..."
```
- Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations.
```text
ci_json='{"githubToken":"...","circleToken":"..."}'
```
- You'll also need to put the GitHub token under its own variable and get a [ZenHub API token](https://app.zenhub.com/dashboard/tokens) for the `release-automations` step.
```text
GITHUB_TOKEN="..."
ZENHUB_API_TOKEN="..."
```
- The `cypress-bot` GitHub app credentials are also needed. Ask another team member who has done a deploy for those.
```text
GITHUB_APP_CYPRESS_INSTALLATION_ID=
GITHUB_APP_ID=
GITHUB_PRIVATE_KEY=
```
- For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. If you don't have access, ask a team member who has done a deploy.
```text
CF_ZONEID="..."
CF_TOKEN="..."
```
- Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations.
### Before Publishing a New Version
@@ -68,6 +78,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
2. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version.
3. Use the `move-binaries` script to move the binaries for `<commit sha>` from `beta` to the `desktop` folder for `<new target version>`. This also purges the cloudflare cache for this version.
```shell
yarn move-binaries --sha <commit sha> --version <new target version>
```
@@ -79,11 +90,13 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
2. Scroll down past the changes to the comments. The first comment should be a `cypress-bot` comment that includes a line beginning `npm install ...`. Grab the `https://cdn.../npm/X.Y.Z/<long sha>/cypress.tgz` link.
![cdn-tgz-link](https://user-images.githubusercontent.com/1157043/80608736-3791e800-8a05-11ea-8d75-e4f80128e857.png)
- Publish to the npm registry straight from the URL:
```shell
npm publish https://cdn.cypress.io/beta/npm/X.Y.Z/<long sha>/cypress.tgz --tag dev
```
5. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output:
```shell
dist-tags:
dev: 3.4.0 latest: 3.3.2
@@ -96,31 +109,36 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- Go into a project, run a quick test, make sure things look right
- Optionally, do more thorough tests:
- Trigger test projects from the command line (if you have the appropriate permissions)
```
```shell
node scripts/test-other-projects.js --npm cypress@X.Y.Z --binary X.Y.Z
```
- Test the new version of Cypress against the Cypress dashboard repo.
- Test the new version of Cypress against the Cypress dashboard repo.
7. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog.
8. Deploy the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation).
- If there is not already a release-specific PR open, create one. You can use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub:
```
```shell
cd packages/issues-in-release
yarn do:changelog --release <release label>
```
- Ensure the changelog is up-to-date and has the correct date.
- Merge any release-specific documentation changes into the main release PR.
- You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check.
- Merge this PR into `master` to deploy it to production.
9. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
9. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
```shell
npm dist-tag add cypress@X.Y.Z
```
10. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system.
```shell
yarn binary-release --version X.Y.Z
```
@@ -135,6 +153,7 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- Move all issues that are still open from the current release to the appropriate future release.
14. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up:
```shell
git commit -am "release X.Y.Z [skip ci]"
git log --pretty=oneline
@@ -142,7 +161,9 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
git tag -a vX.Y.Z <sha>
git push origin vX.Y.Z
```
15. Merge `develop` into `master` and push both branches up. Note: pushing to `master` will automatically publish any independent npm packages that have not yet been published.
```shell
git push origin develop
git checkout master
@@ -152,16 +173,20 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
16. Inside of [cypress-io/release-automations][release-automations]:
- Publish GitHub release to [cypress-io/cypress/releases](https://github.com/cypress-io/cypress/releases) using package `set-releases`:
```shell
cd packages/set-releases && npm run release-log -- --version X.Y.Z
```
- Add a comment to each GH issue that has been resolved with the new published version using package `issues-in-release`:
```shell
cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z
```
- Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left
17. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version.
17. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version. Instructions for updating `cypress-docker-images` can be found [here](https://github.com/cypress-io/cypress-docker-images/blob/master/CONTRIBUTING.md#add-new-included-image).
18. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects:
- [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99)

View File

@@ -1,3 +1,15 @@
# [create-cypress-tests-v1.3.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.2.0...create-cypress-tests-v1.3.0) (2021-12-16)
### Bug Fixes
* Restore broken gif ([#18987](https://github.com/cypress-io/cypress/issues/18987)) ([f251681](https://github.com/cypress-io/cypress/commit/f251681b814b102ca374abdef148b777c4e72c67))
### Features
* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88))
# [create-cypress-tests-v1.2.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.3...create-cypress-tests-v1.2.0) (2021-11-10)

View File

@@ -1,3 +1,10 @@
# [@cypress/eslint-plugin-dev-v5.2.0](https://github.com/cypress-io/cypress/compare/@cypress/eslint-plugin-dev-v5.1.0...@cypress/eslint-plugin-dev-v5.2.0) (2021-12-16)
### Features
* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88))
# [@cypress/eslint-plugin-dev-v5.1.0](https://github.com/cypress-io/cypress/compare/@cypress/eslint-plugin-dev-v5.0.2...@cypress/eslint-plugin-dev-v5.1.0) (2021-02-16)

View File

@@ -1,3 +1,15 @@
# [@cypress/react-v5.11.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.10.3...@cypress/react-v5.11.0) (2021-12-16)
### Bug Fixes
* **react:** link to rerender example ([#19020](https://github.com/cypress-io/cypress/issues/19020)) ([2a471d6](https://github.com/cypress-io/cypress/commit/2a471d633a7cf5bd94cfa7d876ddb27cc32626d1))
### Features
* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88))
# [@cypress/react-v5.10.3](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.10.2...@cypress/react-v5.10.3) (2021-11-01)

View File

@@ -50,7 +50,7 @@
"@types/semver": "7.3.4",
"arg": "4.1.3",
"autoprefixer": "9.7.6",
"axios": "0.18.1",
"axios": "0.21.2",
"babel-loader": "8.0.6",
"babel-plugin-istanbul": "6.0.0",
"babel-plugin-module-resolver": "4.0.0",

View File

@@ -5,6 +5,7 @@ const { allowModuleSourceInPlace } = require('../utils/webpack-helpers')
const { addCypressToWebpackEslintRulesInPlace } = require('../utils/eslint-helpers')
const { getTranspileFolders } = require('../utils/get-transpile-folders')
const { addFolderToBabelLoaderTranspileInPlace } = require('../utils/babel-helpers')
const { reactScriptsFiveModifications, isReactScripts5 } = require('../../dist/react-scripts/reactScriptsFive')
module.exports = function findReactScriptsWebpackConfig (config, {
webpackConfigPath,
@@ -28,6 +29,12 @@ module.exports = function findReactScriptsWebpackConfig (config, {
addFolderToBabelLoaderTranspileInPlace(cypressFolder, webpackConfig)
})
if (isReactScripts5) {
debug('Modifying configuration for react-scripts@5')
reactScriptsFiveModifications(webpackConfig)
}
debug('resolved webpack config: %o', webpackConfig)
return webpackConfig

View File

@@ -0,0 +1,61 @@
import _debug from 'debug'
import type { Configuration } from 'webpack'
import reactScriptsPackageJson from 'react-scripts/package.json'
const debug = _debug('@cypress/react:react-scripts')
type DefinePlugin =
| { definitions: Record<string, Record<string, any>> }
| undefined;
type ESLintWebpackPlugin =
| { options: { baseConfig?: { globals?: Record<string, any> } } }
| undefined;
export function reactScriptsFiveModifications (webpackConfig: Configuration) {
// React-Scripts sets the webpack target to ["browserslist"] which tells
// webpack to target the browsers found within the browserslist config
// depending on the environment (process.env.NODE_ENV). Since we set
// process.env.NODE_ENV = "test", webpack is unable to find any browsers and errors.
// We set BROWSERSLIST_ENV = "development" to override the default NODE_ENV search of browsers.
if (!process.env.BROWSERSLIST_ENV) {
process.env.BROWSERSLIST_ENV = 'development'
}
// We use the "development" configuration of the react-scripts webpack config.
// There is a conflict when settings process.env.NODE_ENV = "test" since DefinePlugin
// uses the "development" configuration and expects process.env.NODE_ENV = "development".
const definePlugin: DefinePlugin = webpackConfig.plugins?.find(
(plugin) => plugin.constructor.name === 'DefinePlugin'
) as unknown as DefinePlugin
if (definePlugin) {
const processEnv = definePlugin.definitions['process.env']
processEnv.NODE_ENV = JSON.stringify('development')
debug('Found "DefinePlugin", modified "process.env" definition %o', processEnv)
}
// React-Scripts v5 no longers uses a loader to configure eslint, so we add globals
// to the plugin.
const eslintPlugin = webpackConfig.plugins?.find(
(plugin) => plugin.constructor.name === 'ESLintWebpackPlugin'
) as unknown as ESLintWebpackPlugin
if (eslintPlugin) {
const cypressGlobals = ['cy', 'Cypress', 'before', 'after', 'context']
.reduce((acc, global) => ({ ...acc, [global]: 'writable' }), {})
eslintPlugin.options.baseConfig = {
...eslintPlugin.options.baseConfig,
globals: {
...eslintPlugin.options.baseConfig?.globals,
...cypressGlobals,
},
}
debug('Found ESLintWebpackPlugin, modified eslint config %o', eslintPlugin.options.baseConfig)
}
}
export const isReactScripts5 = Number(reactScriptsPackageJson.version[0]) >= 5

View File

@@ -19,7 +19,8 @@
"cypress"
] /* Type declaration files to be included in compilation. */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
}

View File

@@ -1,3 +1,10 @@
# [@cypress/vite-dev-server-v2.2.2](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v2.2.1...@cypress/vite-dev-server-v2.2.2) (2021-12-16)
### Bug Fixes
* check the port is avail on all local hosts ([#19402](https://github.com/cypress-io/cypress/issues/19402)) ([4826175](https://github.com/cypress-io/cypress/commit/4826175040bfc024a36df47dc0c74f2871fa944f))
# [@cypress/vite-dev-server-v2.2.1](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v2.2.0...@cypress/vite-dev-server-v2.2.1) (2021-11-19)

View File

@@ -1,4 +1,4 @@
import { resolve, posix, sep } from 'path'
import { resolve, sep } from 'path'
import { readFile } from 'fs'
import { promisify } from 'util'
import Debug from 'debug'

View File

@@ -47,7 +47,7 @@ const resolveServerConfig = async ({ viteConfig, options }: StartDevServerOption
finalConfig.server = finalConfig.server || {}
finalConfig.server.port = await getPort({ port: finalConfig.server.port || 3000, host: 'localhost' }),
finalConfig.server.port = await getPort({ port: finalConfig.server.port || 3000 }),
// Ask vite to pre-optimize all dependencies of the specs
finalConfig.optimizeDeps = finalConfig.optimizeDeps || {}

View File

@@ -1,3 +1,10 @@
# [@cypress/vue-v3.1.0](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.0.5...@cypress/vue-v3.1.0) (2021-12-16)
### Features
* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88))
# [@cypress/vue-v3.0.5](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.0.4...@cypress/vue-v3.0.5) (2021-11-10)

View File

@@ -31,7 +31,7 @@
"@vue/cli-plugin-babel": "~4.4.0",
"@vue/cli-service": "~4.4.0",
"@vue/compiler-sfc": "3.1.5",
"axios": "0.19.2",
"axios": "0.21.2",
"babel-loader": "8.1.0",
"babel-plugin-istanbul": "^6.0.0",
"babel-preset-typescript-vue3": "^2.0.14",

View File

@@ -1,3 +1,10 @@
# [@cypress/webpack-dev-server-v1.8.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.7.0...@cypress/webpack-dev-server-v1.8.0) (2021-12-16)
### Features
* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88))
# [@cypress/webpack-dev-server-v1.7.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.6.0...@cypress/webpack-dev-server-v1.7.0) (2021-10-15)

View File

@@ -1,3 +1,10 @@
# [@cypress/webpack-preprocessor-v5.11.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-preprocessor-v5.10.0...@cypress/webpack-preprocessor-v5.11.0) (2021-12-16)
### Features
* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88))
# [@cypress/webpack-preprocessor-v5.10.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-preprocessor-v5.9.1...@cypress/webpack-preprocessor-v5.10.0) (2021-11-10)

View File

@@ -1,6 +1,6 @@
{
"name": "cypress",
"version": "9.1.1",
"version": "9.2.0",
"description": "Cypress.io end to end testing tool",
"private": true,
"scripts": {

View File

@@ -23,6 +23,7 @@ exports['src/index .getDefaultValues returns list of public config keys 1'] = {
"e2e": {},
"env": {},
"execTimeout": 60000,
"exit": true,
"experimentalFetchPolyfill": false,
"experimentalInteractiveRunEvents": false,
"experimentalSessionSupport": false,
@@ -33,6 +34,8 @@ exports['src/index .getDefaultValues returns list of public config keys 1'] = {
"ignoreTestFiles": "*.hot-update.js",
"includeShadowDom": false,
"integrationFolder": "cypress/integration",
"isInteractive": true,
"keystrokeDelay": 0,
"modifyObstructiveCode": true,
"numTestsKeptInMemory": 50,
"pageLoadTimeout": 60000,
@@ -97,6 +100,7 @@ exports['src/index .getPublicConfigKeys returns list of public config keys 1'] =
"e2e",
"env",
"execTimeout",
"exit",
"experimentalFetchPolyfill",
"experimentalInteractiveRunEvents",
"experimentalSessionSupport",
@@ -107,6 +111,7 @@ exports['src/index .getPublicConfigKeys returns list of public config keys 1'] =
"ignoreTestFiles",
"includeShadowDom",
"integrationFolder",
"keystrokeDelay",
"modifyObstructiveCode",
"nodeVersion",
"numTestsKeptInMemory",
@@ -142,5 +147,6 @@ exports['src/index .getPublicConfigKeys returns list of public config keys 1'] =
"watchForFileChanges",
"browsers",
"hosts",
"isInteractive",
"modifyObstructiveCode"
]

View File

@@ -154,4 +154,4 @@ Expected \`mockConfigKey\` to be a fully qualified URL (starting with \`http://\
exports['empty string'] = `
Expected \`mockConfigKey\` to be a fully qualified URL (starting with \`http://\` or \`https://\`). Instead the value was: \`""\`
`
`

View File

@@ -20,6 +20,7 @@ const breakingKeys = _.map(breakingOptions, 'name')
const defaultValues = createIndex(options, 'name', 'defaultValue')
const publicConfigKeys = _(options).reject({ isInternal: true }).map('name').value()
const validationRules = createIndex(options, 'name', 'validation')
const testConfigOverrideOptions = createIndex(options, 'name', 'canUpdateDuringTestTime')
module.exports = {
allowed: (obj = {}) => {
@@ -101,4 +102,16 @@ module.exports = {
}
})
},
validateNoReadOnlyConfig: (config, onErr) => {
let errProperty
Object.keys(config).some((option) => {
return errProperty = testConfigOverrideOptions[option] === false ? option : undefined
})
if (errProperty) {
return onErr(errProperty)
}
},
}

View File

@@ -6,6 +6,10 @@ interface ResolvedConfigOption {
validation: Function
isFolder?: boolean
isExperimental?: boolean
/**
* Can be mutated with Cypress.config() or test-specific configuration overrides
*/
canUpdateDuringTestTime?: boolean
}
interface RuntimeConfigOption {
@@ -13,6 +17,10 @@ interface RuntimeConfigOption {
defaultValue: any
validation: Function
isInternal?: boolean
/**
* Can be mutated with Cypress.config() or test-specific configuration overrides
*/
canUpdateDuringTestTime?: boolean
}
interface BreakingOption {
@@ -72,158 +80,204 @@ const resolvedOptions: Array<ResolvedConfigOption> = [
name: 'animationDistanceThreshold',
defaultValue: 5,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'baseUrl',
defaultValue: null,
validation: validate.isFullyQualifiedUrl,
canUpdateDuringTestTime: true,
}, {
name: 'blockHosts',
defaultValue: null,
validation: validate.isStringOrArrayOfStrings,
canUpdateDuringTestTime: true,
}, {
name: 'chromeWebSecurity',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'clientCertificates',
defaultValue: [],
validation: validate.isValidClientCertificatesSet,
canUpdateDuringTestTime: false,
}, {
name: 'component',
// runner-ct overrides
defaultValue: {},
validation: isValidConfig,
canUpdateDuringTestTime: false,
}, {
name: 'componentFolder',
defaultValue: 'cypress/component',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'defaultCommandTimeout',
defaultValue: 4000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'downloadsFolder',
defaultValue: 'cypress/downloads',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'e2e',
// e2e runner overrides
defaultValue: {},
validation: isValidConfig,
canUpdateDuringTestTime: false,
}, {
name: 'env',
defaultValue: {},
validation: validate.isPlainObject,
canUpdateDuringTestTime: true,
}, {
name: 'execTimeout',
defaultValue: 60000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'exit',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalFetchPolyfill',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalInteractiveRunEvents',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalSessionSupport',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: true,
}, {
name: 'experimentalSourceRewriting',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalStudio',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'fileServerFolder',
defaultValue: '',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'fixturesFolder',
defaultValue: 'cypress/fixtures',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'ignoreTestFiles',
defaultValue: '*.hot-update.js',
validation: validate.isStringOrArrayOfStrings,
canUpdateDuringTestTime: true,
}, {
name: 'includeShadowDom',
defaultValue: false,
validation: validate.isBoolean,
canUpdateDuringTestTime: true,
}, {
name: 'integrationFolder',
defaultValue: 'cypress/integration',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'keystrokeDelay',
defaultValue: 0,
validation: validate.isNumberOrFalse,
canUpdateDuringTestTime: true,
}, {
name: 'modifyObstructiveCode',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'nodeVersion',
validation: validate.isOneOf('bundled', 'system'),
canUpdateDuringTestTime: false,
}, {
name: 'numTestsKeptInMemory',
defaultValue: 50,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'pageLoadTimeout',
defaultValue: 60000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'pluginsFile',
defaultValue: 'cypress/plugins',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'port',
defaultValue: null,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'projectId',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: true,
}, {
name: 'redirectionLimit',
defaultValue: 20,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'reporter',
defaultValue: 'spec',
validation: validate.isString,
canUpdateDuringTestTime: true,
}, {
name: 'reporterOptions',
defaultValue: null,
validation: validate.isPlainObject,
canUpdateDuringTestTime: true,
}, {
name: 'requestTimeout',
defaultValue: 5000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'resolvedNodePath',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: false,
}, {
name: 'resolvedNodeVersion',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: false,
}, {
name: 'responseTimeout',
defaultValue: 30000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'retries',
defaultValue: {
@@ -231,82 +285,101 @@ const resolvedOptions: Array<ResolvedConfigOption> = [
openMode: 0,
},
validation: validate.isValidRetriesConfig,
canUpdateDuringTestTime: true,
}, {
name: 'screenshotOnRunFailure',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: true,
}, {
name: 'screenshotsFolder',
defaultValue: 'cypress/screenshots',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'slowTestThreshold',
defaultValue: (options: Record<string, any> = {}) => options.testingType === 'component' ? 250 : 10000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'scrollBehavior',
defaultValue: 'top',
validation: validate.isOneOf('center', 'top', 'bottom', 'nearest', false),
canUpdateDuringTestTime: true,
}, {
name: 'supportFile',
defaultValue: 'cypress/support',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'supportFolder',
defaultValue: false,
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'taskTimeout',
defaultValue: 60000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'testFiles',
defaultValue: '**/*.*',
validation: validate.isStringOrArrayOfStrings,
canUpdateDuringTestTime: false,
}, {
name: 'trashAssetsBeforeRuns',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'userAgent',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: false,
}, {
name: 'video',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'videoCompression',
defaultValue: 32,
validation: validate.isNumberOrFalse,
canUpdateDuringTestTime: false,
}, {
name: 'videosFolder',
defaultValue: 'cypress/videos',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'videoUploadOnPasses',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'viewportHeight',
defaultValue: 660,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'viewportWidth',
defaultValue: 1000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'waitForAnimations',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: true,
}, {
name: 'watchForFileChanges',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
},
]
@@ -316,15 +389,18 @@ const runtimeOptions: Array<RuntimeConfigOption> = [
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'browsers',
defaultValue: [],
validation: validate.isValidBrowserList,
canUpdateDuringTestTime: false,
}, {
name: 'clientRoute',
defaultValue: '/__/',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'configFile',
defaultValue: 'cypress.json',
@@ -332,59 +408,76 @@ const runtimeOptions: Array<RuntimeConfigOption> = [
// not truly internal, but can only be set via cli,
// so we don't consider it a "public" option
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'devServerPublicPathRoute',
defaultValue: '/__cypress/src',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'hosts',
defaultValue: null,
validation: validate.isPlainObject,
canUpdateDuringTestTime: false,
}, {
name: 'isInteractive',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'isTextTerminal',
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'morgan',
defaultValue: true,
validation: validate.isBoolean,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'modifyObstructiveCode',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'namespace',
defaultValue: '__cypress',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'reporterRoute',
defaultValue: '/__cypress/reporter',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'socketId',
defaultValue: null,
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'socketIoCookie',
defaultValue: '__socket.io',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'socketIoRoute',
defaultValue: '/__socket.io',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'xhrRoute',
defaultValue: '/xhrs/',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
},
]

View File

@@ -94,7 +94,7 @@ describe('src/index', () => {
'baseUrl': 'https://',
}, errorFn)
expect(errorFn).to.have.been.callCount(0)
expect(errorFn).to.have.callCount(0)
})
it('calls error callback if config is invalid', () => {
@@ -125,7 +125,7 @@ describe('src/index', () => {
configFile: 'config.js',
})
expect(errorFn).to.have.been.callCount(0)
expect(errorFn).to.have.callCount(0)
})
it('calls error callback if config contains breaking option that should throw an error', () => {
@@ -146,4 +146,31 @@ describe('src/index', () => {
})
})
})
describe('.validateNoReadOnlyConfig', () => {
it('returns an error if validation fails', () => {
const errorFn = sinon.spy()
configUtil.validateNoReadOnlyConfig({ chromeWebSecurity: false }, errorFn)
expect(errorFn).to.have.callCount(1)
expect(errorFn).to.have.been.calledWithMatch(/chromeWebSecurity/)
})
it('does not return an error if validation succeeds', () => {
const errorFn = sinon.spy()
configUtil.validateNoReadOnlyConfig({ requestTimeout: 1000 }, errorFn)
expect(errorFn).to.have.callCount(0)
})
it('does not return an error if configuration is a non-Cypress config option', () => {
const errorFn = sinon.spy()
configUtil.validateNoReadOnlyConfig({ foo: 'bar' }, errorFn)
expect(errorFn).to.have.callCount(0)
})
})
})

View File

@@ -17,5 +17,25 @@
h1 { font-size: 32px }
</style>
</div>
<!--
// https://github.com/cypress-io/cypress/issues/19377
Below is added to ensure `style`, `script` tags are not removed by `cy.contains()` command.
-->
<p>Hello</p>
<button id="my_button_1" style="display: none;">I'm a button</button>
<button id="my_button_2">Click me</button>
<div id="result"></div>
<style>
button#my_button_1 {
background-color: red;
display: inline !important;
}
</style>
<script id="my_script">
document.getElementById('my_button_2').addEventListener('click', function() {
document.getElementById('result').innerHTML = 'This is the result';
});
</script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<script>
function defaultBehavior(e) {
document.getElementById('result').innerHTML = JSON.stringify(e.target.getAttribute('target'))
}
function setAttributeBehavior(e) {
e.target.setAttribute('target', '_top')
document.getElementById('result2').innerHTML = JSON.stringify(e.target.getAttribute('target'))
}
function removeAttributeBehavior(e) {
e.target.setAttribute('target', '_top')
e.target.removeAttribute('target')
document.getElementById('result3').innerHTML = JSON.stringify(e.target.getAttribute('target'))
}
</script>
<div>
<a href="#" id="link" onclick="defaultBehavior(event)">link 1</a>
<div id="result"></div>
</div>
<div>
<a href="#" id="link2" onclick="setAttributeBehavior(event)">link 2</a>
<div id="result2"></div>
</div>
<div>
<a href="#" id="link3" onclick="removeAttributeBehavior(event)">link 3</a>
<div id="result3"></div>
</div>
</body>
</html>

View File

@@ -1,6 +1,39 @@
const { assertLogLength } = require('../../support/utils')
const { $, _ } = Cypress
const captureCommands = () => {
const commands = []
let current
cy.on('command:start', (command) => {
current = command
commands.push({
name: command.attributes.name,
snapshots: 0,
retries: 0,
})
})
cy.on('command:retry', () => {
commands[commands.length - 1].retries++
})
cy.on('snapshot', () => {
// Snapshots can occur outside the context of a command - for example, `expect(foo).to.exist` without any wrapping cy command.
// So we keep track of the current command when one starts, and if we're not inside that, create an 'empty' command
// for the snapshot to belong to
if (!commands.length || current !== cy.state('current')) {
current = null
commands.push({ name: null, snapshots: 0, retries: 0 })
}
commands[commands.length - 1].snapshots++
})
return () => _.cloneDeep(commands)
}
describe('src/cy/commands/assertions', () => {
before(() => {
cy
@@ -10,10 +43,14 @@ describe('src/cy/commands/assertions', () => {
})
})
let testCommands
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
testCommands = captureCommands()
})
context('#should', () => {
@@ -29,8 +66,14 @@ describe('src/cy/commands/assertions', () => {
})
it('returns the subject for chainability', () => {
cy.noop({ foo: 'bar' }).should('deep.eq', { foo: 'bar' }).then((obj) => {
expect(obj).to.deep.eq({ foo: 'bar' })
cy
.noop({ foo: 'bar' }).should('deep.eq', { foo: 'bar' })
.then((obj) => {
expect(testCommands()).to.eql([
{ name: 'noop', snapshots: 0, retries: 0 },
{ name: 'should', snapshots: 1, retries: 0 },
{ name: 'then', snapshots: 0, retries: 0 },
])
})
})
@@ -121,11 +164,19 @@ describe('src/cy/commands/assertions', () => {
cy.wrap(obj).then(() => {
setTimeout(() => {
obj.foo = 'baz'
}
, 100)
}, 100)
cy.wrap(obj)
}).should('deep.eq', { foo: 'baz' })
})
.should('deep.eq', { foo: 'baz' })
.then(() => {
expect(testCommands()).to.containSubset([
{ name: 'wrap', snapshots: 1, retries: 0 },
{ name: 'then', snapshots: 0, retries: 0 },
{ name: 'wrap', snapshots: 2, retries: (r) => r > 1 },
{ name: 'then', snapshots: 0, retries: 0 },
])
})
})
// https://github.com/cypress-io/cypress/issues/16006
@@ -151,6 +202,18 @@ describe('src/cy/commands/assertions', () => {
cy.get('button:first').should(($button) => {
expect($button).to.have.class('ready')
})
.then(() => {
expect(testCommands()).to.eql([
// cy.get() has 2 snapshots, 1 for itself, and 1
// for the .should(...) assertion.
// TODO: Investigate whether or not the 2 commands are
// snapshotted at the same time. If there's no tick between
// them, we could reuse the snapshots
{ name: 'get', snapshots: 2, retries: 2 },
{ name: 'then', snapshots: 0, retries: 0 },
])
})
})
it('works with regular objects', () => {
@@ -1475,7 +1538,7 @@ describe('src/cy/commands/assertions', () => {
done()
})
cy.get('div').should(($divs) => {
cy.get('div', { timeout: 100 }).should(($divs) => {
expect($divs, 'Filter should have 1 items').to.have.length(1)
})
})
@@ -2927,4 +2990,42 @@ describe('src/cy/commands/assertions', () => {
cy.get('.foo').should('not.exist')
})
})
context('implicit assertions', () => {
// https://github.com/cypress-io/cypress/issues/18549
// A targeted test for the above issue - in the absence of retries, only a single snapshot
// should be taken.
it('only snapshots once when failing to find DOM elements and not retrying', (done) => {
cy.on('fail', (err) => {
expect(testCommands()).to.eql([{
name: 'get',
snapshots: 1,
retries: 0,
}])
done()
})
cy.get('.badId', { timeout: 0 })
})
// https://github.com/cypress-io/cypress/issues/18549
// This issue was also causing two DOM snapshots to be taken every 50ms
// while waiting for an element to exist. The first test is sufficient to
// prevent regressions of the specific issue, but this one is intended to
// more generally assert that retries do not trigger multiple snapshots.
it('only snapshots once when retrying assertions', (done) => {
cy.on('fail', (err) => {
expect(testCommands()).to.containSubset([{
name: 'get',
snapshots: 1,
retries: (v) => v > 1,
}])
done()
})
cy.get('.badId', { timeout: 1000 })
})
})
})

View File

@@ -101,9 +101,7 @@ describe('src/cy/commands/fixtures', () => {
return null
})
it('throws if fixturesFolder is set to false', {
fixturesFolder: false,
}, function (done) {
it('throws if fixturesFolder is set to false', { fixturesFolder: false }, function (done) {
cy.on('fail', () => {
const { lastLog } = this

View File

@@ -315,6 +315,59 @@ describe('src/cy/commands/navigation', () => {
})
})
it('handles hashchange events', () => {
const emit = cy.spy(Cypress, 'emit').log(false).withArgs('url:changed')
cy
.visit('/fixtures/generic.html')
.get('#hashchange').click()
.then(() => {
cy.go('back')
cy.go('forward')
cy.get('#dimensions').click()
cy.go('back')
cy.go('back')
})
.then(function () {
expect(emit.firstCall).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/generic.html',
)
expect(emit.secondCall).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/generic.html#hashchange',
)
expect(emit.thirdCall).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/generic.html',
)
expect(emit.getCall(3)).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/generic.html#hashchange',
)
expect(emit.getCall(4)).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/dimensions.html',
)
expect(emit.getCall(5)).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/generic.html#hashchange',
)
expect(emit.getCall(6)).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/generic.html',
)
expect(emit.callCount).to.eq(7)
})
})
it('removes listeners', () => {
cy
.visit('/fixtures/generic.html')
@@ -1440,30 +1493,6 @@ describe('src/cy/commands/navigation', () => {
})
it('throws attempting to visit 2 unique ip addresses', function (done) {
const $autIframe = cy.state('$autIframe')
const load = () => {
return $autIframe.trigger('load')
}
cy.stub(Cypress, 'backend')
.withArgs('resolve:url')
.resolves({
isOkStatusCode: true,
isHtml: true,
url: 'http://127.0.0.1:3500',
})
// whenever we're told to change the src
// just fire the load event directly on the $autIframe
cy.stub(Cypress.utils, 'iframeSrc').callsFake(load)
// make it seem like we're already on http://127.0.0.1:3500
const one = Cypress.Location.create('http://127.0.0.1:3500/fixtures/generic.html')
cy.stub(Cypress.utils, 'locExisting')
.returns(one)
cy.on('fail', (err) => {
const { lastLog } = this
@@ -2136,8 +2165,7 @@ describe('src/cy/commands/navigation', () => {
})
})
// this tests isLoading spinner
// and page load event
// this tests isLoading spinner and page load event
context('#page loading', () => {
beforeEach(function () {
this.logs = []
@@ -2453,6 +2481,105 @@ describe('src/cy/commands/navigation', () => {
})
})
// https://github.com/cypress-io/cypress/issues/19230
it('filters page load events when going back with window navigation', () => {
const emit = cy.spy(Cypress, 'emit').log(false).withArgs('navigation:changed')
cy
.visit('/fixtures/generic.html')
.get('#hashchange').click()
.window().then((win) => {
return new Promise((resolve) => {
cy.once('navigation:changed', resolve)
win.history.back()
}).then(() => {
return new Promise((resolve) => {
cy.once('navigation:changed', resolve)
win.history.forward()
})
})
})
cy.get('#dimensions').click()
.window().then((win) => {
return new Promise((resolve) => {
cy.on('navigation:changed', (event) => {
if (event.includes('(load)')) {
resolve()
}
})
win.history.back()
})
.then(() => {
return new Promise((resolve) => {
cy.on('navigation:changed', resolve)
win.history.back()
})
})
.then(() => {
expect(emit.firstCall).to.be.calledWith(
'navigation:changed',
'page navigation event (load)',
)
expect(emit.secondCall).to.be.calledWith(
'navigation:changed',
'page navigation event (before:load)',
)
expect(emit.thirdCall).to.be.calledWith(
'navigation:changed',
'page navigation event (load)',
)
expect(emit.getCall(3)).to.be.calledWithMatch(
'navigation:changed',
'hashchange',
)
expect(emit.getCall(4)).to.be.calledWithMatch(
'navigation:changed',
'hashchange',
)
expect(emit.getCall(5)).to.be.calledWithMatch(
'navigation:changed',
'hashchange',
)
expect(emit.getCall(6)).to.be.calledWith(
'navigation:changed',
'page navigation event (before:load)',
)
expect(emit.getCall(7)).to.be.calledWith(
'navigation:changed',
'page navigation event (load)',
)
expect(emit.getCall(8)).to.be.calledWith(
'navigation:changed',
'page navigation event (before:load)',
)
expect(emit.getCall(9)).to.be.calledWith(
'navigation:changed',
'page navigation event (load)',
)
expect(emit.getCall(10)).to.be.calledWithMatch(
'navigation:changed',
'hashchange',
)
expect(emit.callCount).to.eq(11)
})
})
})
it('logs url changed event', () => {
cy
.visit('/fixtures/generic.html')

View File

@@ -1319,6 +1319,61 @@ describe('network stubbing', function () {
cy.contains('#result', '""').should('be.visible')
})
// @see https://github.com/cypress-io/cypress/issues/19330
// @see https://github.com/cypress-io/cypress/issues/19344
it('load fixture as Buffer when encoding is null', function () {
// call through normally on everything
cy.spy(Cypress, 'backend')
cy.intercept('/fixtures/media/small.mp4', {
fixture: 'media/small.mp4,null',
}).as('video')
cy.visit('/fixtures/video.html')
.then(() => {
// @ts-ignore .getCall is a Sinon spy command
expect(Cypress.backend.getCall(0)).to.be.calledWithMatch(
'net',
'route:added',
{
staticResponse: {
fixture: {
filePath: 'media/small.mp4',
encoding: null,
},
},
},
)
})
})
it('load fixture with specified encoding', function () {
// call through normally on everything
cy.spy(Cypress, 'backend')
cy.intercept('non-existing-image.png', {
headers: { 'content-type': 'image/jpeg' },
fixture: 'media/cypress.png,base64',
}).as('video')
cy.visit('/fixtures/img-embed.html')
.then(() => {
// @ts-ignore .getCall is a Sinon spy command
expect(Cypress.backend.getCall(0)).to.be.calledWithMatch(
'net',
'route:added',
{
staticResponse: {
fixture: {
filePath: 'media/cypress.png',
encoding: 'base64',
},
},
},
)
})
})
// @see https://github.com/cypress-io/cypress/issues/8623
it('works with images', function () {
cy.visit('/fixtures/img-embed.html')

View File

@@ -1955,6 +1955,7 @@ space
})
})
// https://github.com/cypress-io/cypress/issues/14861
describe('ignores style and script tag in body', () => {
it('style', (done) => {
cy.on('fail', (err) => {
@@ -1977,6 +1978,35 @@ space
cy.visit('fixtures/content-in-body.html')
cy.contains('I am in the script tag in body', { timeout: 500 })
})
// https://github.com/cypress-io/cypress/issues/19377
describe('cy.contains() does not remove <style> and <script> tags', () => {
it('cy.contains() does not remove style tags from the DOM', () => {
cy.visit('fixtures/content-in-body.html')
cy.get('button#my_button_1').should('be.visible')
cy.contains('Hello').should('be.visible')
cy.get('button#my_button_1').should('be.visible')
})
it('cy.contains() does not remove script tags from the DOM', () => {
cy.visit('fixtures/content-in-body.html')
cy.window().then((win) => {
const scriptElement = win.document.getElementById('my_script')
expect(scriptElement?.id).to.equal('my_script')
})
cy.get('button#my_button_2').click()
cy.contains('This is the result').should('be.visible')
cy.window().then((win) => {
const scriptElement = win.document.getElementById('my_script')
expect(scriptElement?.id).to.equal('my_script')
})
})
})
})
describe('subject contains text nodes', () => {

View File

@@ -0,0 +1,190 @@
import { $Location } from '../../../src/cypress/location'
import $SetterGetter from '../../../src/cypress/setter_getter'
import { bothUrlsMatchAndOneHasHash, historyNavigationTriggeredHashChange } from '../../../src/cy/navigation'
describe('cy/navigation', () => {
describe('.bothUrlsMatchAndOneHasHash', () => {
describe('matches remote url', () => {
it('when remote url has hash', () => {
const current = $Location.create('https://my_url.com')
const remote = $Location.create('https://my_url.com#hash')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote)
expect(isMatch).to.be.true
})
})
describe('does not match remote url', () => {
it('when only current url has hash', () => {
const current = $Location.create('https://my_url.com#hash')
const remote = $Location.create('https://my_url.com')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote)
expect(isMatch).to.be.false
})
it('when remote url is missing hash', () => {
const current = $Location.create('https://my_url.com')
const remote = $Location.create('https://my_url.com')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote)
expect(isMatch).to.be.false
})
it('when origins are different', () => {
const current = $Location.create('https://my_url.com')
const remote = $Location.create('https://my_other_url.com#hash')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote)
expect(isMatch).to.be.false
})
it('when pathnames are different', () => {
const current = $Location.create('https://my_url.com/home')
const remote = $Location.create('https://my_url.com/random_page#hash')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote)
expect(isMatch).to.be.false
})
it('when search is different', () => {
const current = $Location.create('https://my_url.com/home?user=1')
const remote = $Location.create('https://my_url.com/home#hash?user=1')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote)
expect(isMatch).to.be.false
})
})
describe('matches least one url', () => {
it('when current url has hash', () => {
const current = $Location.create('https://my_url.com#title')
const remote = $Location.create('https://my_url.com')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.true
})
it('when remote url has hash', () => {
const current = $Location.create('https://my_url.com')
const remote = $Location.create('https://my_url.com#title')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.true
})
it('when both urls have a hash', () => {
const current = $Location.create('https://my_url.com#home')
const remote = $Location.create('https://my_url.com#title')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.true
})
})
describe('does not match either url', () => {
it('when neither url has hash', () => {
const current = $Location.create('https://my_url.com')
const remote = $Location.create('https://my_url.com')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.false
})
it('when origins are different', () => {
const current = $Location.create('https://my_url.com')
const remote = $Location.create('https://my_other_url.com#hash')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.false
})
it('when pathnames are different', () => {
const current = $Location.create('https://my_url.com/home')
const remote = $Location.create('https://my_url.com/random_page#hash')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.false
})
it('when search is different', () => {
const current = $Location.create('https://my_url.com/home?user=1')
const remote = $Location.create('https://my_url.com/home#hash?user=1')
const isMatch = bothUrlsMatchAndOneHasHash(current, remote, true)
expect(isMatch).to.be.false
})
})
})
describe('.historyNavigationTriggeredHashChange', () => {
it('when no navHistoryDelta in state', () => {
const state = $SetterGetter.create({ })
const triggeredHashChange = historyNavigationTriggeredHashChange(state)
expect(triggeredHashChange).to.be.false
})
it('when navHistoryDelta is 0', () => {
const state = $SetterGetter.create({
navHistoryDelta: 0,
})
const triggeredHashChange = historyNavigationTriggeredHashChange(state)
expect(triggeredHashChange).to.be.false
})
it('when navHistoryDelta is null', () => {
const state = $SetterGetter.create({
navHistoryDelta: null,
})
const triggeredHashChange = historyNavigationTriggeredHashChange(state)
expect(triggeredHashChange).to.be.false
})
it('when neither url has a hash', () => {
const state = $SetterGetter.create({
navHistoryDelta: 1,
urlPosition: 0,
url: 'https://my_url.com/',
urls: ['https://my_url.com/', 'https://my_url.com/home'],
})
const triggeredHashChange = historyNavigationTriggeredHashChange(state)
expect(triggeredHashChange).to.be.false
})
it('when one url has a hash and navigation moves forward in history', () => {
const state = $SetterGetter.create({
navHistoryDelta: 1,
urlPosition: 0,
url: 'https://my_url.com/home',
urls: ['https://my_url.com/home', 'https://my_url.com/home#hash'],
})
const triggeredHashChange = historyNavigationTriggeredHashChange(state)
expect(triggeredHashChange).to.be.true
expect(state('navHistoryDelta')).to.eq(1)
})
it('when one url has a hash and navigation moves back in history', () => {
const state = $SetterGetter.create({
navHistoryDelta: -1,
urlPosition: 1,
url: 'https://my_url.com/home#hash',
urls: ['https://my_url.com/home', 'https://my_url.com/home#hash'],
})
const triggeredHashChange = historyNavigationTriggeredHashChange(state)
expect(triggeredHashChange).to.be.true
expect(state('navHistoryDelta')).to.eq(-1)
})
})
})

View File

@@ -0,0 +1,78 @@
const {
getBackendStaticResponse,
} = require('../../../../src/cy/net-stubbing/static-response-utils')
describe('driver/src/cy/net-stubbing/static-response-utils', () => {
describe('.getBackendStaticResponse', () => {
describe('delay', () => {
it('does not set delay when delayMS is not provided', () => {
const staticResponse = getBackendStaticResponse({})
expect(staticResponse).to.not.have.property('delay')
})
it('sets delay', () => {
const staticResponse = getBackendStaticResponse({ delayMs: 200 })
expect(staticResponse).to.have.property('delay', 200)
})
})
describe('fixtures', () => {
it('does not set fixtures data when fixtures string is not provided', () => {
const staticResponse = getBackendStaticResponse({})
expect(staticResponse).to.not.have.property('fixtures')
})
it('parses fixture string without file encoding', () => {
const staticResponse = getBackendStaticResponse({ fixture: 'file.html' })
expect(staticResponse.fixture).to.deep.equal({
filePath: 'file.html',
encoding: undefined,
})
})
it('parses fixture string with file encoding', () => {
const staticResponse = getBackendStaticResponse({ fixture: 'file.html,utf8' })
expect(staticResponse.fixture).to.deep.equal({
filePath: 'file.html',
encoding: 'utf8',
})
})
it('parses fixture string with file encoding set as null to indicate Buffer', () => {
const staticResponse = getBackendStaticResponse({ fixture: 'file.html,null' })
expect(staticResponse.fixture).to.deep.equal({
filePath: 'file.html',
encoding: null,
})
})
})
describe('body', () => {
it('does not set body when body is undefined', () => {
const staticResponse = getBackendStaticResponse({})
expect(staticResponse).to.not.have.property('body')
})
it('sets body when body is provided as a string', () => {
const staticResponse = getBackendStaticResponse({ body: 'body' })
expect(staticResponse.body).to.eq('body')
})
it('sets body when body is provided as a ArrayBuffer', () => {
const buffer = new ArrayBuffer(8)
const staticResponse = getBackendStaticResponse({ body: buffer })
expect(staticResponse.body).to.be.a('arraybuffer')
expect(staticResponse.body).to.eq(buffer)
})
})
})
})

View File

@@ -0,0 +1,62 @@
const { Screenshot } = Cypress
let failedEventFired = false
Cypress.on('fail', (error) => {
failedEventFired = true
throw new Error(error)
})
let screenshotTaken = false
Screenshot.defaults({ onAfterScreenshot: () => {
screenshotTaken = true
} })
const pendingTests = []
const passedTests = []
Cypress.on('test:after:run', (test) => {
if (test.state === 'pending') {
return pendingTests.push(test)
}
if (test.state === 'passed') {
return passedTests.push(test)
}
})
beforeEach(() => {
// Set isInteractive to false to ensure that screenshots will be
// triggered in both run and open mode
Cypress.config('isInteractive', false)
})
describe('skipped test', () => {
it('does not fail', function () {
cy.then(() => {
this.skip()
}).then(() => {
expect(true).to.be.false
})
})
it('does not prevent subsequent tests from running', () => {
expect(true).to.be.true
})
})
describe('skipped test side effects', () => {
it('does not have a screenshot taken', () => {
expect(screenshotTaken).to.be.false
})
it('does not fire failed event', () => {
expect(failedEventFired).to.be.false
})
it('does still mark all tests with the correct state', () => {
expect(pendingTests).to.have.length(1)
expect(passedTests).to.have.length(3)
})
})

View File

@@ -0,0 +1,70 @@
const { Screenshot } = Cypress
let failedEventFired = false
Cypress.on('fail', (error) => {
failedEventFired = true
throw new Error(error)
})
let screenshotTaken = false
Screenshot.defaults({ onAfterScreenshot: () => {
screenshotTaken = true
} })
const pendingTests = []
const passedTests = []
Cypress.on('test:after:run', (test) => {
if (test.state === 'pending') {
return pendingTests.push(test)
}
if (test.state === 'passed') {
return passedTests.push(test)
}
})
beforeEach(() => {
// Set isInteractive to false to ensure that screenshots will be
// triggered in both run and open mode
Cypress.config('isInteractive', false)
})
describe('generally skipped test', () => {
before(function () {
this.skip()
})
it('does not fail', function () {
expect(true).to.be.false
})
})
describe('individually skipped tests', () => {
it('does not fail when using this.skip', function () {
this.skip()
expect(true).to.be.false
})
// NOTE: We are skipping this test in order to test skip functionality
it.skip('does not fail when using it.skip', () => {
expect(true).to.be.false
})
})
describe('skipped test side effects', () => {
it('does not have a screenshot taken', () => {
expect(screenshotTaken).to.be.false
})
it('does not fire failed event', () => {
expect(failedEventFired).to.be.false
})
it('does still mark all tests with the correct state', () => {
expect(pendingTests).to.have.length(3)
expect(passedTests).to.have.length(2)
})
})

View File

@@ -377,6 +377,28 @@ describe('testConfigOverrides baseUrl @slow', () => {
})
})
describe('cannot set read-only properties', () => {
afterEach(() => {
window.top.__cySkipValidateConfig = true
})
it('throws if mutating read-only config with Cypress.config()', (done) => {
window.top.__cySkipValidateConfig = false
cy.on('fail', (err) => {
expect(err.message).to.include('`Cypress.config()` cannot mutate option `chromeWebSecurity` because it is a read-only property.')
done()
})
Cypress.config('chromeWebSecurity', false)
})
it('does not throw for non-Cypress config values', () => {
expect(() => {
Cypress.config('foo', 'bar')
}).to.not.throw()
})
})
function hasOnly (test) {
let curSuite = test.parent
let hasOnly = false

View File

@@ -0,0 +1,16 @@
describe('issue 17512', () => {
beforeEach(() => {
cy.visit('fixtures/issue-17512.html')
})
it('returns null when target is not defined', () => {
cy.get('#link').click()
cy.get('#result').should('have.text', 'null')
cy.get('#link2').click()
cy.get('#result2').should('have.text', '"_top"')
cy.get('#link3').click()
cy.get('#result3').should('have.text', 'null')
})
})

View File

@@ -2,6 +2,8 @@ const { $ } = Cypress
const isActuallyInteractive = Cypress.config('isInteractive')
window.top.__cySkipValidateConfig = true
if (!isActuallyInteractive) {
// we want to only enable retries in runMode
// and because we set `isInteractive` above

View File

@@ -64,6 +64,17 @@ index 57a2761..0000000
@@ -1 +0,0 @@
-{"version":3,"file":"unfetch.umd.js","sources":["../src/index.mjs"],"sourcesContent":["export default function(url, options) {\n\toptions = options || {};\n\treturn new Promise( (resolve, reject) => {\n\t\tconst request = new XMLHttpRequest();\n\t\tconst keys = [];\n\t\tconst all = [];\n\t\tconst headers = {};\n\n\t\tconst response = () => ({\n\t\t\tok: (request.status/100|0) == 2,\t\t// 200-299\n\t\t\tstatusText: request.statusText,\n\t\t\tstatus: request.status,\n\t\t\turl: request.responseURL,\n\t\t\ttext: () => Promise.resolve(request.responseText),\n\t\t\tjson: () => Promise.resolve(JSON.parse(request.responseText)),\n\t\t\tblob: () => Promise.resolve(new Blob([request.response])),\n\t\t\tclone: response,\n\t\t\theaders: {\n\t\t\t\tkeys: () => keys,\n\t\t\t\tentries: () => all,\n\t\t\t\tget: n => headers[n.toLowerCase()],\n\t\t\t\thas: n => n.toLowerCase() in headers\n\t\t\t}\n\t\t});\n\n\t\trequest.open(options.method || 'get', url, true);\n\n\t\trequest.onload = () => {\n\t\t\trequest.getAllResponseHeaders().replace(/^(.*?):[^\\S\\n]*([\\s\\S]*?)$/gm, (m, key, value) => {\n\t\t\t\tkeys.push(key = key.toLowerCase());\n\t\t\t\tall.push([key, value]);\n\t\t\t\theaders[key] = headers[key] ? `${headers[key]},${value}` : value;\n\t\t\t});\n\t\t\tresolve(response());\n\t\t};\n\n\t\trequest.onerror = reject;\n\n\t\trequest.withCredentials = options.credentials=='include';\n\n\t\tfor (const i in options.headers) {\n\t\t\trequest.setRequestHeader(i, options.headers[i]);\n\t\t}\n\n\t\trequest.send(options.body || null);\n\t});\n}\n"],"names":["url","options","Promise","resolve","reject","request","XMLHttpRequest","keys","all","headers","response","ok","status","statusText","responseURL","text","responseText","json","JSON","parse","blob","Blob","clone","entries","get","n","toLowerCase","has","const","i","open","method","onload","getAllResponseHeaders","replace","m","key","value","push","onerror","withCredentials","credentials","setRequestHeader","send","body"],"mappings":"6KAAe,SAASA,EAAKC,UAC5BA,EAAUA,GAAW,GACd,IAAIC,iBAAUC,EAASC,OACvBC,EAAU,IAAIC,eACdC,EAAO,GACPC,EAAM,GACNC,EAAU,GAEVC,oBACLC,GAA8B,IAAzBN,EAAQO,OAAO,IAAI,GACxBC,WAAYR,EAAQQ,WACpBD,OAAQP,EAAQO,OAChBZ,IAAKK,EAAQS,YACbC,uBAAYb,QAAQC,QAAQE,EAAQW,eACpCC,uBAAYf,QAAQC,QAAQe,KAAKC,MAAMd,EAAQW,gBAC/CI,uBAAYlB,QAAQC,QAAQ,IAAIkB,KAAK,CAAChB,EAAQK,aAC9CY,MAAOZ,EACPD,QAAS,CACRF,uBAAYA,GACZgB,0BAAef,GACfgB,aAAKC,UAAKhB,EAAQgB,EAAEC,gBACpBC,aAAKF,UAAKA,EAAEC,gBAAiBjB,UAmB1BmB,IAAMC,KAfXxB,EAAQyB,KAAK7B,EAAQ8B,QAAU,MAAO/B,GAAK,GAE3CK,EAAQ2B,kBACP3B,EAAQ4B,wBAAwBC,QAAQ,wCAAiCC,EAAGC,EAAKC,GAChF9B,EAAK+B,KAAKF,EAAMA,EAAIV,eACpBlB,EAAI8B,KAAK,CAACF,EAAKC,IACf5B,EAAQ2B,GAAO3B,EAAQ2B,GAAU3B,EAAQ2B,OAAQC,EAAUA,IAE5DlC,EAAQO,MAGTL,EAAQkC,QAAUnC,EAElBC,EAAQmC,gBAAuC,WAArBvC,EAAQwC,YAElBxC,EAAQQ,QACvBJ,EAAQqC,iBAAiBb,EAAG5B,EAAQQ,QAAQoB,IAG7CxB,EAAQsC,KAAK1C,EAAQ2C,MAAQ"}
\ No newline at end of file
diff --git a/node_modules/unfetch/src/index.d.ts b/node_modules/unfetch/src/index.d.ts
index 0c53ad9..8dd2d54 100644
--- a/node_modules/unfetch/src/index.d.ts
+++ b/node_modules/unfetch/src/index.d.ts
@@ -14,4 +14,6 @@ declare namespace unfetch {
declare const unfetch: typeof fetch;
+export function registerFetch(window: Window): unfetch;
+
export default unfetch;
diff --git a/node_modules/unfetch/src/index.mjs b/node_modules/unfetch/src/index.mjs
deleted file mode 100644
index 783ad42..0000000

View File

@@ -1,5 +1,3 @@
// @ts-nocheck
import _ from 'lodash'
import Promise from 'bluebird'
@@ -54,12 +52,18 @@ const isDomSubjectAndMatchesValue = (value, subject) => {
return false
}
type Parsed = {
subject?: JQuery<any>
actual?: any
expected?: any
}
// Rules:
// 1. always remove value
// 2. if value is a jquery object set a subject
// 3. if actual is undefined or its not expected remove both actual + expected
const parseValueActualAndExpected = (value, actual, expected) => {
const obj = { actual, expected }
const obj: Parsed = { actual, expected }
if ($dom.isJquery(value)) {
obj.subject = value
@@ -77,7 +81,7 @@ export const create = (Cypress, cy) => {
const getUpcomingAssertions = () => {
const index = cy.state('index') + 1
const assertions = []
const assertions: any[] = []
// grab the rest of the queue'd commands
for (let cmd of cy.queue.slice(index)) {
@@ -137,15 +141,29 @@ export const create = (Cypress, cy) => {
message = message.replace(stackTracesRe, '\n')
}
let obj = parseValueActualAndExpected(value, actual, expected)
let parsed = parseValueActualAndExpected(value, actual, expected)
// TODO: make it more specific after defining the type for Cypress.log().
let obj: Record<string, any> = {
...parsed,
}
if ($dom.isElement(value)) {
obj.$el = $dom.wrap(value)
}
// if we are simply verifying the upcoming
// assertions then do not immediately end or snapshot
// else do
// `verifying` represents whether we're deciding whether or not to resolve
// a command (true) or of we're actually performing a user-facing assertion
// (false).
// If we're verifying upcoming assertions (implicit or explicit),
// then we don't need to take a DOM snapshot - one will be taken later when
// retries time out or the command otherwise entirely fails or passes.
// We save the error on _error because we may use it to construct the
// timeout error which we eventually do display to the user.
// If we're actually performing an assertion which will be displayed to the
// user though, then we want to take a DOM snapshot and display this error
// (if any) in the log message on screen.
if (verifying) {
obj._error = error
} else {
@@ -216,10 +234,18 @@ export const create = (Cypress, cy) => {
})
}
type VerifyUpcomingAssertionsCallbacks = {
ensureExistenceFor?: 'subject' | 'dom' | boolean
onPass?: Function
onFail?: (err?, isDefaultAssertionErr?: boolean, cmds?: any[]) => void
onRetry?: () => any
}
return {
finishAssertions,
verifyUpcomingAssertions (subject, options = {}, callbacks = {}) {
// TODO: define the specific type of options
verifyUpcomingAssertions (subject, options: Record<string, any> = {}, callbacks: VerifyUpcomingAssertionsCallbacks = {}) {
const cmds = getUpcomingAssertions()
cy.state('upcomingAssertions', cmds)
@@ -267,6 +293,7 @@ export const create = (Cypress, cy) => {
}
const onPassFn = () => {
cy.state('overrideAssert', undefined)
if (_.isFunction(callbacks.onPass)) {
return callbacks.onPass.call(this, cmds, options.assertions)
}
@@ -289,6 +316,7 @@ export const create = (Cypress, cy) => {
err = e2
}
cy.state('overrideAssert', undefined)
err.isDefaultAssertionErr = isDefaultAssertionErr
options.error = err
@@ -324,6 +352,24 @@ export const create = (Cypress, cy) => {
// bail if we have no assertions and apply
// the default assertions if applicable
if (!cmds.length) {
// In general in cypress, when assertions fail we want to take a DOM
// snapshot to display to the user. In this case though, when we invoke
// ensureExistence, we're not going to display the error (if there is
// one) to the user - we're only deciding whether to resolve this current
// command (assertions pass) or fail (and probably retry). A DOM snapshot
// isn't necessary in either case - one will be taken later as part of the
// command (if they pass) or when we time out retrying.
// Chai assertions have a signature of (passed, message, value, actual,
// expected, error). Our assertFn, defined earlier in the file, adds
// on a 7th arg, "verifying", which defaults to false. We here override
// the assert function with our own, which just invokes the old one
// with verifying = true. This override is cleaned up immediately
// afterwards, in either onPassFn or onFailFn.
cy.state('overrideAssert', function (...args) {
return assertFn.apply(this, args.concat(true) as any)
})
return Promise
.try(ensureExistence)
.then(onPassFn)
@@ -433,12 +479,13 @@ export const create = (Cypress, cy) => {
cy.state('onBeforeLog', setCommandLog)
// send verify=true as the last arg
return assertFn.apply(this, args.concat(true))
return assertFn.apply(this, args.concat(true) as any)
}
const fns = injectAssertionFns(cmds)
const subjects = []
// TODO: remove any when the type of subject, the first argument of this function is specified.
const subjects: any[] = []
// iterate through each subject
// and force the assertion to return
@@ -477,8 +524,6 @@ export const create = (Cypress, cy) => {
return cy.state('overrideAssert', undefined)
}
// store this in case our test ends early
// and we reset between tests
cy.state('overrideAssert', overrideAssert)
return Promise

View File

@@ -1,4 +1,3 @@
// @ts-nocheck
/* eslint-disable prefer-rest-params */
// tests in driver/cypress/integration/commands/assertions_spec.js
@@ -31,20 +30,6 @@ const whitespace = /\s/g
const valueHasLeadingOrTrailingWhitespaces = /\*\*'\s+|\s+'\*\*/g
const imageMarkdown = /!\[.*?\]\(.*?\)/g
let assertProto = null
let matchProto = null
let lengthProto = null
let containProto = null
let existProto = null
let getMessage = null
let chaiUtils = null
let replaceArgMessages = null
let removeOrKeepSingleQuotesBetweenStars = null
let setSpecWindowGlobals = null
let restoreAsserts = null
let overrideExpect = null
let overrideChaiAsserts = null
type CreateFunc = ((specWindow, state, assertFn) => ({
chai: Chai.ChaiStatic
expect: (val: any, message?: string) => Chai.Assertion
@@ -55,7 +40,7 @@ export let create: CreateFunc | null = null
chai.use(sinonChai)
chai.use((chai, u) => {
chaiUtils = u
const chaiUtils = u
$chaiJquery(chai, chaiUtils, {
onInvalid (method, obj) {
@@ -87,14 +72,14 @@ chai.use((chai, u) => {
},
})
assertProto = chai.Assertion.prototype.assert
matchProto = chai.Assertion.prototype.match
lengthProto = chai.Assertion.prototype.__methods.length.method
containProto = chai.Assertion.prototype.__methods.contain.method
existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist').get
const { objDisplay } = chai.util;
const assertProto = chai.Assertion.prototype.assert
const matchProto = (chai.Assertion.prototype as any).match
const lengthProto = (chai.Assertion.prototype as any).__methods.length.method
const containProto = (chai.Assertion.prototype as any).__methods.contain.method
const existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist')!.get
const { objDisplay } = chai.util
({ getMessage } = chai.util)
const getMessage = chai.util.getMessage
const _inspect = chai.util.inspect
const { inspect, setFormatValueHook } = chaiInspect.create(chai)
@@ -108,17 +93,19 @@ chai.use((chai, u) => {
try {
val && val.document
val && val.inspect
} catch (e) {
} catch (e: any) {
if (e.stack.indexOf('cross-origin') !== -1 || // chrome
e.message.indexOf('cross-origin') !== -1) { // firefox
return `[window]`
}
}
return
})
// remove any single quotes between our **,
// except escaped quotes, empty strings and number strings.
removeOrKeepSingleQuotesBetweenStars = (message) => {
const removeOrKeepSingleQuotesBetweenStars = (message) => {
// remove any single quotes between our **, preserving escaped quotes
// and if an empty string, put the quotes back
return message.replace(allBetweenFourStars, (match) => {
@@ -149,8 +136,8 @@ chai.use((chai, u) => {
return message.replace(imageMarkdown, '``$&``')
}
replaceArgMessages = (args, str) => {
return _.reduce(args, (memo, value, index) => {
const replaceArgMessages = (args, str) => {
return _.reduce(args, (memo: string[], value, index) => {
if (_.isString(value)) {
value = value
.replace(allWordsBetweenCurlyBraces, '**$1**')
@@ -166,18 +153,17 @@ chai.use((chai, u) => {
}
return memo
}
, [])
}, [])
}
restoreAsserts = function () {
const restoreAsserts = function () {
chai.util.inspect = _inspect
chai.util.getMessage = getMessage
chai.util.objDisplay = objDisplay
chai.Assertion.prototype.assert = assertProto
chai.Assertion.prototype.match = matchProto
chai.Assertion.prototype.__methods.length.method = lengthProto
chai.Assertion.prototype.__methods.contain.method = containProto
chai.Assertion.prototype.assert = assertProto;
(chai.Assertion.prototype as any).match = matchProto;
(chai.Assertion.prototype as any).__methods.length.method = lengthProto;
(chai.Assertion.prototype as any).__methods.contain.method = containProto
return Object.defineProperty(chai.Assertion.prototype, 'exist', { get: existProto })
}
@@ -218,7 +204,7 @@ chai.use((chai, u) => {
}
}
overrideChaiAsserts = function (specWindow, state, assertFn) {
const overrideChaiAsserts = function (specWindow, state, assertFn) {
chai.Assertion.prototype.assert = createPatchedAssert(specWindow, state, assertFn)
const _origGetmessage = function (obj, args) {
@@ -248,6 +234,9 @@ chai.use((chai, u) => {
return (flagMsg ? `${flagMsg}: ${msg}` : msg)
}
// There are 2 types of getMessage. And we're overriding the second one.
// But TypeScript wants us to do both. So we're ignoring this.
// @ts-ignore
chaiUtils.getMessage = function (assert, args) {
const obj = assert._obj
@@ -307,13 +296,16 @@ chai.use((chai, u) => {
})
}
const containFn2 = (_super) => {
const makeMethodChainable = (_super) => {
return (function () {
return _super.apply(this, arguments)
})
}
chai.Assertion.overwriteChainableMethod('contain', containFn1, containFn2)
// `makeMethodChainable` doesn't match any type definition,
// but it is necessary to make the method chainable.
// @ts-ignore
chai.Assertion.overwriteChainableMethod('contain', containFn1, makeMethodChainable)
chai.Assertion.overwriteChainableMethod('length',
(_super) => {
@@ -346,7 +338,7 @@ chai.use((chai, u) => {
length,
obj.length,
)
} catch (e1) {
} catch (e1: any) {
e1.node = node
e1.negated = chaiUtils.flag(this, 'negate')
e1.type = 'length'
@@ -377,21 +369,21 @@ chai.use((chai, u) => {
}
})
},
// `makeMethodChainable` doesn't match any type definition,
// but it is necessary to make the method chainable.
// @ts-ignore
makeMethodChainable)
(_super) => {
return (function () {
return _super.apply(this, arguments)
})
})
return chai.Assertion.overwriteProperty('exist', (_super) => {
// _super is not documented.
// @ts-ignore
chai.Assertion.overwriteProperty('exist', (_super) => {
return (function () {
const obj = this._obj
if (!($dom.isJquery(obj) || $dom.isElement(obj))) {
try {
return _super.apply(this, arguments)
} catch (e) {
} catch (e: any) {
e.type = 'existence'
throw e
}
@@ -412,7 +404,7 @@ chai.use((chai, u) => {
node,
node,
)
} catch (e1) {
} catch (e1: any) {
e1.node = node
e1.negated = chaiUtils.flag(this, 'negate')
e1.type = 'existence'
@@ -456,20 +448,20 @@ chai.use((chai, u) => {
const createPatchedAssert = (specWindow, state, assertFn) => {
return (function (...args) {
let err
const passed = chaiUtils.test(this, args)
const passed = chaiUtils.test(this, args as Chai.AssertionArgs)
const value = chaiUtils.flag(this, 'object')
const expected = args[3]
const customArgs = replaceArgMessages(args, this._obj)
let message = chaiUtils.getMessage(this, customArgs)
const actual = chaiUtils.getActual(this, customArgs)
let message = chaiUtils.getMessage(this, customArgs as Chai.AssertionArgs)
const actual = chaiUtils.getActual(this, customArgs as Chai.AssertionArgs)
message = removeOrKeepSingleQuotesBetweenStars(message)
message = escapeMarkdown(message)
try {
assertProto.apply(this, args)
assertProto.apply(this, args as Chai.AssertionArgs)
} catch (e) {
err = e
}
@@ -489,7 +481,7 @@ chai.use((chai, u) => {
})
}
overrideExpect = (specWindow, state) => {
const overrideExpect = (specWindow, state) => {
// only override assertions for this specific
// expect function instance so we do not affect
// the outside world
@@ -523,7 +515,7 @@ chai.use((chai, u) => {
return fn
}
setSpecWindowGlobals = function (specWindow, state) {
const setSpecWindowGlobals = function (specWindow, state) {
const expect = overrideExpect(specWindow, state)
const assert = overrideAssert(specWindow, state)
@@ -552,12 +544,3 @@ chai.use((chai, u) => {
export interface IChai {
expect: ReturnType<CreateFunc>['expect']
}
export default {
replaceArgMessages,
removeOrKeepSingleQuotesBetweenStars,
setSpecWindowGlobals,
restoreAsserts,
overrideExpect,
overrideChaiAsserts,
}

View File

@@ -8,6 +8,7 @@ import Promise from 'bluebird'
import $utils from '../../cypress/utils'
import $errUtils from '../../cypress/error_utils'
import $Log from '../../cypress/log'
import { bothUrlsMatchAndOneHasHash } from '../navigation'
import { $Location } from '../../cypress/location'
import debugFn from 'debug'
@@ -60,19 +61,6 @@ const timedOutWaitingForPageLoad = (ms, log) => {
})
}
const bothUrlsMatchAndRemoteHasHash = (current, remote) => {
// the remote has a hash
// or the last char of href
// is a hash
return (remote.hash || remote.href.slice(-1) === '#') &&
// both must have the same origin
current.origin === remote.origin &&
// both must have the same pathname
current.pathname === remote.pathname &&
// both must have the same query params
current.search === remote.search
}
const cannotVisitDifferentOrigin = (origin, previousUrlVisited, remoteUrl, existingUrl, log) => {
const differences = []
@@ -129,13 +117,18 @@ const navigationChanged = (Cypress, cy, state, source, arg) => {
}
// start storing the history entries
const urls = state('urls') || []
let urls = state('urls') || []
let urlPosition = state('urlPosition')
const previousUrl = _.last(urls)
if (urlPosition === undefined) {
urlPosition = -1
}
// ensure our new url doesnt match whatever
const previousUrl = urls[urlPosition]
// ensure our new url doesn't match whatever
// the previous was. this prevents logging
// additionally when the url didnt actually change
// additionally when the url didn't actually change
if (url === previousUrl) {
return
}
@@ -143,11 +136,23 @@ const navigationChanged = (Cypress, cy, state, source, arg) => {
// else notify the world and log this event
Cypress.action('cy:url:changed', url)
urls.push(url)
const navHistoryDelta = state('navHistoryDelta')
// if navigation was changed via a manipulation of the browser session we
// need to update the urlPosition to match the position of the history stack
// and we do not need to push a new url onto the urls state
if (navHistoryDelta) {
urlPosition = urlPosition + navHistoryDelta
state('navHistoryDelta', undefined)
} else {
urls = urls.slice(0, urlPosition + 1)
urls.push(url)
urlPosition = urlPosition + 1
}
state('urls', urls)
state('url', url)
state('urlPosition', urlPosition)
// don't output a command log for 'load' or 'before:load' events
// return if source in command
@@ -586,10 +591,8 @@ export default (Commands, Cypress, cy, state, config) => {
})
},
go (numberOrString, options = {}) {
const userOptions = options
options = _.defaults({}, userOptions, {
go (numberOrString, userOptions = {}) {
const options = _.defaults({}, userOptions, {
log: true,
timeout: config('pageLoadTimeout'),
})
@@ -931,7 +934,7 @@ export default (Commands, Cypress, cy, state, config) => {
// the browser won't actually make a new http request
// for this, and so we need to resolve onLoad immediately
// and bypass the actual visit resolution stuff
if (bothUrlsMatchAndRemoteHasHash(current, remote)) {
if (bothUrlsMatchAndOneHasHash(current, remote)) {
// https://github.com/cypress-io/cypress/issues/1311
if (current.hash === remote.hash) {
consoleProps['Note'] = 'Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire.'

View File

@@ -114,6 +114,13 @@ export type KeyEventType =
| 'textInput'
| 'beforeinput'
export type ModifiersEventOptions = {
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
}
const toModifiersEventOptions = (modifiers: KeyboardModifiers) => {
return {
altKey: modifiers.alt,

View File

@@ -4,6 +4,7 @@ import _ from 'lodash'
import { handleInvalidEventTarget, handleInvalidAnchorTarget } from './top_attr_guards'
const HISTORY_ATTRS = 'pushState replaceState'.split(' ')
const HISTORY_NAV_ATTRS = 'go back forward'.split(' ')
let events = []
let listenersAdded = null
@@ -81,6 +82,20 @@ export default {
callbacks.onNavigation('hashchange', e)
})
for (let attr of HISTORY_NAV_ATTRS) {
const orig = contentWindow.history?.[attr]
if (!orig) {
continue
}
contentWindow.history[attr] = function (delta) {
callbacks.onHistoryNav(attr === 'back' ? -1 : (attr === 'forward' ? 1 : delta))
orig.apply(this, [delta])
}
}
for (let attr of HISTORY_ATTRS) {
const orig = contentWindow.history?.[attr]

View File

@@ -1,9 +1,8 @@
// @ts-nocheck
import $ from 'jquery'
import _ from 'lodash'
import $dom from '../dom'
import $elements from '../dom/elements'
import $Keyboard from './keyboard'
import $Keyboard, { ModifiersEventOptions } from './keyboard'
import $selection from '../dom/selection'
import debugFn from 'debug'
@@ -16,7 +15,7 @@ const debug = debugFn('cypress:driver:mouse')
* @property {Document} doc
*/
const getLastHoveredEl = (state) => {
const getLastHoveredEl = (state): HTMLElement | null => {
let lastHoveredEl = state('mouseLastHoveredEl')
const lastHoveredElAttached = lastHoveredEl && $elements.isAttachedEl(lastHoveredEl)
@@ -42,6 +41,12 @@ const getMouseCoords = (state) => {
return state('mouseCoords')
}
type DefaultMouseOptions = ModifiersEventOptions & CoordsEventOptions & {
view: Window
composed: boolean
relatedTarget: HTMLElement | null
}
export const create = (state, keyboard, focused, Cypress) => {
const isFirefox = Cypress.browser.family === 'firefox'
@@ -65,7 +70,7 @@ export const create = (state, keyboard, focused, Cypress) => {
return sendPointerEvent(el, evtOptions, 'pointerup', true, true)
}
const sendPointerdown = (el, evtOptions) => {
const sendPointerdown = (el, evtOptions): {} | SentEvent => {
if (isFirefox && el.disabled) {
return {}
}
@@ -75,7 +80,7 @@ export const create = (state, keyboard, focused, Cypress) => {
const sendPointermove = (el, evtOptions) => {
return sendPointerEvent(el, evtOptions, 'pointermove', true, true)
}
const sendPointerover = (el, evtOptions) => {
const sendPointerover = (el, evtOptions: DefaultMouseOptions) => {
return sendPointerEvent(el, evtOptions, 'pointerover', true, true)
}
const sendPointerenter = (el, evtOptions) => {
@@ -95,7 +100,7 @@ export const create = (state, keyboard, focused, Cypress) => {
return sendMouseEvent(el, evtOptions, 'mouseup', true, true)
}
const sendMousedown = (el, evtOptions) => {
const sendMousedown = (el, evtOptions): {} | SentEvent => {
if (isFirefox && el.disabled) {
return {}
}
@@ -105,7 +110,7 @@ export const create = (state, keyboard, focused, Cypress) => {
const sendMousemove = (el, evtOptions) => {
return sendMouseEvent(el, evtOptions, 'mousemove', true, true)
}
const sendMouseover = (el, evtOptions) => {
const sendMouseover = (el, evtOptions: DefaultMouseOptions) => {
return sendMouseEvent(el, evtOptions, 'mouseover', true, true)
}
const sendMouseenter = (el, evtOptions) => {
@@ -117,7 +122,7 @@ export const create = (state, keyboard, focused, Cypress) => {
const sendMouseout = (el, evtOptions) => {
return sendMouseEvent(el, evtOptions, 'mouseout', true, true)
}
const sendClick = (el, evtOptions, opts = {}) => {
const sendClick = (el, evtOptions, opts: { force?: boolean } = {}) => {
// send the click event if firefox and force (needed for force check checkbox)
if (!opts.force && isFirefox && el.disabled) {
return {}
@@ -154,7 +159,7 @@ export const create = (state, keyboard, focused, Cypress) => {
return !_.isEqual(xy(fromElViewport), xy(coords))
}
const shouldMoveCursorToEndAfterMousedown = (el) => {
const shouldMoveCursorToEndAfterMousedown = (el: HTMLElement) => {
const _debug = debug.extend(':shouldMoveCursorToEnd')
_debug('shouldMoveToEnd?', el)
@@ -182,7 +187,7 @@ export const create = (state, keyboard, focused, Cypress) => {
}
const mouse = {
_getDefaultMouseOptions (x, y, win) {
_getDefaultMouseOptions (x, y, win): DefaultMouseOptions {
const _activeModifiers = keyboard.getActiveModifiers()
const modifiersEventOptions = $Keyboard.toModifiersEventOptions(_activeModifiers)
const coordsEventOptions = toCoordsEventOptions(x, y, win)
@@ -201,7 +206,7 @@ export const create = (state, keyboard, focused, Cypress) => {
* @param {Coords} coords
* @param {HTMLElement} forceEl
*/
move (fromElViewport, forceEl) {
move (fromElViewport, forceEl?) {
debug('mouse.move', fromElViewport)
const lastHoveredEl = getLastHoveredEl(state)
@@ -259,16 +264,21 @@ export const create = (state, keyboard, focused, Cypress) => {
skipped: formatReasonNotFired('Already on Coordinates'),
}
}
type EventFunc =
| (() => { skipped: string })
| (() => SentEvent)
let pointerout = _.noop
let pointerleave = _.noop
let pointerover = notFired
let pointerover: EventFunc = notFired
let pointerenter = _.noop
let mouseout = _.noop
let mouseleave = _.noop
let mouseover = notFired
let mouseover: EventFunc = notFired
let mouseenter = _.noop
let pointermove = notFired
let mousemove = notFired
let pointermove: EventFunc = notFired
let mousemove: EventFunc = notFired
const lastHoveredEl = getLastHoveredEl(state)
@@ -285,9 +295,9 @@ export const create = (state, keyboard, focused, Cypress) => {
sendMouseout(lastHoveredEl, _.extend({}, defaultMouseOptions, { relatedTarget: el }))
}
let curParent = lastHoveredEl
let curParent: Node | null = lastHoveredEl
const elsToSendMouseleave = []
const elsToSendMouseleave: Node[] = []
while (curParent && curParent.ownerDocument && curParent !== commonAncestor) {
elsToSendMouseleave.push(curParent)
@@ -317,8 +327,8 @@ export const create = (state, keyboard, focused, Cypress) => {
return sendPointerover(el, _.extend({}, defaultPointerOptions, { relatedTarget: lastHoveredEl }))
}
let curParent = el
const elsToSendMouseenter = []
let curParent: Node | null = el
const elsToSendMouseenter: Node[] = []
while (curParent && curParent.ownerDocument && curParent !== commonAncestor) {
elsToSendMouseenter.push(curParent)
@@ -349,7 +359,8 @@ export const create = (state, keyboard, focused, Cypress) => {
return sendMousemove(el, defaultMouseOptions)
}
const events = []
// TODO: make `type` below more specific.
const events: Array<ReturnType<EventFunc> & { type: string }> = []
pointerout()
pointerleave()
@@ -419,7 +430,7 @@ export const create = (state, keyboard, focused, Cypress) => {
let pointerdown = sendPointerdown(
el,
pointerEvtOptions,
)
) as Partial<SentEvent>
const pointerdownPrevented = pointerdown.preventedDefault
const elIsDetached = $elements.isDetachedEl(el)
@@ -461,7 +472,7 @@ export const create = (state, keyboard, focused, Cypress) => {
// el we just send pointerdown
const el = mouseDownPhase.targetEl
if (mouseDownPhase.events.pointerdown.preventedDefault || mouseDownPhase.events.mousedown.preventedDefault || !$elements.isAttachedEl(el)) {
if (mouseDownPhase.events.pointerdown.preventedDefault || (mouseDownPhase.events.mousedown as Partial<SentEvent>).preventedDefault || !$elements.isAttachedEl(el)) {
return mouseDownPhase
}
@@ -488,6 +499,8 @@ export const create = (state, keyboard, focused, Cypress) => {
if (shouldMoveCursorToEndAfterMousedown(el)) {
debug('moveSelectionToEnd due to click', el)
// It's a curried function, so the 2 arguments are valid.
// @ts-ignore
$selection.moveSelectionToEnd(el, { onlyIfEmptySelection: true })
}
@@ -532,7 +545,7 @@ export const create = (state, keyboard, focused, Cypress) => {
const mouseDownPhase = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
const skipMouseupEvent = mouseDownPhase.events.pointerdown.preventedDefault
const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
@@ -638,7 +651,7 @@ export const create = (state, keyboard, focused, Cypress) => {
return { click }
},
_contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend) {
_contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend?) {
const el = forceEl || mouse.moveToCoords(fromElViewport)
const win = $dom.getWindowByElement(el)
@@ -695,7 +708,7 @@ export const create = (state, keyboard, focused, Cypress) => {
const contextmenuEvent = mouse._contextmenuEvent(fromElViewport, forceEl)
const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
const skipMouseupEvent = mouseDownPhase.events.pointerdown.preventedDefault
const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const clickEvents = _.extend({}, mouseDownPhase.events, mouseUpPhase.events)
@@ -709,7 +722,14 @@ export const create = (state, keyboard, focused, Cypress) => {
const { stopPropagation } = window.MouseEvent.prototype
const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, Constructor, composed = false) => {
type SentEvent = {
stoppedPropagation: boolean
preventedDefault: boolean
el: HTMLElement
modifiers: string
}
const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, Constructor, composed = false): SentEvent => {
evtOptions = _.extend({}, evtOptions, { bubbles, cancelable })
const _eventModifiers = $Keyboard.fromModifierEventOptions(evtOptions)
const modifiers = $Keyboard.modifiersToString(_eventModifiers)
@@ -720,6 +740,9 @@ const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false,
evt.stopPropagation = function (...args) {
evt._hasStoppedPropagation = true
// stopPropagation doesn't have any arguments. So, we cannot type-safely pass the second argument.
// But we're passing it just in case.
// @ts-ignore
return stopPropagation.apply(this, ...args)
}
}
@@ -740,6 +763,19 @@ const formatReasonNotFired = (reason) => {
return `⚠️ not fired (${reason})`
}
type CoordsEventOptions = {
x: number
y: number
clientX: number
clientY: number
screenX: number
screenY: number
pageX: number
pageY: number
layerX: number
layerY: number
}
const toCoordsEventOptions = (x, y, win) => {
// these are the coords from the element's window,
// ignoring scroll position

View File

@@ -0,0 +1,35 @@
import { $Location } from '../cypress/location'
export const bothUrlsMatchAndOneHasHash = (current, remote, eitherShouldHaveHash: boolean = false): boolean => {
// the current has a hash or the last char of href is a hash
const currentHasHash = current.hash || current.href.slice(-1) === '#'
// the remote has a hash or the last char of href is a hash
const remoteHasHash = remote.hash || remote.href.slice(-1) === '#'
const urlHasHash = eitherShouldHaveHash ? (remoteHasHash || currentHasHash) : remoteHasHash
return urlHasHash &&
// both must have the same origin
current.origin === remote.origin &&
// both must have the same pathname
current.pathname === remote.pathname &&
// both must have the same query params
current.search === remote.search
}
export const historyNavigationTriggeredHashChange = (state): boolean => {
const delta = state('navHistoryDelta') || 0
if (delta === 0 || delta == null) { // page refresh
return false
}
const urls = state('urls')
const urlPosition = state('urlPosition')
const currentUrl = $Location.create(urls[urlPosition])
const nextPosition = urlPosition + delta
const nextUrl = $Location.create(urls[nextPosition])
return bothUrlsMatchAndOneHasHash(currentUrl, nextUrl, true)
}

View File

@@ -95,7 +95,7 @@ export function parseStaticResponseShorthand (statusCodeOrBody: number | string
function getFixtureOpts (fixture: string): FixtureOpts {
const [filePath, encoding] = fixture.split(',')
return { filePath, encoding }
return { filePath, encoding: encoding === 'null' ? null : encoding }
}
export function getBackendStaticResponse (staticResponse: Readonly<StaticResponse>): BackendStaticResponseWithArrayBuffer {

View File

@@ -132,6 +132,7 @@ export const create = ($$, state) => {
}
const createSnapshot = (name, $elToHighlight) => {
Cypress.action('cy:snapshot', name)
// create a unique selector for this el
// but only IF the subject is truly an element. For example
// we might be wrapping a primitive like "$([1, 2]).first()"

View File

@@ -8,16 +8,24 @@ const invalidTargets = new Set(['_parent', '_top'])
*/
export function handleInvalidEventTarget (e: Event & {target: HTMLFormElement | HTMLAnchorElement}) {
let targetValue = e.target.target
let targetSet = e.target.hasAttribute('target')
if (invalidTargets.has(e.target.target)) {
e.target.target = ''
}
const { getAttribute, setAttribute } = e.target
const { getAttribute, setAttribute, removeAttribute } = e.target
const targetDescriptor = Object.getOwnPropertyDescriptor(e.target, 'target')
e.target.getAttribute = function (k) {
if (k === 'target') {
// https://github.com/cypress-io/cypress/issues/17512
// When the target attribute doesn't exist, it should return null.
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#non-existing_attributes
if (!targetSet) {
return null
}
return targetValue
}
@@ -26,6 +34,7 @@ export function handleInvalidEventTarget (e: Event & {target: HTMLFormElement |
e.target.setAttribute = function (k, v) {
if (k === 'target') {
targetSet = true
targetValue = v
return $elements.callNativeMethod(this, 'setAttribute', 'cyTarget', v)
@@ -34,6 +43,16 @@ export function handleInvalidEventTarget (e: Event & {target: HTMLFormElement |
return setAttribute.call(this, k, v)
}
e.target.removeAttribute = function (k) {
if (k === 'target') {
targetSet = false
targetValue = ''
// We're not using `$elements.callNativeMethod` here because it disallows `removeAttribute`.
return removeAttribute.call(this, k)
}
}
if (!targetDescriptor) {
Object.defineProperty(e.target, 'target', {
configurable: false,

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import { validate } from '@packages/config'
import { validate, validateNoReadOnlyConfig } from '@packages/config'
import _ from 'lodash'
import $ from 'jquery'
import * as blobUtil from 'blob-util'
@@ -144,6 +144,24 @@ class $Cypress {
this.state = $SetterGetter.create({})
this.originalConfig = _.cloneDeep(config)
this.config = $SetterGetter.create(config, (config) => {
if (!window.top.__cySkipValidateConfig) {
validateNoReadOnlyConfig(config, (errProperty) => {
let errMessage
if (this.state('runnable')) {
errMessage = $errUtils.errByPath('config.invalid_cypress_config_override', {
errProperty,
})
} else {
errMessage = $errUtils.errByPath('config.invalid_test_config_override', {
errProperty,
})
}
throw new this.state('specWindow').Error(errMessage)
})
}
validate(config, (errMsg) => {
throw new this.state('specWindow').Error(errMsg)
})
@@ -503,6 +521,9 @@ class $Cypress {
case 'cy:scrolled':
return this.emit('scrolled', ...args)
case 'cy:snapshot':
return this.emit('snapshot', ...args)
case 'app:uncaught:exception':
return this.emitMap('uncaught:exception', ...args)

View File

@@ -371,6 +371,14 @@ export class CommandQueue extends Queue<Command> {
}
const onError = (err: Error | string) => {
// If the runnable was marked as pending, this test was skipped
// go ahead and just return
const runnable = this.state('runnable')
if (runnable.isPending()) {
return
}
if (this.state('onCommandFailed')) {
return this.state('onCommandFailed')(err, this, next)
}

View File

@@ -1,5 +1,3 @@
// @ts-nocheck
/* eslint-disable prefer-rest-params */
import _ from 'lodash'
import Promise from 'bluebird'
@@ -7,14 +5,14 @@ import debugFn from 'debug'
import $dom from '../dom'
import $utils from './utils'
import $errUtils from './error_utils'
import $errUtils, { ErrorFromProjectRejectionEvent } from './error_utils'
import $stackUtils from './stack_utils'
import { create as createChai, IChai } from '../cy/chai'
import { create as createXhr, IXhr } from '../cy/xhrs'
import { create as createJQuery, IJQuery } from '../cy/jquery'
import { create as createAliases, IAliases } from '../cy/aliases'
import * as $Events from './events'
import { extend as extendEvents } from './events'
import { create as createEnsures, IEnsures } from '../cy/ensures'
import { create as createFocused, IFocused } from '../cy/focused'
import { create as createMouse, Mouse } from '../cy/mouse'
@@ -33,6 +31,8 @@ import { CommandQueue } from './command_queue'
import { initVideoRecorder } from '../cy/video-recorder'
import { TestConfigOverride } from '../cy/testConfigOverrides'
import { create as createOverrides, IOverrides } from '../cy/overrides'
import { historyNavigationTriggeredHashChange } from '../cy/navigation'
import { EventEmitter2 } from 'eventemitter2'
const debugErrors = debugFn('cypress:driver:errors')
@@ -54,7 +54,7 @@ function __stackReplacementMarker (fn, ctx, args) {
return fn.apply(ctx, args)
}
declare let top: WindowProxy & { __alreadySetErrorHandlers__: boolean } | null
declare let top: WindowProxy & { __alreadySetErrorHandlers__: boolean }
// We only set top.onerror once since we make it configurable:false
// but we update cy instance every run (page reload or rerun button)
@@ -80,7 +80,7 @@ const setTopOnError = function (Cypress, cy: $Cy) {
// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
const onTopError = (handlerType) => (event) => {
const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event)
const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) as ErrorFromProjectRejectionEvent
// in some callbacks like for cy.intercept, we catch the errors and then
// rethrow them, causing them to get caught by the top frame
@@ -118,7 +118,7 @@ const setTopOnError = function (Cypress, cy: $Cy) {
top.__alreadySetErrorHandlers__ = true
}
export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused {
export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused {
id: string
specWindow: any
state: any
@@ -138,6 +138,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
isStable: IStability['isStable']
whenStable: IStability['whenStable']
isAnticipatingMultidomain: IStability['isAnticipatingMultidomain']
whenStableOrAnticipatingMultidomain: IStability['whenStableOrAnticipatingMultidomain']
assert: IAssertions['assert']
verifyUpcomingAssertions: IAssertions['verifyUpcomingAssertions']
@@ -207,6 +209,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
private commandFns: Record<string, Function> = {}
constructor (specWindow, Cypress, Cookies, state, config, autoRun = true) {
super()
state('specWindow', specWindow)
this.specWindow = specWindow
@@ -248,7 +252,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
this.isStable = stability.isStable
this.whenStable = stability.whenStable
this.isAnticipatingMultidomain = stability.isAnticipatingMultidomain
this.whenAnticipatingMultidomain = stability.whenAnticipatingMultidomain
this.whenStableOrAnticipatingMultidomain = stability.whenStableOrAnticipatingMultidomain
const assertions = createAssertions(Cypress, this)
@@ -256,7 +260,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
this.verifyUpcomingAssertions = assertions.verifyUpcomingAssertions
const onFinishAssertions = function () {
return assertions.finishAssertions.apply(window, arguments)
return assertions.finishAssertions.apply(window, arguments as any)
}
const retries = createRetries(Cypress, state, this.timeout, this.clearTimeout, this.whenStable, onFinishAssertions)
@@ -353,7 +357,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
// make cy global in the specWindow
specWindow.cy = this
$Events.extend(this)
extendEvents(this)
Cypress.on('enqueue:command', (attrs) => {
this.enqueue(attrs)
@@ -368,7 +372,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
return this.queue.stopped
}
fail (err, options = {}) {
fail (err, options: { async?: boolean } = {}) {
// this means the error has already been through this handler and caught
// again. but we don't need to run it through again, so we can re-throw
// it and it will fail the test as-is
@@ -450,7 +454,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
try {
// collect all of the callbacks for 'fail'
rets = this.Cypress.action('cy:fail', err, this.state('runnable'))
} catch (cyFailErr) {
} catch (cyFailErr: any) {
// and if any of these throw synchronously immediately error
cyFailErr.isCyFailErr = true
@@ -488,8 +492,19 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
// proxy has not injected Cypress.action('window:before:load')
// so Cypress.onBeforeAppWindowLoad() was never called
return $autIframe.on('load', () => {
// if setting these props failed
// then we know we're in a cross origin failure
if (historyNavigationTriggeredHashChange(this.state)) {
// Skip load event.
// Chromium 97+ triggers fires iframe onload for cross-origin-initiated same-document
// navigations to make it appear to be a cross-document navigation, even when it wasn't
// to alleviate security risk where a cross-origin initiator can check whether
// or not onload fired to guess the url of a target frame.
// When the onload is fired, neither the before:unload or unload event is fired to remove
// the attached listeners or to clean up the current page state.
// https://github.com/cypress-io/cypress/issues/19230
return
}
// if setting these props failed then we know we're in a cross origin failure
try {
const autWindow = getContentWindow($autIframe)
@@ -835,6 +850,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
r(err)
}
}
return
}
setRunnable (runnable, hookId) {
@@ -962,6 +979,12 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
// else just return ret
return ret
} catch (err) {
// If the runnable was marked as pending, this test was skipped
// go ahead and just return
if (runnable.isPending()) {
return
}
// if runnable.fn threw synchronously, then it didnt fail from
// a cypress command, but we should still teardown and handle
// the error
@@ -1018,7 +1041,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
$Listeners.bindTo(contentWindow, {
// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
onError: (handlerType) => (event) => {
const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event)
const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) as ErrorFromProjectRejectionEvent
const handled = cy.onUncaughtException({
err,
promise,
@@ -1034,6 +1057,9 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
// uncaught exception behavior (logging to console)
return undefined
},
onHistoryNav (delta) {
cy.state('navHistoryDelta', delta)
},
onSubmit (e) {
return cy.Cypress.action('app:form:submitted', e)
},
@@ -1106,7 +1132,8 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
return this.Cypress.action('cy:command:enqueued', obj)
}
private getCommandsUntilFirstParentOrValidSubject (command, memo = []) {
// TODO: Replace any with Command type.
private getCommandsUntilFirstParentOrValidSubject (command, memo: any[] = []) {
if (!command) {
return null
}
@@ -1122,11 +1149,12 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
return this.getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo)
}
private pushSubjectAndValidate (name, args, firstCall, prevSubject) {
// TODO: make string[] more
private pushSubjectAndValidate (name, args, firstCall, prevSubject: string[]) {
if (firstCall) {
// if we have a prevSubject then error
// since we're invoking this improperly
if (prevSubject && ![].concat(prevSubject).includes('optional')) {
if (prevSubject && !([] as string[]).concat(prevSubject).includes('optional')) {
const stringifiedArg = $utils.stringifyActual(args[0])
$errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', {
@@ -1148,7 +1176,7 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer
if (prevSubject) {
// make sure our current subject is valid for
// what we expect in this command
this.ensureSubjectByType(subject, prevSubject, name)
this.ensureSubjectByType(subject, prevSubject)
}
args.unshift(subject)

View File

@@ -250,10 +250,16 @@ export default {
message: `Setting the config via ${cmd('Cypress.config')} failed with the following validation error:\n\n{{errMsg}}`,
docsUrl: 'https://on.cypress.io/config',
},
'invalid_test_override': {
invalid_test_override: {
message: `The config override passed to your test has the following validation error:\n\n{{errMsg}}`,
docsUrl: 'https://on.cypress.io/config',
},
invalid_cypress_config_override: {
message: `\`Cypress.config()\` cannot mutate option \`{{errProperty}}\` because it is a read-only property.`,
},
invalid_test_config_override: {
message: `Cypress test configuration cannot mutate option \`{{errProperty}}\` because it is a read-only property.`,
},
},
contains: {

View File

@@ -265,6 +265,7 @@ export class InternalCypressError extends Error {
export class CypressError extends Error {
docsUrl?: string
retry?: boolean
constructor (message) {
super(message)
@@ -469,7 +470,12 @@ const convertErrorEventPropertiesToObject = (args) => {
})
}
const errorFromErrorEvent = (event) => {
export interface ErrorFromErrorEvent {
originalErr: Error
err: Error
}
const errorFromErrorEvent = (event): ErrorFromErrorEvent => {
let { message, filename, lineno, colno, error } = event
let docsUrl = error?.docsUrl
@@ -497,7 +503,11 @@ const errorFromErrorEvent = (event) => {
}
}
const errorFromProjectRejectionEvent = (event) => {
export interface ErrorFromProjectRejectionEvent extends ErrorFromErrorEvent {
promise: Promise<any>
}
const errorFromProjectRejectionEvent = (event): ErrorFromProjectRejectionEvent => {
// Bluebird triggers "unhandledrejection" with its own custom error event
// where the `promise` and `reason` are attached to event.detail
// http://bluebirdjs.com/docs/api/error-management-configuration.html

View File

@@ -135,7 +135,7 @@ const getCodeFrameFromSource = (sourceCode, { line, column, relativeFile, absolu
}
}
const captureUserInvocationStack = (ErrorConstructor, userInvocationStack) => {
const captureUserInvocationStack = (ErrorConstructor, userInvocationStack?) => {
if (!userInvocationStack) {
const newErr = new ErrorConstructor('userInvocationStack')

View File

@@ -44,10 +44,10 @@ export const isAttached = function ($el) {
})
}
export const isDetachedEl = (el: HTMLElement) => {
export const isDetachedEl = (el: HTMLElement | JQuery<any>) => {
return !isAttachedEl(el)
}
export const isAttachedEl = function (el: HTMLElement) {
export const isAttachedEl = function (el: HTMLElement | JQuery<any>) {
return isAttached($(el))
}

View File

@@ -4,7 +4,7 @@ import $document from '../document'
import $jquery from '../jquery'
import { getTagName } from './elementHelpers'
import { isWithinShadowRoot, getShadowElementFromPoint } from './shadow'
import { normalizeWhitespaces, escapeQuotes, isSelector } from './utils'
import { normalizeWhitespaces, escapeQuotes } from './utils'
/**
* Find Parents relative to an initial element
@@ -125,10 +125,16 @@ export const getFirstDeepestElement = ($el: JQuery, index = 0) => {
const $current = $el.slice(index, index + 1)
const $next = $el.slice(index + 1, index + 2)
if (!$next) {
if (!$next || $current.length === 0) {
return $current
}
// https://github.com/cypress-io/cypress/issues/14861
// filter out the <script> and <style> tags
if ($current && ['SCRIPT', 'STYLE'].includes($current.prop('tagName'))) {
return getFirstDeepestElement($el, index + 1)
}
// does current contain next?
if ($.contains($current.get(0), $next.get(0))) {
return getFirstDeepestElement($el, index + 1)
@@ -217,20 +223,6 @@ export const getElements = ($el) => {
return els
}
// Remove <style> and <script> elements inside <body>. Even though the contains
// selector avoids selecting them with :not(script,style), it will find the
// text anyway and attribute it to the <body>
// https://github.com/cypress-io/cypress/issues/14861
const removeScriptAndStyleElements = (elem) => {
const $elem = $(elem)
if (!isSelector($elem, 'body')) return elem
$elem.find('script,style').remove()
return $elem[0]
}
export const getContainsSelector = (text, filter = '', options: {
matchCase?: boolean
} = {}) => {
@@ -252,14 +244,12 @@ export const getContainsSelector = (text, filter = '', options: {
// taken from jquery's normal contains method
cyContainsSelector = function (elem) {
elem = removeScriptAndStyleElements(elem)
let testText = normalizeWhitespaces(elem)
const testText = normalizeWhitespaces(elem)
return text.test(testText)
}
} else if (_.isString(text)) {
cyContainsSelector = function (elem) {
elem = removeScriptAndStyleElements(elem)
let testText = normalizeWhitespaces(elem)
if (!options.matchCase) {
@@ -284,7 +274,7 @@ export const getContainsSelector = (text, filter = '', options: {
const textToFind = escapedText.includes(`\'`) ? `"${escapedText}"` : `'${escapedText}'`
// use custom cy-contains selector that is registered above
return `${filter}:not(script,style):cy-contains(${textToFind}), ${filter}[type='submit'][value~=${textToFind}]`
return `${filter}:cy-contains(${textToFind}), ${filter}[type='submit'][value~=${textToFind}]`
})
return selectors.join()

View File

@@ -24,7 +24,7 @@
},
"devDependencies": {
"electron": "15.2.0",
"electron-packager": "14.1.1",
"electron-packager": "15.1.0",
"execa": "4.1.0",
"mocha": "3.5.3"
},

View File

@@ -7,7 +7,7 @@ import type {
} from './external-types'
export type FixtureOpts = {
encoding: string
encoding: string | null
filePath: string
}

View File

@@ -728,9 +728,14 @@ describe('errors ui', () => {
})
describe('docs url', () => {
after(() => {
window.top.__cySkipValidateConfig = false
})
const file = 'docs_url_spec.js'
const docsUrl = 'https://on.cypress.io/viewport'
window.top.__cySkipValidateConfig = true
verify.it('displays as button in interactive mode', { retries: 1 }, {
file,
verifyFn () {

View File

@@ -401,8 +401,13 @@ describe('runner/cypress retries.ui.spec', { viewportWidth: 600, viewportHeight:
})
describe('can configure retries', () => {
after(() => {
window.top.__cySkipValidateConfig = false
})
const haveCorrectError = ($el) => cy.wrap($el).last().parentsUntil('.collapsible').last().parent().find('.runnable-err').should('contain', 'Unspecified AssertionError')
window.top.__cySkipValidateConfig = true
it('via config value', () => {
runIsolatedCypress({
suites: {

View File

@@ -520,13 +520,13 @@ export = {
const originalBrowserKill = launchedBrowser.kill
/* @ts-expect-error */
launchedBrowser.kill = async (...args) => {
launchedBrowser.kill = (...args) => {
debug('closing remote interface client')
await criClient.close()
criClient.close()
debug('closing chrome')
await originalBrowserKill.apply(launchedBrowser, args)
originalBrowserKill.apply(launchedBrowser, args)
}
await this._maybeRecordVideo(criClient, options, browser.majorVersion)

View File

@@ -191,7 +191,6 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
maybeDebugCdpMessages(cri)
cri.send = Bluebird.promisify(cri.send, { context: cri })
cri.close = Bluebird.promisify(cri.close, { context: cri })
// @see https://github.com/cyrus-and/chrome-remote-interface/issues/72
cri._notifier.on('disconnect', reconnect)

View File

@@ -165,7 +165,7 @@ module.exports = {
return obj
}).catch((err) => {
throw new Error(`'${fixture}' is not a valid JavaScript object.${err.toString()}`)
throw new Error(`'${fixture}' is not a valid JavaScript object.\n${err.toString()}`)
})
},
@@ -190,10 +190,16 @@ module.exports = {
parseHtml (p, fixture) {
return fs.readFileAsync(p, 'utf8')
.bind(this)
.catch((err) => {
throw new Error(`Unable to parse '${fixture}'.\n${err.toString()}`)
})
},
parse (p, fixture, encoding) {
return fs.readFileAsync(p, encoding)
.bind(this)
.catch((err) => {
throw new Error(`Unable to parse '${fixture}'.\n${err.toString()}`)
})
},
}

View File

@@ -300,10 +300,9 @@ describe('lib/browsers/chrome', () => {
.then(() => {
expect(this.launchedBrowser.kill).to.be.a('function')
return this.launchedBrowser.kill()
}).then(() => {
expect(this.criClient.close).to.be.calledOnce
this.launchedBrowser.kill()
expect(this.criClient.close).to.be.calledOnce
expect(kill).to.be.calledOnce
})
})

View File

@@ -1430,6 +1430,7 @@ describe('lib/config', () => {
e2e: { from: 'default', value: {} },
env: {},
execTimeout: { value: 60000, from: 'default' },
exit: { value: true, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalSourceRewriting: { value: false, from: 'default' },
@@ -1441,6 +1442,8 @@ describe('lib/config', () => {
ignoreTestFiles: { value: '*.hot-update.js', from: 'default' },
includeShadowDom: { value: false, from: 'default' },
integrationFolder: { value: 'cypress/integration', from: 'default' },
isInteractive: { value: true, from: 'default' },
keystrokeDelay: { value: 0, from: 'default' },
modifyObstructiveCode: { value: true, from: 'default' },
numTestsKeptInMemory: { value: 50, from: 'default' },
pageLoadTimeout: { value: 60000, from: 'default' },
@@ -1517,6 +1520,7 @@ describe('lib/config', () => {
downloadsFolder: { value: 'cypress/downloads', from: 'default' },
e2e: { from: 'default', value: {} },
execTimeout: { value: 60000, from: 'default' },
exit: { value: true, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalSourceRewriting: { value: false, from: 'default' },
@@ -1550,6 +1554,8 @@ describe('lib/config', () => {
ignoreTestFiles: { value: '*.hot-update.js', from: 'default' },
includeShadowDom: { value: false, from: 'default' },
integrationFolder: { value: 'cypress/integration', from: 'default' },
isInteractive: { value: true, from: 'default' },
keystrokeDelay: { value: 0, from: 'default' },
modifyObstructiveCode: { value: true, from: 'default' },
numTestsKeptInMemory: { value: 50, from: 'default' },
pageLoadTimeout: { value: 60000, from: 'default' },

View File

@@ -215,7 +215,7 @@ ParseError: Unterminated string constant\
.then(() => {
throw new Error('should have failed but did not')
}).catch((err) => {
expect(err.message).to.eq(`'bad_js.js' is not a valid JavaScript object.\n${e}`)
expect(err.message).to.eq(`'bad_js.js' is not a valid JavaScript object.\n\n${e}`)
})
})
})

View File

@@ -1,32 +1,49 @@
/* eslint-disable no-console */
const minimist = require('minimist')
const options = minimist(process.argv)
const os = require('os')
const la = require('lazy-ass')
const fs = require('fs-extra')
const is = require('check-more-types')
const execa = require('execa')
const { getNameAndBinary } = require('./utils')
/* eslint-disable no-console */
const options = minimist(process.argv)
const cwd = options.cwd || '/tmp/testing'
fs.ensureDirSync(cwd)
const spawnOpts = {
cwd,
shell: os.platform() === 'win32' ? 'bash.exe' : '/bin/bash',
stdio: 'inherit',
}
const { npm, binary } = getNameAndBinary(process.argv)
la(is.unemptyString(npm), 'missing npm url')
la(is.unemptyString(binary), 'missing binary url')
console.log('testing NPM from', npm)
console.log('and binary from', binary)
const cwd = options.cwd || process.cwd()
console.log('in', cwd)
execa(`npm install ${npm}`, {
cwd,
shell: true,
stdio: 'inherit',
env: {
CYPRESS_INSTALL_BINARY: binary,
},
})
console.log('Create Dummy Project')
execa('npm init -y', spawnOpts)
.then(console.log)
.then(() => {
console.log('testing NPM from', npm)
console.log('and binary from', binary)
console.log('in', cwd)
return execa(`npm install ${npm}`, {
...spawnOpts,
env: {
CYPRESS_INSTALL_BINARY: binary,
},
}).then(console.log)
})
.then(() => {
console.log('Verify Cypress binary')
return execa('$(yarn bin cypress) verify', spawnOpts)
.then(console.log)
})
.catch((e) => {
console.error(e)
process.exit(1)

View File

@@ -0,0 +1,70 @@
exports['e2e issue 6407 throws if mutating read-only config with test configuration 1'] = `
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 1.2.3 │
│ Browser: FooBrowser 88 │
│ Specs: 1 found (issue_6407_spec.js) │
│ Searched: cypress/integration/issue_6407_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: issue_6407_spec.js (1 of 1)
1) throws if mutating read-only config with test configuration
0 passing
1 failing
1) throws if mutating read-only config with test configuration:
CypressError: The config override passed to your test has the following validation error:
CypressError: Cypress test configuration cannot mutate option \`chromeWebSecurity\` because it is a read-only property.
https://on.cypress.io/config
Error
[stack trace lines]
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 1 │
│ Passing: 0 │
│ Failing: 1 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: true │
│ Duration: X seconds │
│ Spec Ran: issue_6407_spec.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/issue_6407_spec.js.mp4 (X second)
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✖ issue_6407_spec.js XX:XX 1 - 1 - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✖ 1 of 1 failed (100%) XX:XX 1 - 1 - -
`

View File

@@ -1,15 +1,15 @@
import fs from 'fs-extra'
import _path from 'path'
import chokidar from 'chokidar'
import os from 'os'
import cachedir from 'cachedir'
import execa from 'execa'
import tempDir from 'temp-dir'
const root = _path.join(__dirname, '..')
const serverRoot = _path.join(__dirname, '../../packages/server/')
const projects = _path.join(root, 'projects')
const tmpDir = _path.join(os.tmpdir(), 'cy-projects')
const cyTmpDir = _path.join(tempDir, 'cy-projects')
// copy contents instead of deleting+creating new file, which can cause
// filewatchers to lose track of toFile.
@@ -27,9 +27,9 @@ const copyContents = (fromFile, toFile) => {
}
// copies all of the project fixtures
// to the tmpDir .projects in the root
// to the cyTmpDir .projects in the root
export function scaffold () {
fs.copySync(projects, tmpDir)
fs.copySync(projects, cyTmpDir)
}
/**
@@ -37,7 +37,7 @@ export function scaffold () {
*/
export function scaffoldProject (project: string): void {
const from = _path.join(projects, project)
const to = _path.join(tmpDir, project)
const to = _path.join(cyTmpDir, project)
fs.copySync(from, to)
}
@@ -131,7 +131,7 @@ function getYarnCommand (opts: {
// in CircleCI, this offline cache can be used
if (opts.isCI) cmd += ` --cache-folder=~/.yarn-${process.platform} `
else cmd += ` --cache-folder=${_path.join(os.tmpdir(), 'cy-system-tests-yarn-cache', String(Date.now()))}`
else cmd += ` --cache-folder=${_path.join(tempDir, 'cy-system-tests-yarn-cache', String(Date.now()))}`
return cmd
}
@@ -291,7 +291,7 @@ export async function scaffoldCommonNodeModules () {
}
export async function symlinkNodeModule (pkg) {
const from = _path.join(tmpDir, 'node_modules', pkg)
const from = _path.join(cyTmpDir, 'node_modules', pkg)
const to = pathToPackage(pkg)
await fs.ensureDir(_path.dirname(from))
@@ -312,7 +312,7 @@ export function scaffoldWatch () {
chokidar.watch(watchdir, {
})
.on('change', (srcFilepath, stats) => {
const tmpFilepath = _path.join(tmpDir, _path.relative(watchdir, srcFilepath))
const tmpFilepath = _path.join(cyTmpDir, _path.relative(watchdir, srcFilepath))
return copyContents(srcFilepath, tmpFilepath)
})
@@ -320,19 +320,19 @@ export function scaffoldWatch () {
}
// removes all of the project fixtures
// from the tmpDir .projects in the root
// from the cyTmpDir .projects in the root
export function remove () {
return fs.removeSync(tmpDir)
return fs.removeSync(cyTmpDir)
}
// returns the path to project fixture
// in the tmpDir
// in the cyTmpDir
export function project (...args) {
return this.projectPath.apply(this, args)
}
export function projectPath (name) {
return _path.join(tmpDir, name)
return _path.join(cyTmpDir, name)
}
export function get (fixture, encoding: BufferEncoding = 'utf8') {

View File

@@ -73,6 +73,7 @@
"ssestream": "1.0.1",
"supertest": "4.0.2",
"systeminformation": "5.6.4",
"temp-dir": "^2.0.0",
"webpack": "4.43.0",
"ws": "5.2.3"
},

View File

@@ -0,0 +1,4 @@
/* eslint-disable mocha/no-global-tests, no-undef */
it('throws if mutating read-only config with test configuration', { chromeWebSecurity: false }, () => {
expect(true)
})

View File

@@ -1,4 +1,5 @@
/// <reference types="cypress" />
window.top.__cySkipValidateConfig = true
Cypress.config('isInteractive', true)
Cypress.config('experimentalSessionSupport', true)

View File

@@ -8,6 +8,8 @@ import { fail, verify } from '../../e2e/cypress/support/util'
context('validation errors', function () {
beforeEach(() => {
// @ts-ignore
window.top.__cySkipValidateConfig = true
Cypress.config('isInteractive', true)
})
@@ -16,7 +18,7 @@ context('validation errors', function () {
})
verify(this, {
line: 15,
line: 17,
column: 8,
message: 'can only accept a string preset or',
stack: ['throwErrBadArgs', 'From Your Spec Code:'],

View File

@@ -1,5 +1,4 @@
/// <reference types="cypress" />
/**
* This tests the error UI for a certain webpack preprocessor setup.
* It does this by having a test fail and then a subsequent test run that
@@ -16,6 +15,8 @@ import { fail, verify } from '../../../e2e/cypress/support/util'
context('validation errors', function () {
beforeEach(() => {
// @ts-ignore
window.top.__cySkipValidateConfig = true
// @ts-ignore
Cypress.config('isInteractive', true)
})
@@ -26,7 +27,7 @@ context('validation errors', function () {
})
verify(this, {
line: 25,
line: 26,
column: 8,
message: 'can only accept a string preset or',
stack: ['throwErrBadArgs', 'From Your Spec Code:'],

View File

@@ -1,5 +1,4 @@
/// <reference types="cypress" />
/**
* This tests the error UI for a certain webpack preprocessor setup.
* It does this by having a test fail and then a subsequent test run that
@@ -16,6 +15,8 @@ import { fail, verify } from '../../../e2e/cypress/support/util'
context('validation errors', function () {
beforeEach(() => {
// @ts-ignore
window.top.__cySkipValidateConfig = true
// @ts-ignore
Cypress.config('isInteractive', true)
})
@@ -26,7 +27,7 @@ context('validation errors', function () {
})
verify(this, {
line: 25,
line: 26,
column: 8,
message: 'can only accept a string preset or',
stack: ['throwErrBadArgs', 'From Your Spec Code:'],

View File

@@ -6,8 +6,11 @@
import { fail, verify } from '../../../e2e/cypress/support/util'
window.top.__cySkipValidateConfig = true
context('validation errors', function () {
beforeEach(() => {
// @ts-ignore
window.top.__cySkipValidateConfig = true
Cypress.config('isInteractive', true)
})
@@ -16,7 +19,7 @@ context('validation errors', function () {
})
verify(this, {
line: 15,
line: 18,
column: 8,
message: 'can only accept a string preset or',
stack: ['throwErrBadArgs', 'From Your Spec Code:'],

View File

@@ -0,0 +1,14 @@
const systemTests = require('../lib/system-tests').default
describe('e2e issue 6407', () => {
systemTests.setup()
// https://github.com/cypress-io/cypress/issues/6407
it('throws if mutating read-only config with test configuration', function () {
return systemTests.exec(this, {
spec: 'issue_6407_spec.js',
snapshot: true,
expectedExitCode: 1,
})
})
})

164
yarn.lock
View File

@@ -10799,22 +10799,7 @@ asap@^2.0.0, asap@~2.0.3, asap@~2.0.6:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
asar@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/asar/-/asar-2.1.0.tgz#97c6a570408c4e38a18d4a3fb748a621b5a7844e"
integrity sha512-d2Ovma+bfqNpvBzY/KU8oPY67ZworixTpkjSx0PCXnQi67c2cXmssaTxpFDUM0ttopXoGx/KRxNg/GDThYbXQA==
dependencies:
chromium-pickle-js "^0.2.0"
commander "^2.20.0"
cuint "^0.2.2"
glob "^7.1.3"
minimatch "^3.0.4"
mkdirp "^0.5.1"
tmp-promise "^1.0.5"
optionalDependencies:
"@types/glob" "^7.1.1"
asar@^3.0.3:
asar@^3.0.0, asar@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/asar/-/asar-3.1.0.tgz#70b0509449fe3daccc63beb4d3c7d2e24d3c6473"
integrity sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==
@@ -11145,14 +11130,6 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
axios@0.18.1:
version "0.18.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3"
integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==
dependencies:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
axios@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
@@ -11161,12 +11138,12 @@ axios@0.19.0:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
axios@0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
axios@0.21.2:
version "0.21.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
dependencies:
follow-redirects "1.5.10"
follow-redirects "^1.14.0"
axobject-query@2.0.2:
version "2.0.2"
@@ -15370,13 +15347,6 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
cross-zip@^2.1.5:
version "2.1.6"
resolved "https://registry.yarnpkg.com/cross-zip/-/cross-zip-2.1.6.tgz#344d3ba9488609942987d815bb84860cff3d9491"
integrity sha512-xLIETNkzRcU6jGRzenJyRFxahbtP4628xEKMTI/Ql0Vu8m4h8M7uRLVi7E5OYHuJ6VQPsG4icJumKAFUvfm0+A==
dependencies:
rimraf "^3.0.0"
crypt@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
@@ -15905,11 +15875,6 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b"
integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==
cuint@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -17390,15 +17355,7 @@ electron-is-dev@^2.0.0:
resolved "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz#833487a069b8dad21425c67a19847d9064ab19bd"
integrity sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==
electron-notarize@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-0.2.1.tgz#759e8006decae19134f82996ed910db26d9192cc"
integrity sha512-oZ6/NhKeXmEKNROiFmRNfytqu3cxqC95sjooG7kBXQVEUSQkZnbiAhxVh5jXngL881G197pbwpeVPJyM7Ikmxw==
dependencies:
debug "^4.1.1"
fs-extra "^8.1.0"
electron-notarize@^1.1.1:
electron-notarize@^1.0.0, electron-notarize@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.1.1.tgz#3ed274b36158c1beb1dbef14e7faf5927e028629"
integrity sha512-kufsnqh86CTX89AYNG3NCPoboqnku/+32RxeJ2+7A4Rbm4bbOx0Nc7XTy3/gAlBfpj9xPAxHfhZLOHgfi6cJVw==
@@ -17430,18 +17387,19 @@ electron-osx-sign@^0.5.0:
minimist "^1.2.0"
plist "^3.0.1"
electron-packager@14.1.1:
version "14.1.1"
resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-14.1.1.tgz#260affa0287070e1cf25e5fed074564b8c5494ed"
integrity sha512-PODWJ8LFfwUCniTMy4Z5iiZyVHi4W71Pvn/SxJPC6kbI3EfZvo8n5H856XATxNUGVxlmAB5qeSbRym8/f9jISg==
electron-packager@15.1.0:
version "15.1.0"
resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-15.1.0.tgz#16a3733e4cad26112a2ac36f0b0f35c3b0170eff"
integrity sha512-THNm4bz1DfvR9f0g51+NjuAYELflM8+1vhQ/iv/G8vyZNKzSMuFd5doobngQKq3rRsLdPNZVnGqDdgS884d7Og==
dependencies:
"@electron/get" "^1.6.0"
asar "^2.0.1"
cross-zip "^2.1.5"
asar "^3.0.0"
debug "^4.0.1"
electron-notarize "^0.2.0"
electron-notarize "^1.0.0"
electron-osx-sign "^0.4.11"
fs-extra "^8.1.0"
extract-zip "^2.0.0"
filenamify "^4.1.0"
fs-extra "^9.0.0"
galactus "^0.2.1"
get-package-info "^1.0.0"
junk "^3.1.0"
@@ -17449,9 +17407,8 @@ electron-packager@14.1.1:
plist "^3.0.0"
rcedit "^2.0.0"
resolve "^1.1.6"
sanitize-filename "^1.6.0"
semver "^6.0.0"
yargs-parser "^16.0.0"
semver "^7.1.3"
yargs-parser "^19.0.1"
electron-publish@22.13.1:
version "22.13.1"
@@ -19009,7 +18966,7 @@ extract-text-webpack-plugin@4.0.0-beta.0:
schema-utils "^0.4.5"
webpack-sources "^1.1.0"
extract-zip@2.0.1, extract-zip@^2.0.1:
extract-zip@2.0.1, extract-zip@^2.0.0, extract-zip@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@@ -19334,6 +19291,20 @@ filename-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=
filename-reserved-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229"
integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik=
filenamify@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.3.0.tgz#62391cb58f02b09971c9d4f9d63b3cf9aba03106"
integrity sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==
dependencies:
filename-reserved-regex "^2.0.0"
strip-outer "^1.0.1"
trim-repeated "^1.0.0"
filesize@3.6.1, filesize@^3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@@ -19780,6 +19751,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6"
integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg==
follow-redirects@^1.14.0:
version "1.14.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
@@ -28558,14 +28534,14 @@ normalize-url@^3.0.0, normalize-url@^3.3.0:
integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
normalize-url@^4.1.0, normalize-url@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
version "4.5.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
normalize-url@^5.0.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-5.3.0.tgz#8959b3cdaa295b61592c1f245dded34b117618dd"
integrity sha512-9/nOVLYYe/dO/eJeQUNaGUF4m4Z5E7cb9oNTKabH+bNf19mqj60txTcveQxL0GlcWLXCxkOu2/LwL8oW0idIDA==
version "5.3.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-5.3.1.tgz#c8485c0f5ba2f9c17a6d2907b56117ae5967f882"
integrity sha512-K1c7+vaAP+Yh5bOGmA10PGPpp+6h7WZrl7GwqKhUflBc9flU9pzG27DDeB9+iuhZkE3BJZOcgN1P/2sS5pqrWw==
normalize.css@^8.0.1:
version "8.0.1"
@@ -34786,7 +34762,7 @@ sane@^4.0.3:
minimist "^1.1.1"
walker "~1.0.5"
sanitize-filename@1.6.3, sanitize-filename@^1.6.0, sanitize-filename@^1.6.1, sanitize-filename@^1.6.3:
sanitize-filename@1.6.3, sanitize-filename@^1.6.1, sanitize-filename@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
@@ -35150,7 +35126,7 @@ semver@7.3.4:
dependencies:
lru-cache "^6.0.0"
semver@7.3.5, semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
semver@7.3.5, semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -36438,9 +36414,9 @@ ssri@^5.2.4:
safe-buffer "^5.1.1"
ssri@^6.0.0, ssri@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
version "6.0.2"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
dependencies:
figgy-pudding "^3.5.1"
@@ -36955,6 +36931,13 @@ strip-json-comments@^1.0.4:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
strip-outer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631"
integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==
dependencies:
escape-string-regexp "^1.0.2"
strong-log-transformer@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10"
@@ -37921,14 +37904,6 @@ title-case@^2.1.0:
no-case "^2.2.0"
upper-case "^1.0.3"
tmp-promise@^1.0.5:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
integrity sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==
dependencies:
bluebird "^3.5.0"
tmp "0.1.0"
tmp-promise@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"
@@ -37943,13 +37918,6 @@ tmp@0.0.33, tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
tmp@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
dependencies:
rimraf "^2.6.3"
tmp@^0.2.0, tmp@^0.2.1, tmp@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -38150,6 +38118,13 @@ trim-off-newlines@^1.0.0:
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
trim-repeated@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21"
integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE=
dependencies:
escape-string-regexp "^1.0.2"
trim-right@^1.0.0, trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -41645,14 +41620,6 @@ yargs-parser@^15.0.1:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^16.0.0:
version "16.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1"
integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
@@ -41661,6 +41628,11 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^19.0.1:
version "19.0.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-19.0.4.tgz#99183a3a59268b205c6b04177f2a5bfb46e79ba7"
integrity sha512-eXeQm7yXRjPFFyf1voPkZgXQZJjYfjgQUmGPbD2TLtZeIYzvacgWX7sQ5a1HsRgVP+pfKAkRZDNtTGev4h9vhw==
yargs-parser@^20.2.2, yargs-parser@^20.2.3:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"