chore: convert CLI tests to vitest (#32416)

* chore: rename snapshots and spec files to fit vitest convention (#32405)

* chore: move compiled files to dist directory to make vitest convertion easier (#32406)

* chore: convert utils to vitest (#32407)

* chore: convert logger to vitest

* chore: convert errors spec to vitest

* chore: convert cypress spec to vitest

* chore: convert `exec` directory to `vitest` (#32428)

* chore: cut over exec directory to vitest

* Update cli/test/lib/exec/run.spec.ts

* Update cli/test/lib/exec/run.spec.ts

* Update cli/test/lib/exec/run.spec.ts

* chore: convert the CLI and build script specs over to vitest (#32438)

* chore: convert tasks directory to vitest (#32434)

change way verify module is exported due to issues interpreting module (thinks its an esm)

rework scripts as we cannot run an empty mocha suite

chore: fix snapshots and verify requires that are internal to the cypress project

fix stubbing issues with fs-extra which is also used by request-progress under the hood

fix issues where xvfb was stopping prematurely

* chore: remove files no longer used now that mocha tests are converted to vitest (#32455)

* build binaries

* chore: fix CLI tests (#32484)

* chore: remove CI branch
This commit is contained in:
Bill Glesias
2025-09-12 19:20:13 -04:00
committed by GitHub
parent 387a2d8264
commit 31ee30b6f3
101 changed files with 9922 additions and 8658 deletions

View File

@@ -1,2 +1,2 @@
# Bump this version to force CI to re-create the cache from scratch.
9-2-2025-cli2
9-12-2025

View File

@@ -38,7 +38,7 @@ mainBuildFilters: &mainBuildFilters
- /^release\/\d+\.\d+\.\d+$/
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'update-v8-snapshot-cache-on-develop'
- 'update-tsx'
- 'feature/cli_to_vitest'
# 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
@@ -49,7 +49,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'update-tsx', << pipeline.git.branch >> ]
- equal: [ 'feature/cli_to_vitest', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -60,7 +60,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'chore/migrate-electron-lib-ts', << pipeline.git.branch >> ]
- equal: [ 'feature/cli_to_vitest', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -83,7 +83,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'chore/migrate-electron-lib-ts', << pipeline.git.branch >> ]
- equal: [ 'feature/cli_to_vitest', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -163,7 +163,7 @@ commands:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-tsx" ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "feature/cli_to_vitest" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command
@@ -1828,7 +1828,7 @@ jobs:
source ./scripts/ensure-node.sh
yarn lerna run types
- sanitize-verify-and-store-mocha-results:
expectedResultCount: 20
expectedResultCount: 19
verify-release-readiness:
<<: *defaults

View File

@@ -13,4 +13,5 @@ package.json
/react
/vue
/svelte
/mount-utils
/mount-utils
tsconfig.esm.json

View File

@@ -1,6 +0,0 @@
module.exports = {
spec: 'test/**/*_spec.ts',
timeout: 10000,
reporter: 'spec',
recursive: true
}

View File

@@ -1,37 +0,0 @@
exports['package.json build outputs expected properties 1'] = {
'name': 'test',
'engines': 'test engines',
'version': 'x.y.z',
'buildInfo': 'replaced by normalizePackageJson',
'description': 'Cypress is a next generation front end testing tool built for the modern web',
'homepage': 'https://cypress.io',
'license': 'MIT',
'bugs': {
'url': 'https://github.com/cypress-io/cypress/issues',
},
'repository': {
'type': 'git',
'url': 'https://github.com/cypress-io/cypress.git',
},
'keywords': [
'automation',
'browser',
'cypress',
'cypress.io',
'e2e',
'end-to-end',
'integration',
'component',
'mocks',
'runner',
'spies',
'stubs',
'test',
'testing',
],
'types': 'types',
'scripts': {
'postinstall': 'node index.js --exec install',
'size': 't="$(npm pack .)"; wc -c "${t}"; tar tvf "${t}"; rm "${t}";',
},
}

View File

@@ -1,4 +0,0 @@
exports['cypress .run resolves with contents of tmp file 1'] = {
'code': 0,
'failingTests': [],
}

View File

@@ -1,66 +0,0 @@
exports['download status errors 1'] = `
Error: The Cypress App could not be downloaded.
Does your workplace require a proxy to be used to access the Internet? If so, you must configure the HTTP_PROXY environment variable before downloading Cypress. Read more: https://on.cypress.io/proxy-configuration
Otherwise, please check network connectivity and try again:
----------
URL: https://download.cypress.io/desktop?platform=OS&arch=x64
404 - Not Found
----------
Platform: OS-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['latest desktop url 1'] = `
https://download.cypress.io/desktop?platform=OS&arch=ARCH
`
exports['specific version desktop url 1'] = `
https://download.cypress.io/desktop/0.20.2?platform=OS&arch=ARCH
`
exports['desktop url from template'] = `
https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip
`
exports['desktop url from template with version'] = `
https://mycompany/0.20.2/OS-ARCH/cypress.zip
`
exports['desktop url from template with multiple replacements'] = `
https://download.cypress.io/desktop/0.20.2/OS/ARCH/cypress-0.20.2-OS-ARCH.zip?referrer=https://download.cypress.io/desktop/0.20.2&version=0.20.2
`
exports['desktop url from template with escaped dollar sign'] = `
https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip
`
exports['desktop url from template wrapped in quote'] = `
https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip
`
exports['desktop url from template with escaped dollar sign wrapped in quote'] = `
https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip
`
exports['base url from CYPRESS_DOWNLOAD_MIRROR 1'] = `
https://cypress.example.com/desktop/0.20.2?platform=OS&arch=ARCH
`
exports['base url from CYPRESS_DOWNLOAD_MIRROR with trailing slash 1'] = `
https://cypress.example.com/desktop/0.20.2?platform=OS&arch=ARCH
`
exports['base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory 1'] = `
https://cypress.example.com/example/desktop/0.20.2?platform=OS&arch=ARCH
`
exports['base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory and trailing slash 1'] = `
https://cypress.example.com/example/desktop/0.20.2?platform=OS&arch=ARCH
`

View File

@@ -1,102 +0,0 @@
exports['errors individual has the following errors 1'] = [
'CYPRESS_RUN_BINARY',
'binaryNotExecutable',
'childProcessKilled',
'failedDownload',
'failedUnzip',
'failedUnzipWindowsMaxPathLength',
'incompatibleHeadlessFlags',
'incompatibleTestTypeFlags',
'incompatibleTestingTypeAndFlag',
'invalidCacheDirectory',
'invalidConfigFile',
'invalidCypressEnv',
'invalidOS',
'invalidRunProjectPath',
'invalidSmokeTestDisplayError',
'invalidTestingType',
'missingApp',
'missingDependency',
'missingXvfb',
'nonZeroExitCodeXvfb',
'notInstalledCI',
'smokeTestFailure',
'unexpected',
'unknownError',
'versionMismatch',
]
exports['child kill error object'] = {
'description': 'The Test Runner unexpectedly exited via a exit event with signal SIGKILL',
'solution': 'Please search Cypress documentation for possible solutions:\n\n https://on.cypress.io\n\nCheck if there is a GitHub issue describing this crash:\n\n https://github.com/cypress-io/cypress/issues\n\nConsider opening a new issue.',
}
exports['Error message'] = `
The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: test platform-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['errors .errors.formErrorText returns fully formed text message 1'] = `
Your system is missing the dependency: Xvfb
Install Xvfb and run Cypress again.
Read our documentation on dependencies for more information:
https://on.cypress.io/required-dependencies
If you are using Docker, we provide containers with all required dependencies installed.
----------
Platform: test platform-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['errors .errors.formErrorText calls solution if a function 1'] = `
description
a solution
----------
Platform: test platform-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['invalid display error'] = `
Cypress verification failed.
Cypress failed to start after spawning a new Xvfb server.
The error logs we received were:
----------
current message
----------
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error above for more detail.
----------
Platform: test platform-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`

View File

@@ -1,321 +0,0 @@
exports['silent install 1'] = `
[no output]
`
exports['error when installing on unsupported os'] = `
Error: The Cypress App could not be installed. Your machine does not meet the operating system requirements.
https://on.cypress.io/app/get-started/install-cypress#System-requirements
----------
Platform: win32-ia32
`
exports['skip installation 1'] = `
Note: Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.
`
exports['/lib/tasks/install .start non-stable builds logs a warning about installing a pre-release 1'] = `
⚠ Warning: You are installing a pre-release build of Cypress.
Bugs may be present which do not exist in production builds.
This build was created from:
* Commit SHA: 3b7f0b5c59def1e9b5f385bd585c9b2836706c29
* Commit Branch: aBranchName
* Commit Timestamp: 1996-11-27Txx:xx:xx.000Z
Installing Cypress (version: https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-3b7f0b5c59def1e9b5f385bd585c9b2836706c29/cypress.zip)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['specify version in env vars 1'] = `
⚠ Warning: Forcing a binary version different than the default.
The CLI expected to install version: 1.2.3
Instead we will install version: 0.12.1
These versions may not work properly together.
Installing Cypress (version: 0.12.1)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['version already installed - cypress install 1'] = `
Cypress 1.2.3 is installed in /cache/Cypress/1.2.3
Skipping installation:
Pass the --force option if you'd like to reinstall anyway.
`
exports['version already installed - postInstall 1'] = `
Cypress 1.2.3 is installed in /cache/Cypress/1.2.3
`
exports['continues installing on failure 1'] = `
Installing Cypress (version: 1.2.3)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['installs without existing installation 1'] = `
Installing Cypress (version: 1.2.3)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['installed version does not match needed version 1'] = `
Cypress x.x.x is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['forcing true always installs 1'] = `
Cypress 1.2.3 is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['warning installing as global 1'] = `
Cypress x.x.x is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
⠋ Downloaded Cypress
✔ Downloaded Cypress
✔ Downloaded Cypress
⠋ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Downloaded Cypress
✔ Unzipped Cypress
⠋ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
✔ Downloaded Cypress
✔ Unzipped Cypress
✔ Finished Installation /cache/Cypress/1.2.3
⚠ Warning: It looks like you've installed Cypress globally.
The recommended way to install Cypress is as a devDependency per project.
You should probably run these commands:
- npm uninstall -g cypress
- npm install --save-dev cypress
`
exports['installing in ci 1'] = `
Cypress x.x.x is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
`
exports['invalid cache directory 1'] = `
Error: Cypress cannot write to the cache directory due to file permissions
See discussion and possible solutions at
https://github.com/cypress-io/cypress/issues/1281
----------
Failed to access /invalid/cache/dir:
EACCES: permission denied, mkdir '/invalid'
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`

View File

@@ -1,15 +0,0 @@
exports['stripIndent removes indent from literal string 1'] = `
first line
second line
third line
last line
`
exports['stripIndent can be used with nested message 1'] = `
first line
foo
bar
last line
`

View File

@@ -1,27 +0,0 @@
exports['exec run .processRunOptions passes --browser option 1'] = [
'--run-project',
null,
'--browser',
'test browser',
]
exports['exec run .processRunOptions passes --record option 1'] = [
'--run-project',
null,
'--record',
'my record id',
]
exports['exec run .processRunOptions does not remove --record option when using --browser 1'] = [
'--run-project',
null,
'--browser',
'test browser',
'--record',
'foo',
]
exports['exec run .processRunOptions defaults to e2e testingType 1'] = [
'--run-project',
null,
]

View File

@@ -1,35 +0,0 @@
exports['lib/exec/spawn .start forces colors and streams when supported 1'] = {
'FORCE_COLOR': '1',
'DEBUG_COLORS': '1',
'MOCHA_COLORS': '1',
'FORCE_STDIN_TTY': '1',
'FORCE_STDOUT_TTY': '1',
'FORCE_STDERR_TTY': '1',
}
exports['lib/exec/spawn .start does not force colors and streams when not supported 1'] = {
'FORCE_COLOR': '0',
'DEBUG_COLORS': '0',
'FORCE_STDIN_TTY': '0',
'FORCE_STDOUT_TTY': '0',
'FORCE_STDERR_TTY': '0',
}
exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = `
The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 0.0.0-development
`

View File

@@ -1,27 +0,0 @@
exports['others_unchanged 1'] = {
'foo': 'bar',
}
exports['env_as_string 1'] = {
'env': 'foo=bar',
}
exports['env_as_object 1'] = {
'env': '{"foo":"bar","magicNumber":1234,"host":"kevin.dev.local"}',
}
exports['config_as_object 1'] = {
'config': '{"baseUrl":"http://localhost:2000","watchForFileChanges":false}',
}
exports['reporter_options_as_object 1'] = {
'reporterOptions': '{"mochaFile":"results/my-test-output.xml","toConsole":true}',
}
exports['spec_as_array 1'] = {
'spec': '["a","b","c"]',
}
exports['spec_as_string 1'] = {
'spec': 'x,y,z',
}

View File

@@ -1,383 +0,0 @@
exports['Cypress non-executable permissions 1'] = `
Error: Cypress cannot run because this binary file does not have executable permissions here:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
Reasons this may happen:
- node was installed as 'root' or with 'sudo'
- the cypress npm package as 'root' or with 'sudo'
Please check that you have the appropriate user permissions.
You can also try clearing the cache with 'cypress cache clear' and reinstalling.
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['current version has not been verified 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
`
exports['darwin: error when invalid CYPRESS_RUN_BINARY 1'] = `
Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/
This overrides the default Cypress binary path used.
Error: Could not run binary set by environment variable: CYPRESS_RUN_BINARY=/custom/
Ensure the environment variable is a path to the Cypress binary, matching **/Contents/MacOS/Cypress
----------
ENOENT: no such file or directory, stat '/custom/'
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['different version installed 1'] = `
Found binary version 7.8.9 installed in: /cache/Cypress/1.2.3/Cypress.app
⚠ Warning: Binary version 7.8.9 does not match the expected package version 1.2.3
These versions may not work properly together.
It looks like this is your first time using Cypress: 7.8.9
Opening Cypress...
`
exports['error binary not found in ci 1'] = `
Error: The cypress npm package is installed, but the Cypress binary is missing.
We expected the binary to be installed here: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
Reasons it may be missing:
- You're caching 'node_modules' but are not caching this path: /cache/Cypress
- You ran 'npm install' at an earlier build step but did not persist: /cache/Cypress
Properly caching the binary will fix this error and avoid downloading and unzipping Cypress.
Alternatively, you can run 'cypress install' to download the binary again.
https://on.cypress.io/not-installed-ci-error
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['executable cannot be found 1'] = `
Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app
Please reinstall Cypress by running: cypress install
----------
Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['fails verifying Cypress 1'] = `
Error: Cypress failed to start.
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error below for more details.
----------
an error about dependencies
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['fails with no stderr 1'] = `
Error: Cypress failed to start.
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error below for more details.
----------
Error: EPERM NOT PERMITTED
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['lib/tasks/verify logs error when child process hangs 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Error: Cypress verification timed out.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222
----------
some stderr
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['lib/tasks/verify logs error when child process returns incorrect stdout (stderr when exists) 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Error: Cypress verification failed.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222
----------
some stderr
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['lib/tasks/verify logs error when child process returns incorrect stdout (stdout when no stderr) 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Error: Cypress verification failed.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222
----------
some stdout
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['linux: error when invalid CYPRESS_RUN_BINARY 1'] = `
Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/
This overrides the default Cypress binary path used.
Error: Could not run binary set by environment variable: CYPRESS_RUN_BINARY=/custom/
Ensure the environment variable is a path to the Cypress binary, matching **/Cypress
----------
ENOENT: no such file or directory, stat '/custom/'
----------
Platform: linux-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['no Cypress executable 1'] = `
Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app
Please reinstall Cypress by running: cypress install
----------
Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['no version of Cypress installed 1'] = `
Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app
Please reinstall Cypress by running: cypress install
----------
Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['no welcome message 1'] = `
Found binary version 7.8.9 installed in: /cache/Cypress/1.2.3/Cypress.app
⚠ Warning: Binary version 7.8.9 does not match the expected package version 1.2.3
These versions may not work properly together.
`
exports['silent verify 1'] = `
[no output]
`
exports['valid CYPRESS_RUN_BINARY 1'] = `
Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/Contents/MacOS/Cypress
This overrides the default Cypress binary path used.
It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
`
exports['verbose stdout output 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
`
exports['verification with executable 1'] = `
Opening Cypress...
`
exports['verifying in ci 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
`
exports['warning installed version does not match verified version 1'] = `
Found binary version bloop installed in: /cache/Cypress/1.2.3/Cypress.app
⚠ Warning: Binary version bloop does not match the expected package version 1.2.3
These versions may not work properly together.
`
exports['win32: error when invalid CYPRESS_RUN_BINARY 1'] = `
Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/
This overrides the default Cypress binary path used.
Error: Could not run binary set by environment variable: CYPRESS_RUN_BINARY=/custom/
Ensure the environment variable is a path to the Cypress binary, matching **/Cypress.exe
----------
ENOENT: no such file or directory, stat '/custom/'
----------
Platform: win32-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['xvfb fails 1'] = `
It looks like this is your first time using Cypress: 1.2.3
Error: Xvfb exited with a non zero exit code.
There was a problem spawning Xvfb.
This is likely a problem with your system, permissions, or installation of Xvfb.
----------
Error: test without xvfb
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['tried to verify twice, on the first try got the DISPLAY error'] = `
Cypress verification failed.
Cypress failed to start after spawning a new Xvfb server.
The error logs we received were:
----------
[some noise here] Gtk: cannot open display: 987
some other error
again with
some weird indent
----------
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error above for more detail.
----------
Platform: linux-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
const CLI = require('../lib/cli').default
const CLI = require('../dist/cli').default
CLI.init()

View File

@@ -11,7 +11,7 @@ import cache from './tasks/cache'
import openModule from './exec/open'
import runModule from './exec/run'
import verifyModule from './tasks/verify'
import { start } from './tasks/verify'
import installModule from './tasks/install'
import versionModule from './exec/versions'
import infoModule from './exec/info'
@@ -27,7 +27,7 @@ function unknownOption (this: any, flag: string, type: string = 'option'): void
logger.error(` error: unknown ${type}:`, flag)
logger.error()
this.outputHelp()
util.exit(1)
process.exit(1)
}
commander.Command.prototype.unknownOption = unknownOption
@@ -168,7 +168,7 @@ function includesVersion (args: string[]): boolean {
)
}
function showVersions (opts: any): any {
async function showVersions (opts: any): Promise<any> {
debug('printing Cypress version')
debug('additional arguments %o', opts)
@@ -211,9 +211,9 @@ function showVersions (opts: any): any {
electronNodeVersion: undefined,
}
return versionModule
.getVersions()
.then((versions: any = defaultVersions) => {
try {
const versions = (await versionModule.getVersions()) || defaultVersions
if (opts?.component) {
reportComponentVersion(opts.component, versions)
} else {
@@ -221,8 +221,9 @@ function showVersions (opts: any): any {
}
process.exit(0)
})
.catch(util.logErrorExit1)
} catch (e: any) {
util.logErrorExit1(e)
}
}
const createProgram = (): any => {
@@ -403,7 +404,7 @@ const cliModule = {
/**
* Parses the command line and kicks off Cypress process.
*/
init (args?: string[]): any {
async init (args?: string[]): Promise<any> {
if (!args) {
args = process.argv
}
@@ -472,21 +473,28 @@ const cliModule = {
.description(text('version')))
maybeAddInspectFlags(addCypressOpenCommand(program))
.action((opts: any) => {
.action(async (opts: any) => {
debug('opening Cypress')
openModule.start(util.parseOpts(opts))
.then(util.exit)
.catch(util.logErrorExit1)
try {
const code = await openModule.start(util.parseOpts(opts))
process.exit(code)
} catch (e: any) {
util.logErrorExit1(e)
}
})
maybeAddInspectFlags(addCypressRunCommand(program))
.action((...fnArgs: any[]) => {
.action(async (...fnArgs: any[]) => {
debug('running Cypress with args %o', fnArgs)
try {
const code = await runModule.start(parseVariableOpts(fnArgs, args as string[]))
runModule.start(parseVariableOpts(fnArgs, args as string[]))
.then(util.exit)
.catch(util.logErrorExit1)
process.exit(code)
} catch (e: any) {
util.logErrorExit1(e)
}
})
program
@@ -496,10 +504,12 @@ const cliModule = {
'Installs the Cypress executable matching this package\'s version',
)
.option('-f, --force', text('forceInstall'))
.action((opts: any) => {
installModule
.start(util.parseOpts(opts))
.catch(util.logErrorExit1)
.action(async (opts: any) => {
try {
await installModule.start(util.parseOpts(opts))
} catch (e: any) {
util.logErrorExit1(e)
}
})
program
@@ -509,14 +519,16 @@ const cliModule = {
'Verifies that Cypress is installed correctly and executable',
)
.option('--dev', text('dev'), coerceFalse)
.action((opts: any) => {
.action(async (opts: any) => {
const defaultOpts = { force: true, welcomeMessage: false }
const parsedOpts = util.parseOpts(opts)
const options = _.extend(parsedOpts, defaultOpts)
verifyModule
.start(options)
.catch(util.logErrorExit1)
try {
await start(options)
} catch (e: any) {
util.logErrorExit1(e)
}
})
program
@@ -528,10 +540,10 @@ const cliModule = {
.option('clear', text('cacheClear'))
.option('prune', text('cachePrune'))
.option('--size', text('cacheSize'))
.action(function (this: any, opts: any, args: string[]) {
.action(async function (this: any, opts: any, args: string[]) {
if (!args || !args.length) {
this.outputHelp()
util.exit(1)
process.exit(1)
}
const [command] = args
@@ -546,16 +558,18 @@ const cliModule = {
size: opts.size,
})
return cache.list(opts.size)
.catch({ code: 'ENOENT' }, () => {
logger.always('No cached binary versions were found.')
process.exit(0)
})
.catch((e: Error) => {
debug('cache list command failed with "%s"', e.message)
try {
const result = await cache.list(opts.size)
return result
} catch (e: any) {
if (e.code === 'ENOENT') {
logger.always('No cached binary versions were found.')
process.exit(0)
}
util.logErrorExit1(e)
})
}
}
cache[command]()
@@ -566,11 +580,14 @@ const cliModule = {
.usage('[command]')
.description('Prints Cypress and system information')
.option('--dev', text('dev'), coerceFalse)
.action((opts: any) => {
infoModule
.start(opts)
.then(util.exit)
.catch(util.logErrorExit1)
.action(async (opts: any) => {
try {
const code = await infoModule.start(opts)
process.exit(code)
} catch (e: any) {
util.logErrorExit1(e)
}
})
debug('cli starts with arguments %j', args)
@@ -590,7 +607,7 @@ const cliModule = {
logger.error('Unknown command', `"${firstCommand}"`)
program.outputHelp()
return util.exit(1)
return process.exit(1)
}
if (includesVersion(args)) {
@@ -608,11 +625,3 @@ const cliModule = {
}
export default cliModule
// @ts-ignore
if (!module.parent) {
logger.error('This CLI module should be required from another Node module')
logger.error('and not executed directly')
process.exit(-1)
}

View File

@@ -1,15 +1,11 @@
// https://github.com/cypress-io/cypress/issues/316
import Bluebird from 'bluebird'
import tmpModule from 'tmp'
import fs from './fs'
import tmp from 'tmp'
import fs from 'fs-extra'
import openModule from './exec/open'
import runModule from './exec/run'
import util from './util'
import cli from './cli'
const tmp = Bluebird.promisifyAll(tmpModule) as any
const cypressModuleApi = {
/**
* Opens Cypress GUI
@@ -25,35 +21,30 @@ const cypressModuleApi = {
* Runs Cypress tests in the current project
* @see https://on.cypress.io/module-api#cypress-run
*/
run (options: any = {}): any {
async run (options: any = {}): Promise<any> {
if (!runModule.isValidProject(options.project)) {
return Bluebird.reject(new Error(`Invalid project path parameter: ${options.project}`))
throw new Error(`Invalid project path parameter: ${options.project}`)
}
options = util.normalizeModuleOptions(options)
tmp.setGracefulCleanup()
return tmp.fileAsync()
.then((outputPath: string) => {
options.outputPath = outputPath
const outputPath: string = tmp.fileSync().name
return runModule.start(options)
.then((failedTests: any) => {
return fs.readJsonAsync(outputPath, { throws: false })
.then((output: any) => {
if (!output) {
return {
status: 'failed',
failures: failedTests,
message: 'Could not find Cypress test run results',
}
}
options.outputPath = outputPath
return output
})
})
})
const failedTests = await runModule.start(options)
const output = await fs.readJson(outputPath, { throws: false })
if (!output) {
return {
status: 'failed',
failures: failedTests,
message: 'Could not find Cypress test run results',
}
}
return output
},
cli: {

View File

@@ -1,10 +1,12 @@
import chalk from 'chalk'
import { stripIndent, stripIndents } from 'common-tags'
import la from 'lazy-ass'
import is from 'check-more-types'
import util from './util'
import state from './tasks/state'
// TODO: this package needs to be replaced as we can't import it in vitest
const is = require('check-more-types')
const docsUrl = 'https://on.cypress.io'
const requiredDependenciesUrl = `${docsUrl}/required-dependencies`
const runDocumentationUrl = `${docsUrl}/cypress-run`
@@ -266,10 +268,10 @@ const CYPRESS_RUN_BINARY = {
},
}
function addPlatformInformation (info: any): any {
return util.getPlatformInfo().then((platform: string) => {
return { ...info, platform }
})
async function addPlatformInformation (info: any): Promise<any> {
const platform = await util.getPlatformInfo()
return { ...info, platform }
}
/**
@@ -284,88 +286,88 @@ function addPlatformInformation (info: any): any {
return getError(errorObject).then(reject)
```
*/
export function getError (errorObject: any): Promise<Error> {
return formErrorText(errorObject).then((errorMessage: string) => {
const err: any = new Error(errorMessage)
export async function getError (errorObject: any): Promise<Error> {
const errorMessage = await formErrorText(errorObject)
err.known = true
const err: any = new Error(errorMessage)
return err
})
err.known = true
return err
}
/**
* Forms nice error message with error and platform information,
* and if possible a way to solve it. Resolves with a string.
*/
export function formErrorText (info: any, msg?: string, prevMessage?: string): any {
return addPlatformInformation(info).then((obj: any) => {
const formatted: string[] = []
export async function formErrorText (info: any, msg?: string, prevMessage?: string): Promise<string> {
const infoWithPlatform = await addPlatformInformation(info)
function add (msg: string): void {
formatted.push(stripIndents(msg))
}
const formatted: string[] = []
la(
is.unemptyString(obj.description),
'expected error description to be text',
obj.description,
)
function add (msg: string): void {
formatted.push(stripIndents(msg))
}
// assuming that if there the solution is a function it will handle
// error message and (optional previous error message)
if (is.fn(obj.solution)) {
const text = obj.solution(msg, prevMessage)
la(
is.unemptyString(infoWithPlatform.description),
'expected error description to be text',
infoWithPlatform.description,
)
la(is.unemptyString(text), 'expected solution to be text', text)
// assuming that if there the solution is a function it will handle
// error message and (optional previous error message)
if (is.fn(infoWithPlatform.solution)) {
const text = infoWithPlatform.solution(msg, prevMessage)
add(`
${obj.description}
la(is.unemptyString(text), 'expected solution to be text', text)
add(`
${infoWithPlatform.description}
${text}
`)
} else {
la(
is.unemptyString(obj.solution),
'expected error solution to be text',
obj.solution,
)
} else {
la(
is.unemptyString(infoWithPlatform.solution),
'expected error solution to be text',
infoWithPlatform.solution,
)
add(`
${obj.description}
add(`
${infoWithPlatform.description}
${obj.solution}
${infoWithPlatform.solution}
`)
if (msg) {
add(`
if (msg) {
add(`
${hr}
${msg}
`)
}
}
}
add(`
add(`
${hr}
${obj.platform}
${infoWithPlatform.platform}
`)
if (obj.footer) {
add(`
if (infoWithPlatform.footer) {
add(`
${hr}
${obj.footer}
${infoWithPlatform.footer}
`)
}
}
return formatted.join('\n\n')
})
return formatted.join('\n\n')
}
export const raise = (info: any) => {
@@ -382,8 +384,10 @@ export const raise = (info: any) => {
}
export const throwFormErrorText = (info: any) => {
return (msg?: string, prevMessage?: string) => {
return formErrorText(info, msg, prevMessage).then(raise(info))
return async (msg?: string, prevMessage?: string) => {
const errorText = await formErrorText(info, msg, prevMessage)
raise(info)(errorText)
}
}
@@ -394,12 +398,12 @@ export const throwFormErrorText = (info: any) => {
* @example return exitWithError(errors.invalidCypressEnv)('foo')
*/
export const exitWithError = (info: any) => {
return (msg?: string) => {
return formErrorText(info, msg).then((text: string) => {
// eslint-disable-next-line no-console
console.error(text)
process.exit(info.exitCode || 1)
})
return async (msg?: string) => {
const text: string = await formErrorText(info, msg)
// eslint-disable-next-line no-console
console.error(text)
process.exit(info.exitCode || 1)
}
}

View File

@@ -1,7 +1,7 @@
import Debug from 'debug'
import util from '../util'
import spawn from './spawn'
import verifyModule from '../tasks/verify'
import { start as verifyStart } from '../tasks/verify'
import { processTestingType, checkConfigFile } from './shared'
import { exitWithError } from '../errors'
@@ -74,7 +74,7 @@ export const processOpenOptions = (options: any = {}): string[] => {
return args
}
export const start = (options: any = {}): any => {
export const start = async (options: any = {}): Promise<any> => {
function open (): any {
try {
const args = processOpenOptions(options)
@@ -96,8 +96,9 @@ export const start = (options: any = {}): any => {
return open()
}
return verifyModule.start()
.then(open)
await verifyStart()
return open()
}
export default {

View File

@@ -2,7 +2,7 @@ import _ from 'lodash'
import Debug from 'debug'
import util from '../util'
import spawn from './spawn'
import verifyModule from '../tasks/verify'
import { start } from '../tasks/verify'
import { exitWithError, errors } from '../errors'
import { processTestingType, throwInvalidOptionError, checkConfigFile } from './shared'
@@ -164,7 +164,7 @@ const runModule = {
processRunOptions,
isValidProject,
// resolves with the number of failed tests
start (options: any = {}): any {
async start (options: any = {}): Promise<any> {
_.defaults(options, {
key: null,
spec: null,
@@ -195,8 +195,9 @@ const runModule = {
return run()
}
return verifyModule.start()
.then(run)
await start()
return run()
},
}

View File

@@ -7,9 +7,10 @@ import Debug from 'debug'
import util from '../util'
import state from '../tasks/state'
import xvfb from './xvfb'
import verifyModule from '../tasks/verify'
import { needsSandbox } from '../tasks/verify'
import { throwFormErrorText, getError, errors } from '../errors'
import readline from 'readline'
import { stdin, stdout, stderr } from 'process'
const debug = Debug('cypress:cli')
@@ -50,7 +51,7 @@ function getStdio (needsXvfb: boolean): any {
}
const spawnModule = {
start (args: any, options: any = {}): any {
async start (args: any, options: any = {}): Promise<any> {
const needsXvfb = xvfb.isNeeded()
let executable = state.getPathToExecutable(state.getBinaryDir())
@@ -99,7 +100,7 @@ const spawnModule = {
debug('in dev mode the args became %o', args)
}
if (!options.dev && verifyModule.needsSandbox()) {
if (!options.dev && needsSandbox()) {
electronArgs.push('--no-sandbox')
}
@@ -149,13 +150,15 @@ const spawnModule = {
const child = cp.spawn(executable, args, stdioOptions)
function resolveOn (event: any): any {
return function (code: any, signal: any): any {
return async function (code: any, signal: any): Promise<any> {
debug('child event fired %o', { event, code, signal })
if (code === null) {
const errorObject = errors.childProcessKilled(event, signal)
return getError(errorObject).then(reject)
const err = await getError(errorObject)
return reject(err)
}
resolve(code)
@@ -168,8 +171,8 @@ const spawnModule = {
if (isPlatform('win32')) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
input: stdin,
output: stdout,
})
// on windows, SIGINT does not propagate to the child process when ctrl+c is pressed
@@ -188,12 +191,12 @@ const spawnModule = {
// child STDERR => process STDERR with additional filtering
if (child.stdin) {
debug('piping process STDIN into child STDIN')
process.stdin.pipe(child.stdin)
stdin.pipe(child.stdin)
}
if (child.stdout) {
debug('piping child STDOUT to process STDOUT')
child.stdout.pipe(process.stdout)
child.stdout.pipe(stdout)
}
// if this is defined then we are manually piping for linux
@@ -210,7 +213,7 @@ const spawnModule = {
}
// else pass it along!
process.stderr.write(data)
stderr.write(data)
})
}
@@ -221,7 +224,7 @@ const spawnModule = {
// into the child process. unpiping does not seem
// to have any effect. so we're just catching the
// error here and not doing anything.
process.stdin.on('error', (err: any) => {
stdin.on('error', (err: any) => {
if (['EPIPE', 'ENOTCONN'].includes(err.code)) {
return
}
@@ -235,17 +238,22 @@ const spawnModule = {
})
}
const spawnInXvfb = (): any => {
return xvfb
.start()
.then(userFriendlySpawn)
.finally(xvfb.stop)
const spawnInXvfb = async (): Promise<number> => {
try {
await xvfb.start()
const code = await userFriendlySpawn()
return code
} finally {
await xvfb.stop()
}
}
const userFriendlySpawn = (linuxWithDisplayEnv: any): any => {
const userFriendlySpawn = async (linuxWithDisplayEnv?: any): Promise<any> => {
debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv))
let brokenGtkDisplay: boolean
let brokenGtkDisplay: boolean = false
const overrides: any = {}
@@ -262,8 +270,9 @@ const spawnModule = {
})
}
return spawn(overrides)
.then((code: any) => {
try {
const code: number = await spawn(overrides)
if (code !== 0 && brokenGtkDisplay) {
util.logBrokenGtkDisplayWarning()
@@ -271,10 +280,17 @@ const spawnModule = {
}
return code
})
// we can format and handle an error message from the code above
// prevent wrapping error again by using "known: undefined" filter
.catch({ known: undefined }, throwFormErrorText(errors.unexpected))
} catch (error: any) {
// we can format and handle an error message from the code above
// prevent wrapping error again by using "known: undefined" filter
if ((error as any).known === undefined) {
const raiseErrorFn = throwFormErrorText(errors.unexpected)
await raiseErrorFn(error.message)
}
throw error
}
}
if (needsXvfb) {

View File

@@ -1,4 +1,3 @@
import Bluebird from 'bluebird'
import Debug from 'debug'
import path from 'path'
import util from '../util'
@@ -7,59 +6,62 @@ import { throwFormErrorText, errors } from '../errors'
const debug = Debug('cypress:cli')
const getVersions = (): any => {
return Bluebird.try(() => {
if (util.getEnv('CYPRESS_RUN_BINARY')) {
let envBinaryPath = path.resolve(util.getEnv('CYPRESS_RUN_BINARY') as string)
const getBinaryDirectory = async (): Promise<string> => {
if (util.getEnv('CYPRESS_RUN_BINARY')) {
let envBinaryPath = path.resolve(util.getEnv('CYPRESS_RUN_BINARY') as string)
return state.parseRealPlatformBinaryFolderAsync(envBinaryPath)
.then((envBinaryDir: any) => {
if (!envBinaryDir) {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))()
}
try {
const envBinaryDir = await state.parseRealPlatformBinaryFolderAsync(envBinaryPath)
debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir)
if (!envBinaryDir) {
const raiseErrorFn = throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))
return envBinaryDir
})
.catch({ code: 'ENOENT' }, (err: any) => {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(err.message)
})
await raiseErrorFn()
}
debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir)
return envBinaryDir
} catch (err: any) {
const raiseErrorFn = throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))
await raiseErrorFn(err.message)
}
}
return state.getBinaryDir()
})
.then(state.getBinaryPkgAsync)
.then((pkg: any) => {
const versions = {
binary: state.getBinaryPkgVersion(pkg),
electronVersion: state.getBinaryElectronVersion(pkg),
electronNodeVersion: state.getBinaryElectronNodeVersion(pkg),
}
return state.getBinaryDir()
}
debug('binary versions %o', versions)
const getVersions = async (): Promise<any> => {
const binDir = await getBinaryDirectory()
return versions
})
.then((binaryVersions: any) => {
const buildInfo = util.pkgBuildInfo()
const pkg = await state.getBinaryPkgAsync(binDir)
let packageVersion = util.pkgVersion()
const versions = {
binary: state.getBinaryPkgVersion(pkg),
electronVersion: state.getBinaryElectronVersion(pkg),
electronNodeVersion: state.getBinaryElectronNodeVersion(pkg),
}
if (!buildInfo) packageVersion += ' (development)'
else if (!buildInfo.stable) packageVersion += ' (pre-release)'
debug('binary versions %o', versions)
const versions = {
package: packageVersion,
binary: binaryVersions.binary || 'not installed',
electronVersion: binaryVersions.electronVersion || 'not found',
electronNodeVersion: binaryVersions.electronNodeVersion || 'not found',
}
const buildInfo = util.pkgBuildInfo()
debug('combined versions %o', versions)
let packageVersion = util.pkgVersion()
return versions
})
if (!buildInfo) packageVersion += ' (development)'
else if (!buildInfo.stable) packageVersion += ' (pre-release)'
const versionsFinal = {
package: packageVersion,
binary: versions.binary || 'not installed',
electronVersion: versions.electronVersion || 'not found',
electronNodeVersion: versions.electronNodeVersion || 'not found',
}
debug('combined versions %o', versions)
return versionsFinal
}
const versionsModule = {

View File

@@ -33,29 +33,40 @@ const xvfbModule = {
_xvfbOptions: xvfbOptions, // expose for testing
start (): any {
async start (): Promise<any> {
debug('Starting Xvfb')
return xvfb.startAsync()
.return(null)
.catch({ nonZeroExitCode: true }, throwFormErrorText(errors.nonZeroExitCodeXvfb))
.catch((err: any) => {
if (err.known) {
throw err
try {
await xvfb.startAsync()
return null
} catch (e: any) {
if (e.nonZeroExitCode === true) {
const raiseErrorFn = throwFormErrorText(errors.nonZeroExitCodeXvfb)
await raiseErrorFn(e)
}
return throwFormErrorText(errors.missingXvfb)(err)
})
if (e.known) {
throw e
}
const raiseErrorFn = throwFormErrorText(errors.missingXvfb)
await raiseErrorFn(e)
}
},
stop (): any {
async stop (): Promise<null> {
debug('Stopping Xvfb')
return xvfb.stopAsync()
.return(null)
.catch(() => {
// noop
})
try {
await xvfb.stopAsync()
return null
} catch (e) {
return null
}
},
isNeeded (): boolean {
@@ -95,15 +106,18 @@ const xvfbModule = {
},
// async method, resolved with Boolean
verify (): any {
return xvfb.startAsync()
.return(true)
.catch((err: any) => {
async verify (): Promise<boolean> {
try {
await xvfb.startAsync()
return true
} catch (err: any) {
debug('Could not verify xvfb: %s', err.message)
return false
})
.finally(xvfb.stopAsync)
} finally {
await xvfb.stopAsync()
}
},
}

View File

@@ -1,4 +0,0 @@
import Bluebird from 'bluebird'
import fsExtra from 'fs-extra'
export default Bluebird.promisifyAll(fsExtra) as any

View File

@@ -2,7 +2,7 @@ import module from 'module'
const require = module.createRequire(import.meta.url)
const cypress = require('./lib/cypress')
const cypress = require('./cypress')
export default cypress

View File

@@ -1,9 +1,9 @@
import minimist from 'minimist'
import debug from 'debug'
import util from './lib/util'
import CLI from './lib/cypress'
import installModule from './lib/tasks/install'
import verifyModule from './lib/tasks/verify'
import util from './util'
import CLI from './cypress'
import installModule from './tasks/install'
import { start as verifyStart } from './tasks/verify'
const debugCli = debug('cypress:cli')
const args: any = minimist(process.argv.slice(2))
@@ -23,7 +23,7 @@ async function handleExec (): Promise<void> {
// for simple testing in the monorepo
debugCli('verifying Cypress')
verifyModule.start({ force: true }) // always force verification
verifyStart({ force: true }) // always force verification
.catch(util.logErrorExit1)
break

View File

@@ -1,6 +1,6 @@
import state from './state'
import logger from '../logger'
import fs from '../fs'
import fs from 'fs-extra'
import util from '../util'
import { join } from 'path'
@@ -28,7 +28,7 @@ const logCachePath = (): undefined => {
}
const clear = (): Promise<void> => {
return fs.removeAsync(state.getCacheDir())
return fs.remove(state.getCacheDir())
}
const prune = async (): Promise<void> => {
@@ -38,7 +38,7 @@ const prune = async (): Promise<void> => {
let deletedBinary = false
try {
const versions = await fs.readdirAsync(cacheDir)
const versions = await fs.readdir(cacheDir)
for (const version of versions) {
if (version !== checkedInBinaryVersion) {
@@ -46,7 +46,7 @@ const prune = async (): Promise<void> => {
const versionDir = join(cacheDir, version)
await fs.removeAsync(versionDir)
await fs.remove(versionDir)
}
}
@@ -74,88 +74,98 @@ const fileSizeInMB = (size: number): string => {
* Collects all cached versions, finds when each was used
* and prints a table with results to the terminal
*/
const list = (showSize: boolean = false): any => {
return getCachedVersions(showSize)
.then((binaries: any) => {
const head = [colors.titles('version'), colors.titles('last used')]
const list = async (showSize: boolean = false): Promise<void> => {
const binaries = await getCachedVersions(showSize)
const head = [colors.titles('version'), colors.titles('last used')]
if (showSize) {
head.push(colors.titles('size'))
}
const table = new Table({
head,
})
binaries.forEach((binary: { version: string, accessed?: string, size?: number }) => {
const versionString = colors.values(binary.version)
const lastUsed = binary.accessed ? colors.dates(binary.accessed) : 'unknown'
const row = [versionString, lastUsed]
if (showSize) {
head.push(colors.titles('size'))
const size = colors.size(fileSizeInMB(binary.size as number))
row.push(size)
}
const table = new Table({
head,
})
binaries.forEach((binary: any) => {
const versionString = colors.values(binary.version)
const lastUsed = binary.accessed ? colors.dates(binary.accessed) : 'unknown'
const row = [versionString, lastUsed]
if (showSize) {
const size = colors.size(fileSizeInMB(binary.size))
row.push(size)
}
return table.push(row)
})
logger.always(table.toString())
return table.push(row)
})
logger.always(table.toString())
}
const getCachedVersions = (showSize: boolean): Promise<any> => {
const getCachedVersions = async (showSize: boolean): Promise<{
version: string
folderPath: string
accessed?: string
size?: number
}[]> => {
const cacheDir = state.getCacheDir()
return fs
.readdirAsync(cacheDir)
.filter(util.isSemver)
.map((version: any) => {
const versions = await fs.readdir(cacheDir)
const filteredVersions = versions.filter(util.isSemver).map((version: any) => {
return {
version,
folderPath: join(cacheDir, version),
}
})
.mapSeries((binary: any) => {
// last access time on the folder is different from last access time
// on the Cypress binary
const binaries: {
version: string
folderPath: string
accessed?: string
size?: number
}[] = []
for (const binary of filteredVersions) {
const binaryDir = state.getBinaryDir(binary.version)
const executable = state.getPathToExecutable(binaryDir)
return fs.statAsync(executable).then((stat: any) => {
try {
const stat = await fs.stat(executable)
const lastAccessedTime = _.get(stat, 'atime')
if (!lastAccessedTime) {
// the test runner has never been opened
// or could be a test simulating missing timestamp
return binary
if (lastAccessedTime) {
const accessed = dayjs(lastAccessedTime).fromNow()
// @ts-expect-error - accessed is not defined in the type
binary.accessed = accessed
}
const accessed = dayjs(lastAccessedTime).fromNow()
binary.accessed = accessed
return binary
}, (e: any) => {
// if no lastAccessedTime
// the test runner has never been opened
// or could be a test simulating missing timestamp
} catch (e) {
// could not find the binary or gets its stats
return binary
})
})
.mapSeries((binary: any) => {
// no-op
}
if (showSize) {
const binaryDir = state.getBinaryDir(binary.version)
return getFolderSize(binaryDir).then((size: number) => {
return {
...binary,
size,
}
})
}
const size: number = await getFolderSize(binaryDir)
return binary
})
binaries.push({
...binary,
size,
})
} else {
binaries.push(binary)
}
}
return binaries
}
const cacheModule = {

View File

@@ -1,5 +1,4 @@
import la from 'lazy-ass'
import is from 'check-more-types'
import os from 'os'
import url from 'url'
import path from 'path'
@@ -10,9 +9,12 @@ import requestProgress from 'request-progress'
import { stripIndent } from 'common-tags'
import { getProxyForUrl } from 'proxy-from-env'
import { throwFormErrorText, errors } from '../errors'
import fs from '../fs'
import fs from 'fs-extra'
import util from '../util'
// TODO: this package needs to be replaced as we can't import it in vitest
const is = require('check-more-types')
const debug = Debug('cypress:cli')
const defaultBaseUrl = 'https://download.cypress.io/'
@@ -39,22 +41,24 @@ const getBaseUrl = (): string => {
return defaultBaseUrl
}
const getCA = (): any => {
return new Bluebird((resolve: any) => {
if (process.env.npm_config_cafile) {
fs.readFile(process.env.npm_config_cafile, 'utf8')
.then((cafileContent: string) => {
resolve(cafileContent)
})
.catch(() => {
resolve()
})
} else if (process.env.npm_config_ca) {
resolve(process.env.npm_config_ca)
} else {
resolve()
const getCA = async (): Promise<string | undefined> => {
if (process.env.npm_config_cafile) {
try {
const caFileContent = await fs.readFile(process.env.npm_config_cafile, 'utf8')
return caFileContent
} catch (error) {
debug('error reading ca file', error)
return
}
})
}
if (process.env.npm_config_ca) {
return process.env.npm_config_ca
}
return
}
const prepend = (arch: string, urlPath: string, version: string): string => {
@@ -79,16 +83,16 @@ const prepend = (arch: string, urlPath: string, version: string): string => {
: `${endpoint}?platform=${platform}&arch=${arch}`
}
const getUrl = (arch: string, version: string): string => {
const getUrl = (arch: string, version?: string): string => {
if (is.webUrl(version)) {
debug('version is already an url', version)
return version
return version as string
}
const urlPath = version ? `desktop/${version}` : 'desktop'
return prepend(arch, urlPath, version)
return prepend(arch, urlPath, version || '')
}
const statusMessage = (err: any): string => {
@@ -112,7 +116,7 @@ const prettyDownloadErr = (err: any, url: string): any => {
* Checks checksum and file size for the given file. Allows both
* values or just one of them to be checked.
*/
const verifyDownloadedFile = (filename: string, expectedSize?: number, expectedChecksum?: string): any => {
const verifyDownloadedFile = async (filename: string, expectedSize?: number, expectedChecksum?: string): Promise<any> => {
if (expectedSize && expectedChecksum) {
debug('verifying checksum and file size')
@@ -147,24 +151,23 @@ const verifyDownloadedFile = (filename: string, expectedSize?: number, expectedC
if (expectedChecksum) {
debug('only checking expected file checksum %d', expectedChecksum)
return util.getFileChecksum(filename)
.then((checksum: string) => {
if (checksum === expectedChecksum) {
debug('downloaded file has the expected checksum ✅')
const checksum: string = await util.getFileChecksum(filename)
return
}
if (checksum === expectedChecksum) {
debug('downloaded file has the expected checksum ✅')
debug('raising error: file checksum mismatch')
const text = stripIndent`
Corrupted download
return
}
Expected downloaded file to have checksum: ${expectedChecksum}
Computed checksum: ${checksum}
`
debug('raising error: file checksum mismatch')
const text = stripIndent`
Corrupted download
throw new Error(text)
})
Expected downloaded file to have checksum: ${expectedChecksum}
Computed checksum: ${checksum}
`
throw new Error(text)
}
if (expectedSize) {
@@ -172,29 +175,28 @@ const verifyDownloadedFile = (filename: string, expectedSize?: number, expectedC
// which we can check against the file size
debug('only checking expected file size %d', expectedSize)
return util.getFileSize(filename)
.then((filesize: number) => {
if (filesize === expectedSize) {
debug('downloaded file has the expected size ✅')
const filesize: number = await util.getFileSize(filename)
return
}
if (filesize === expectedSize) {
debug('downloaded file has the expected size ✅')
debug('raising error: file size mismatch')
const text = stripIndent`
Corrupted download
return
}
Expected downloaded file to have size: ${expectedSize}
Computed size: ${filesize}
`
debug('raising error: file size mismatch')
const text = stripIndent`
Corrupted download
throw new Error(text)
})
Expected downloaded file to have size: ${expectedSize}
Computed size: ${filesize}
`
throw new Error(text)
}
debug('downloaded file lacks checksum or size to verify')
return Bluebird.resolve()
return
}
// downloads from given url
@@ -354,18 +356,16 @@ const start = async (opts: any): Promise<any> => {
debug('source url %s', versionUrl)
debug(`downloading cypress.zip to "${downloadDestination}"`)
// ensure download dir exists
return fs.ensureDirAsync(path.dirname(downloadDestination))
.then(() => {
return getCA()
})
.then((ca: any) => {
try {
// ensure download dir exists
await fs.ensureDir(path.dirname(downloadDestination))
const ca: string | undefined = await getCA()
return downloadFromUrl({ url: versionUrl, downloadDestination, progress, ca, version,
...(redirectTTL ? { redirectTTL } : {}) })
})
.catch((err: any) => {
} catch (err: any) {
return prettyDownloadErr(err, versionUrl)
})
}
}
const downloadModule = {

View File

@@ -1,4 +1,4 @@
import fs from '../fs'
import fs from 'fs-extra'
import { join } from 'path'
import Bluebird from 'bluebird'

View File

@@ -4,10 +4,11 @@ import path from 'path'
import chalk from 'chalk'
import Debug from 'debug'
import { Listr } from 'listr2'
import Bluebird from 'bluebird'
import logSymbols from 'log-symbols'
import { stripIndent } from 'common-tags'
import fs from '../fs'
import timers from 'timers/promises'
import fs from 'fs-extra'
import download from './download'
import util from '../util'
import state from './state'
@@ -92,24 +93,22 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }: any): any => {
const tasks = new Listr([
{
options: { title: util.titleize('Downloading Cypress') },
task: (ctx: any, task: any) => {
task: async (ctx: any, task: any) => {
// as our download progresses indicate the status
progress.onProgress = progessify(task, 'Downloading Cypress')
return download.start({ version, downloadDestination, progress })
.then((redirectVersion: any) => {
if (redirectVersion) version = redirectVersion
const redirectVersion = await download.start({ version, downloadDestination, progress })
debug(`finished downloading file: ${downloadDestination}`)
})
.then(() => {
// save the download destination for unzipping
util.setTaskTitle(
task,
util.titleize(chalk.green('Downloaded Cypress')),
rendererOptions.renderer,
)
})
if (redirectVersion) version = redirectVersion
debug(`finished downloading file: ${downloadDestination}`)
// save the download destination for unzipping
util.setTaskTitle(
task,
util.titleize(chalk.green('Downloaded Cypress')),
rendererOptions.renderer,
)
},
},
unzipTask({
@@ -120,35 +119,34 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }: any): any => {
}),
{
options: { title: util.titleize('Finishing Installation') },
task: (ctx: any, task: any) => {
const cleanup = () => {
task: async (ctx: any, task: any) => {
const cleanup = async () => {
debug('removing zip file %s', downloadDestination)
return fs.removeAsync(downloadDestination)
await fs.remove(downloadDestination)
}
return cleanup()
.then(() => {
debug('finished installation in', installDir)
await cleanup()
util.setTaskTitle(
task,
util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)),
rendererOptions.renderer,
)
})
debug('finished installation in', installDir)
util.setTaskTitle(
task,
util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)),
rendererOptions.renderer,
)
},
},
], { rendererOptions })
// start the tasks!
return Bluebird.resolve(tasks.run())
return tasks.run()
}
const validateOS = (): any => {
return util.getPlatformInfo().then((platformInfo: string) => {
return platformInfo.match(/(win32-x64|win32-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/)
})
const validateOS = async (): Promise<RegExpMatchArray | null> => {
const platformInfo = await util.getPlatformInfo()
return platformInfo.match(/(win32-x64|win32-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/)
}
/**
@@ -245,14 +243,19 @@ const start = async (options: any = {}): Promise<any> => {
return throwFormErrorText(errors.invalidOS)()
}
await fs.ensureDirAsync(cacheDir)
.catch({ code: 'EACCES' }, (err: any) => {
return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent`
Failed to access ${chalk.cyan(cacheDir)}:
try {
await fs.ensureDir(cacheDir)
} catch (err: any) {
if (err.code === 'EACCES') {
return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent`
Failed to access ${chalk.cyan(cacheDir)}:
${err.message}
`)
})
${err.message}
`)
}
throw err
}
const binaryPkg = await state.getBinaryPkgAsync(binaryDir)
const binaryVersion = await state.getBinaryPkgVersion(binaryPkg)
@@ -310,7 +313,7 @@ const start = async (options: any = {}): Promise<any> => {
const getLocalFilePath = async (): Promise<string | false> => {
// see if version supplied is a path to a binary
if (await fs.pathExistsAsync(versionToInstall)) {
if (await fs.pathExists(versionToInstall)) {
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false
}
@@ -320,7 +323,7 @@ const start = async (options: any = {}): Promise<any> => {
// if this exists return the path to it
// else false
if ((await fs.pathExistsAsync(possibleFile)) && path.extname(possibleFile) === '.zip') {
if ((await fs.pathExists(possibleFile)) && path.extname(possibleFile) === '.zip') {
return possibleFile
}
@@ -360,7 +363,7 @@ const start = async (options: any = {}): Promise<any> => {
await downloadAndUnzip({ version: versionToInstall, installDir, downloadDir })
// delay 1 sec for UX, unless we are testing
await Bluebird.delay(1000)
await timers.setTimeout(1000)
displayCompletionMsg()
}
@@ -368,18 +371,16 @@ const start = async (options: any = {}): Promise<any> => {
const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }: any): any => {
return {
options: { title: util.titleize('Unzipping Cypress') },
task: (ctx: any, task: any) => {
task: async (ctx: any, task: any) => {
// as our unzip progresses indicate the status
progress.onProgress = progessify(task, 'Unzipping Cypress')
return unzip.start({ zipFilePath, installDir, progress })
.then(() => {
util.setTaskTitle(
task,
util.titleize(chalk.green('Unzipped Cypress')),
rendererOptions.renderer,
)
})
await unzip.start({ zipFilePath, installDir, progress })
util.setTaskTitle(
task,
util.titleize(chalk.green('Unzipped Cypress')),
rendererOptions.renderer,
)
},
}
}

View File

@@ -3,7 +3,8 @@ import os from 'os'
import path from 'path'
import untildify from 'untildify'
import Debug from 'debug'
import fs from '../fs'
import { cwd } from 'process'
import fs from 'fs-extra'
import util from '../util'
const debug = Debug('cypress:cli')
@@ -65,7 +66,7 @@ const getVersionDir = (version: string = util.pkgVersion(), buildInfo: any = uti
*/
const isInstallingFromPostinstallHook = (): boolean => {
// individual folders
const cwdFolders = process.cwd().split(path.sep)
const cwdFolders = cwd().split(path.sep)
const length = cwdFolders.length
return cwdFolders[length - 2] === 'node_modules' && cwdFolders[length - 1] === 'cypress'
@@ -93,20 +94,19 @@ const getCacheDir = (): string => {
return cache_directory
}
const parseRealPlatformBinaryFolderAsync = (binaryPath: string): any => {
return fs.realpathAsync(binaryPath)
.then((realPath: any) => {
debug('CYPRESS_RUN_BINARY has realpath:', realPath)
if (!realPath.toString().endsWith(getPlatformExecutable())) {
return false
}
const parseRealPlatformBinaryFolderAsync = async (binaryPath: string): Promise<any> => {
const realPath = await fs.realpath(binaryPath)
if (os.platform() === 'darwin') {
return path.resolve(realPath, '..', '..', '..')
}
debug('CYPRESS_RUN_BINARY has realpath:', realPath)
if (!realPath.toString().endsWith(getPlatformExecutable())) {
return false
}
return path.resolve(realPath, '..')
})
if (os.platform() === 'darwin') {
return path.resolve(realPath, '..', '..', '..')
}
return path.resolve(realPath, '..')
}
const getDistDir = (): string => {
@@ -122,25 +122,34 @@ const getBinaryStatePath = (binaryDir: string): string => {
return path.join(binaryDir, '..', 'binary_state.json')
}
const getBinaryStateContentsAsync = (binaryDir: string): any => {
const getBinaryStateContentsAsync = async (binaryDir: string): Promise<any> => {
const fullPath = getBinaryStatePath(binaryDir)
return fs.readJsonAsync(fullPath)
.catch({ code: 'ENOENT' }, SyntaxError, () => {
debug('could not read binary_state.json file at "%s"', fullPath)
try {
const contents = await fs.readJson(fullPath)
return {}
})
debug('binary_state.json contents:', contents)
return contents
} catch (error: any) {
if (error.code === 'ENOENT' || error instanceof SyntaxError) {
debug('could not read binary_state.json file at "%s"', fullPath)
return {}
}
throw error
}
}
const getBinaryVerifiedAsync = (binaryDir: string): any => {
return getBinaryStateContentsAsync(binaryDir)
.tap(debug)
.get('verified')
const getBinaryVerifiedAsync = async (binaryDir: string): Promise<boolean> => {
const contents = await getBinaryStateContentsAsync(binaryDir)
return contents.verified
}
const clearBinaryStateAsync = (binaryDir: string): any => {
return fs.removeAsync(getBinaryStatePath(binaryDir))
const clearBinaryStateAsync = async (binaryDir: string): Promise<void> => {
await fs.remove(getBinaryStatePath(binaryDir))
}
/**
@@ -149,15 +158,14 @@ const clearBinaryStateAsync = (binaryDir: string): any => {
* @param {string} binaryDir Folder holding the binary
* @returns {Promise<void>} returns a promise
*/
const writeBinaryVerifiedAsync = (verified: boolean, binaryDir: string): any => {
return getBinaryStateContentsAsync(binaryDir)
.then((contents: any) => {
return fs.outputJsonAsync(
getBinaryStatePath(binaryDir),
_.extend(contents, { verified }),
{ spaces: 2 },
)
})
const writeBinaryVerifiedAsync = async (verified: boolean, binaryDir: string): Promise<void> => {
const contents = await getBinaryStateContentsAsync(binaryDir)
await fs.outputJson(
getBinaryStatePath(binaryDir),
_.extend(contents, { verified }),
{ spaces: 2 },
)
}
const getPathToExecutable = (binaryDir: string): string => {
@@ -168,19 +176,18 @@ const getPathToExecutable = (binaryDir: string): string => {
* Resolves with an object read from the binary app package.json file.
* If the file does not exist resolves with null
*/
const getBinaryPkgAsync = (binaryDir: string): any => {
const getBinaryPkgAsync = async (binaryDir: string): Promise<any> => {
const pathToPackageJson = getBinaryPkgPath(binaryDir)
debug('Reading binary package.json from:', pathToPackageJson)
return fs.pathExistsAsync(pathToPackageJson)
.then((exists: boolean) => {
if (!exists) {
return null
}
const exists: boolean = await fs.pathExists(pathToPackageJson)
return fs.readJsonAsync(pathToPackageJson)
})
if (!exists) {
return null
}
return fs.readJson(pathToPackageJson)
}
const getBinaryPkgVersion = (o: any): any => _.get(o, 'version', null)

View File

@@ -1,17 +1,18 @@
import _ from 'lodash'
import la from 'lazy-ass'
import is from 'check-more-types'
import cp from 'child_process'
import os from 'os'
import yauzl from 'yauzl'
import Debug from 'debug'
import extract from 'extract-zip'
import Bluebird from 'bluebird'
import readline from 'readline'
import fs from 'fs-extra'
import { throwFormErrorText, errors } from '../errors'
import fs from '../fs'
import util from '../util'
// TODO: this package needs to be replaced as we can't import it in vitest
const is = require('check-more-types')
const debug = Debug('cypress:cli:unzip')
const unzipTools = {
@@ -19,7 +20,7 @@ const unzipTools = {
}
// expose this function for simple testing
const unzip = ({ zipFilePath, installDir, progress }: any): any => {
const unzip = async ({ zipFilePath, installDir, progress }: any): Promise<void> => {
debug('unzipping from %s', zipFilePath)
debug('into', installDir)
@@ -30,170 +31,167 @@ const unzip = ({ zipFilePath, installDir, progress }: any): any => {
const startTime = Date.now()
let yauzlDoneTime = 0
return fs.ensureDirAsync(installDir)
.then(() => {
return new Bluebird((resolve: any, reject: any) => {
return yauzl.open(zipFilePath, (err: any, zipFile: any) => {
yauzlDoneTime = Date.now()
await fs.ensureDir(installDir)
if (err) {
debug('error using yauzl %s', err.message)
await new Promise<void>((resolve, reject) => {
return yauzl.open(zipFilePath, (err: any, zipFile: any) => {
yauzlDoneTime = Date.now()
return reject(err)
if (err) {
debug('error using yauzl %s', err.message)
return reject(err)
}
const total = zipFile.entryCount
debug('zipFile entries count', total)
const started = new Date()
let percent = 0
let count = 0
const notify = (percent: number): void => {
const elapsed = +new Date() - +started
const eta = util.calculateEta(percent, elapsed)
progress.onProgress(percent, util.secsRemaining(eta))
}
const tick = (): any => {
count += 1
percent = ((count / total) * 100)
const displayPercent = percent.toFixed(0)
return notify(Number(displayPercent))
}
const unzipWithNode = async (): Promise<any> => {
debug('unzipping with node.js (slow)')
const opts = {
dir: installDir,
onEntry: tick,
}
const total = zipFile.entryCount
debug('calling Node extract tool %s %o', zipFilePath, opts)
debug('zipFile entries count', total)
try {
await unzipTools.extract(zipFilePath, opts)
debug('node unzip finished')
const started = new Date()
return resolve()
} catch (err: any) {
const error = err || new Error('Unknown error with Node extract tool')
let percent = 0
let count = 0
debug('error %s', error.message)
const notify = (percent: number): void => {
const elapsed = +new Date() - +started
const eta = util.calculateEta(percent, elapsed)
progress.onProgress(percent, util.secsRemaining(eta))
return reject(error)
}
}
const tick = (): any => {
count += 1
const unzipFallback = _.once(unzipWithNode)
percent = ((count / total) * 100)
const displayPercent = percent.toFixed(0)
const unzipWithUnzipTool = (): any => {
debug('unzipping via `unzip`')
return notify(Number(displayPercent))
}
const inflatingRe = /inflating:/
const unzipWithNode = (): any => {
debug('unzipping with node.js (slow)')
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir])
const opts = {
dir: installDir,
onEntry: tick,
}
sp.on('error', (err: any) => {
debug('unzip tool error: %s', err.message)
unzipFallback()
})
debug('calling Node extract tool %s %o', zipFilePath, opts)
return unzipTools.extract(zipFilePath, opts)
.then(() => {
debug('node unzip finished')
sp.on('close', (code: number) => {
debug('unzip tool close with code %d', code)
if (code === 0) {
percent = 100
notify(percent)
return resolve()
})
.catch((err: any) => {
const error = err || new Error('Unknown error with Node extract tool')
}
debug('error %s', error.message)
debug('`unzip` failed %o', { code })
return reject(error)
})
}
return unzipFallback()
})
const unzipFallback = _.once(unzipWithNode)
sp.stdout.on('data', (data: any) => {
if (inflatingRe.test(data)) {
return tick()
}
})
const unzipWithUnzipTool = (): any => {
debug('unzipping via `unzip`')
sp.stderr.on('data', (data: any) => {
debug('`unzip` stderr %s', data)
})
}
const inflatingRe = /inflating:/
// we attempt to first unzip with the native osx
// ditto because its less likely to have problems
// with corruption, symlinks, or icons causing failures
// and can handle resource forks
// http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
const unzipWithOsx = (): any => {
debug('unzipping via `ditto`')
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir])
const copyingFileRe = /^copying file/
sp.on('error', (err: any) => {
debug('unzip tool error: %s', err.message)
unzipFallback()
})
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir])
sp.on('close', (code: number) => {
debug('unzip tool close with code %d', code)
if (code === 0) {
percent = 100
notify(percent)
// f-it just unzip with node
sp.on('error', (err: any) => {
debug(err.message)
unzipFallback()
})
return resolve()
}
debug('`unzip` failed %o', { code })
return unzipFallback()
})
sp.stdout.on('data', (data: any) => {
if (inflatingRe.test(data)) {
return tick()
}
})
sp.stderr.on('data', (data: any) => {
debug('`unzip` stderr %s', data)
})
}
// we attempt to first unzip with the native osx
// ditto because its less likely to have problems
// with corruption, symlinks, or icons causing failures
// and can handle resource forks
// http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
const unzipWithOsx = (): any => {
debug('unzipping via `ditto`')
const copyingFileRe = /^copying file/
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir])
// f-it just unzip with node
sp.on('error', (err: any) => {
debug(err.message)
unzipFallback()
})
sp.on('close', (code: number) => {
if (code === 0) {
sp.on('close', (code: number) => {
if (code === 0) {
// make sure we get to 100% on the progress bar
// because reading in lines is not really accurate
percent = 100
notify(percent)
percent = 100
notify(percent)
return resolve()
}
return resolve()
}
debug('`ditto` failed %o', { code })
debug('`ditto` failed %o', { code })
return unzipFallback()
})
return unzipFallback()
})
return readline.createInterface({
input: sp.stderr,
})
.on('line', (line: string) => {
if (copyingFileRe.test(line)) {
return tick()
}
})
}
return readline.createInterface({
input: sp.stderr,
})
.on('line', (line: string) => {
if (copyingFileRe.test(line)) {
return tick()
}
})
}
switch (os.platform()) {
case 'darwin':
return unzipWithOsx()
case 'linux':
return unzipWithUnzipTool()
case 'win32':
return unzipWithNode()
default:
return
}
})
})
.tap(() => {
debug('unzip completed %o', {
yauzlMs: yauzlDoneTime - startTime,
unzipMs: Date.now() - yauzlDoneTime,
})
switch (os.platform()) {
case 'darwin':
return unzipWithOsx()
case 'linux':
return unzipWithUnzipTool()
case 'win32':
return unzipWithNode()
default:
return
}
})
})
debug('unzip completed %o', {
yauzlMs: yauzlDoneTime - startTime,
unzipMs: Date.now() - yauzlDoneTime,
})
}
function isMaybeWindowsMaxPathLengthError (err: any): boolean {
@@ -214,7 +212,7 @@ const start = async ({ zipFilePath, installDir, progress }: any): Promise<void>
if (installDirExists) {
debug('removing existing unzipped binary', installDir)
await fs.removeAsync(installDir)
await fs.remove(installDir)
}
await unzip({ zipFilePath, installDir, progress })

View File

@@ -16,7 +16,7 @@ import state from './state'
const debug = Debug('cypress:cli')
const VERIFY_TEST_RUNNER_TIMEOUT_MS = (() => {
export const verifyTestRunnerTimeoutMs = () => {
const verifyTimeout = +(util?.getEnv('CYPRESS_VERIFY_TIMEOUT') || 'NaN')
if (_.isNumber(verifyTimeout) && !_.isNaN(verifyTimeout)) {
@@ -24,60 +24,38 @@ const VERIFY_TEST_RUNNER_TIMEOUT_MS = (() => {
}
return 30000
})()
}
const checkExecutable = (binaryDir: string): any => {
const checkExecutable = async (binaryDir: string): Promise<void> => {
const executable = state.getPathToExecutable(binaryDir)
debug('checking if executable exists', executable)
return util.isExecutableAsync(executable)
.then((isExecutable: boolean) => {
try {
const isExecutable = await util.isExecutableAsync(executable)
debug('Binary is executable? :', isExecutable)
if (!isExecutable) {
return throwFormErrorText(errors.binaryNotExecutable(executable))()
}
})
.catch({ code: 'ENOENT' }, () => {
if (util.isCi()) {
return throwFormErrorText(errors.notInstalledCI(executable))()
} catch (err: any) {
if (err.code === 'ENOENT') {
if (util.isCi()) {
return throwFormErrorText(errors.notInstalledCI(executable))()
}
return throwFormErrorText(errors.missingApp(binaryDir))(stripIndent`
Cypress executable not found at: ${chalk.cyan(executable)}
`)
}
return throwFormErrorText(errors.missingApp(binaryDir))(stripIndent`
Cypress executable not found at: ${chalk.cyan(executable)}
`)
})
throw err
}
}
const runSmokeTest = (binaryDir: string, options: any): any => {
let executable = state.getPathToExecutable(binaryDir)
const onSmokeTestError = (smokeTestCommand: string, linuxWithDisplayEnv: boolean) => {
return (err: any) => {
debug('Smoke test failed:', err)
let errMessage = err.stderr || err.message
debug('error message:', errMessage)
if (err.timedOut) {
debug('error timedOut is true')
return throwFormErrorText(
errors.smokeTestFailure(smokeTestCommand, true),
)(errMessage)
}
if (linuxWithDisplayEnv && util.isBrokenGtkDisplay(errMessage)) {
util.logBrokenGtkDisplayWarning()
return throwFormErrorText(errors.invalidSmokeTestDisplayError)(errMessage)
}
return throwFormErrorText(errors.missingDependency)(errMessage)
}
}
const needsXvfb = xvfb.isNeeded()
debug('needs Xvfb?', needsXvfb)
@@ -86,7 +64,7 @@ const runSmokeTest = (binaryDir: string, options: any): any => {
* Spawn Cypress running smoke test to check if all operating system
* dependencies are good.
*/
const spawn = (linuxWithDisplayEnv: boolean): any => {
const spawn = async (linuxWithDisplayEnv: boolean): Promise<any> => {
const random = _.random(0, 1000)
const args = ['--smoke-test', `--ping=${random}`]
@@ -118,13 +96,13 @@ const runSmokeTest = (binaryDir: string, options: any): any => {
timeout: options.smokeTestTimeout,
})
return Bluebird.resolve(util.exec(
executable,
args,
stdioOptions,
))
.catch(onSmokeTestError(smokeTestCommand, linuxWithDisplayEnv))
.then((result: any) => {
try {
const result = await util.exec(
executable,
args,
stdioOptions,
)
// TODO: when execa > 1.1 is released
// change this to `result.all` for both stderr and stdout
// use lodash to be robust during tests against null result or missing stdout
@@ -140,25 +118,51 @@ const runSmokeTest = (binaryDir: string, options: any): any => {
return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, false))(errorText)
}
} catch (err: any) {
debug('Smoke test failed:', err)
let errMessage = err.stderr || err.message
debug('error message:', errMessage)
if (err.timedOut) {
debug('error timedOut is true')
return throwFormErrorText(
errors.smokeTestFailure(smokeTestCommand, true),
)(errMessage)
}
if (linuxWithDisplayEnv && util.isBrokenGtkDisplay(errMessage)) {
util.logBrokenGtkDisplayWarning()
return throwFormErrorText(errors.invalidSmokeTestDisplayError)(errMessage)
}
return throwFormErrorText(errors.missingDependency)(errMessage)
}
}
const spawnInXvfb = async (linuxWithDisplayEnv?: boolean): Promise<any> => {
await xvfb.start()
return spawn(linuxWithDisplayEnv || false).finally(async () => {
await xvfb.stop()
})
}
const spawnInXvfb = (linuxWithDisplayEnv?: boolean): any => {
return xvfb
.start()
.then(() => {
return spawn(linuxWithDisplayEnv || false)
})
.finally(xvfb.stop)
}
const userFriendlySpawn = (linuxWithDisplayEnv: boolean): any => {
const userFriendlySpawn = async (linuxWithDisplayEnv: boolean): Promise<void> => {
debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv))
return spawn(linuxWithDisplayEnv)
.catch({ code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR' }, () => {
return spawnInXvfb(linuxWithDisplayEnv)
})
try {
await spawn(linuxWithDisplayEnv)
} catch (err: any) {
if (err.code === 'INVALID_SMOKE_TEST_DISPLAY_ERROR') {
return spawnInXvfb(linuxWithDisplayEnv)
}
throw err
}
}
if (needsXvfb) {
@@ -173,7 +177,7 @@ const runSmokeTest = (binaryDir: string, options: any): any => {
return userFriendlySpawn(linuxWithDisplayEnv)
}
function testBinary (version: string, binaryDir: string, options: any): any {
function testBinary (version: string, binaryDir: string, options: any): Promise<any> {
debug('running binary verification check', version)
// if running from 'cypress verify', don't print this message
@@ -200,31 +204,28 @@ function testBinary (version: string, binaryDir: string, options: any): any {
const tasks = new Listr([
{
title: util.titleize('Verifying Cypress can run', chalk.gray(binaryDir)),
task: (ctx: any, task: any) => {
task: async (ctx: any, task: any) => {
debug('clearing out the verified version')
return state.clearBinaryStateAsync(binaryDir)
.then(() => {
return Bluebird.all([
runSmokeTest(binaryDir, options),
Bluebird.delay(1500), // good user experience
])
})
.then(() => {
debug('write verified: true')
await state.clearBinaryStateAsync(binaryDir)
return state.writeBinaryVerifiedAsync(true, binaryDir)
})
.then(() => {
util.setTaskTitle(
task,
util.titleize(
chalk.green('Verified Cypress!'),
chalk.gray(binaryDir),
),
rendererOptions.renderer as string,
)
})
await Promise.all([
runSmokeTest(binaryDir, options),
Bluebird.delay(1500), // good user experience
])
debug('write verified: true')
await state.writeBinaryVerifiedAsync(true, binaryDir)
util.setTaskTitle(
task,
util.titleize(
chalk.green('Verified Cypress!'),
chalk.gray(binaryDir),
),
rendererOptions.renderer as string,
)
},
},
] as any, rendererOptions as any)
@@ -232,32 +233,30 @@ function testBinary (version: string, binaryDir: string, options: any): any {
return tasks.run()
}
const maybeVerify = (installedVersion: string, binaryDir: string, options: any): any => {
return state.getBinaryVerifiedAsync(binaryDir)
.then((isVerified: boolean) => {
debug('is Verified ?', isVerified)
const maybeVerify = async (installedVersion: string, binaryDir: string, options: any): Promise<void> => {
const isVerified = await state.getBinaryVerifiedAsync(binaryDir)
let shouldVerify = !isVerified
debug('is Verified ?', isVerified)
// force verify if options.force
if (options.force) {
debug('force verify')
shouldVerify = true
let shouldVerify = !isVerified
// force verify if options.force
if (options.force) {
debug('force verify')
shouldVerify = true
}
if (shouldVerify) {
await testBinary(installedVersion, binaryDir, options)
if (options.welcomeMessage) {
logger.log()
logger.log('Opening Cypress...')
}
if (shouldVerify) {
return testBinary(installedVersion, binaryDir, options)
.then(() => {
if (options.welcomeMessage) {
logger.log()
logger.log('Opening Cypress...')
}
})
}
})
}
}
const start = (options: any = {}): any => {
export const start = async (options: any = {}): Promise<void> => {
debug('verifying Cypress app')
const packageVersion = util.pkgVersion()
@@ -267,21 +266,21 @@ const start = (options: any = {}): any => {
dev: false,
force: false,
welcomeMessage: true,
smokeTestTimeout: VERIFY_TEST_RUNNER_TIMEOUT_MS,
smokeTestTimeout: verifyTestRunnerTimeoutMs(),
skipVerify: util.getEnv('CYPRESS_SKIP_VERIFY') === 'true',
})
if (options.skipVerify) {
debug('skipping verification of the Cypress app')
return Bluebird.resolve()
return Promise.resolve()
}
if (options.dev) {
return runSmokeTest('', options)
}
const parseBinaryEnvVar = (): any => {
const parseBinaryEnvVar = async (): Promise<void> => {
const envBinaryPath = util.getEnv('CYPRESS_RUN_BINARY')
debug('CYPRESS_RUN_BINARY exists, =', envBinaryPath)
@@ -295,19 +294,18 @@ const start = (options: any = {}): any => {
logger.log()
return util.isExecutableAsync(envBinaryPath as string)
.then((isExecutable: boolean) => {
try {
const isExecutable = await util.isExecutableAsync(envBinaryPath as string)
debug('CYPRESS_RUN_BINARY is executable? :', isExecutable)
if (!isExecutable) {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath as string))(stripIndent`
The supplied binary path is not executable
`)
The supplied binary path is not executable
`)
}
})
.then(() => {
return state.parseRealPlatformBinaryFolderAsync(envBinaryPath as string)
})
.then((envBinaryDir: string) => {
const envBinaryDir = await state.parseRealPlatformBinaryFolderAsync(envBinaryPath as string)
if (!envBinaryDir) {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath as string))()
}
@@ -315,31 +313,26 @@ const start = (options: any = {}): any => {
debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir)
binaryDir = envBinaryDir
})
.catch({ code: 'ENOENT' }, (err: any) => {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath as string))(err.message)
})
} catch (err: any) {
if (err.code === 'ENOENT') {
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath as string))(err.message)
}
throw err
}
}
return Bluebird.try(() => {
try {
debug('checking environment variables')
if (util.getEnv('CYPRESS_RUN_BINARY')) {
return parseBinaryEnvVar()
await parseBinaryEnvVar()
}
})
.then(() => {
return checkExecutable(binaryDir)
})
.tap(() => {
return debug('binaryDir is ', binaryDir)
})
.then(() => {
return state.getBinaryPkgAsync(binaryDir)
})
.then((pkg: any) => {
return state.getBinaryPkgVersion(pkg)
})
.then((binaryVersion: string) => {
await checkExecutable(binaryDir)
debug('binaryDir is ', binaryDir)
const pkg = await state.getBinaryPkgAsync(binaryDir)
const binaryVersion = state.getBinaryPkgVersion(pkg)
if (!binaryVersion) {
debug('no Cypress binary found for cli version ', packageVersion)
@@ -366,16 +359,14 @@ const start = (options: any = {}): any => {
logger.log()
}
return maybeVerify(binaryVersion, binaryDir, options)
})
.catch((err: any) => {
await maybeVerify(binaryVersion, binaryDir, options)
} catch (err: any) {
if (err.known) {
throw err
}
return throwFormErrorText(errors.unexpected)(err.stack)
})
}
}
const isLinuxLike = (): boolean => os.platform() !== 'win32'
@@ -389,10 +380,4 @@ const isLinuxLike = (): boolean => os.platform() !== 'win32'
* Seems there is a lot of discussion around this issue among Electron users
* @see https://github.com/electron/electron/issues/17972
*/
const needsSandbox = (): boolean => isLinuxLike()
export default {
start,
needsSandbox,
VERIFY_TEST_RUNNER_TIMEOUT_MS,
}
export const needsSandbox = (): boolean => isLinuxLike()

View File

@@ -4,7 +4,6 @@ import os from 'os'
import ospath from 'ospath'
import hasha from 'hasha'
import la from 'lazy-ass'
import is from 'check-more-types'
import tty from 'tty'
import path from 'path'
import { isCI as isCi } from 'ci-info'
@@ -15,18 +14,20 @@ import Bluebird from 'bluebird'
import cachedir from 'cachedir'
import logSymbols from 'log-symbols'
import executable from 'executable'
import { cwd } from 'process'
import { stripIndent } from 'common-tags'
import supportsColor from 'supports-color'
import isInstalledGlobally from 'is-installed-globally'
import logger from './logger'
import Debug from 'debug'
import fs from './fs'
import fs from 'fs-extra'
import pkg from '../package.json'
// TODO: this package needs to be replaced as we can't import it in vitest
const is = require('check-more-types')
const debug = Debug('cypress:cli')
// Import package.json dynamically to avoid TypeScript JSON import issues
const pkg = require(path.join(__dirname, '..', 'package.json'))
const issuesUrl = 'https://github.com/cypress-io/cypress/issues'
/**
@@ -38,10 +39,12 @@ const getFileChecksum = (filename: string): any => {
return hasha.fromFile(filename, { algorithm: 'sha512' })
}
const getFileSize = (filename: string): any => {
const getFileSize = async (filename: string): Promise<any> => {
la(is.unemptyString(filename), 'expected filename', filename)
return fs.statAsync(filename).get('size')
const { size } = await fs.stat(filename)
return size
}
const isBrokenGtkDisplayRe = /Gtk: cannot open display/
@@ -162,7 +165,6 @@ function printNodeOptions (log: any = debug): void {
```
*/
const dequote = (str: string): string => {
// @ts-expect-error method exists but is not typed
la(is.string(str), 'expected a string to remove double quotes', str)
if (str.length > 1 && str[0] === '"' && str[str.length - 1] === '"') {
return str.substr(1, str.length - 2)
@@ -242,6 +244,7 @@ const getApplicationDataFolder = (...paths: string[]): string => {
// allow overriding the app_data folder
let folder = env.CYPRESS_CONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'
// @ts-expect-error value exists but is not typed
const PRODUCT_NAME = pkg.productName || pkg.name
const OS_DATA_PATH = ospath.data()
@@ -270,13 +273,13 @@ const util = {
getEnvOverrides (options: any = {}): any {
return _
.chain({})
.extend(util.getEnvColors())
.extend(util.getForceTty())
.extend(this.getEnvColors())
.extend(this.getForceTty())
.omitBy(_.isUndefined) // remove undefined values
.mapValues((value: any) => { // stringify to 1 or 0
return value ? '1' : '0'
})
.extend(util.getOriginalNodeOptions())
.extend(this.getOriginalNodeOptions())
.value()
},
@@ -292,14 +295,14 @@ const util = {
getForceTty (): any {
return {
FORCE_STDIN_TTY: util.isTty(process.stdin.fd),
FORCE_STDOUT_TTY: util.isTty(process.stdout.fd),
FORCE_STDERR_TTY: util.isTty(process.stderr.fd),
FORCE_STDIN_TTY: this.isTty(process.stdin.fd),
FORCE_STDOUT_TTY: this.isTty(process.stdout.fd),
FORCE_STDERR_TTY: this.isTty(process.stderr.fd),
}
},
getEnvColors (): any {
const sc = util.supportsColor()
const sc = this.supportsColor()
return {
FORCE_COLOR: sc,
@@ -330,10 +333,11 @@ const util = {
},
cwd (): string {
return process.cwd()
return cwd()
},
pkgBuildInfo (): any {
// @ts-expect-error value exists but is not typed
return pkg.buildInfo
},
@@ -341,6 +345,7 @@ const util = {
return pkg.version
},
// TODO: remove this method
exit (code: number): never {
process.exit(code)
},
@@ -411,30 +416,29 @@ const util = {
isLinux,
getOsVersionAsync () {
return Bluebird.try(() => {
return si.osInfo()
.then((osInfo) => {
if (osInfo.distro && osInfo.release) {
return `${osInfo.distro} - ${osInfo.release}`
}
async getOsVersionAsync (): Promise<any> {
try {
const osInfo = await si.osInfo()
return os.release()
}).catch(() => {
return os.release()
})
})
if (osInfo.distro && osInfo.release) {
return `${osInfo.distro} - ${osInfo.release}`
}
} catch (err) {
return os.release()
}
return os.release()
},
async getPlatformInfo (): Promise<string> {
const [version, osArch] = await Bluebird.all([
util.getOsVersionAsync(),
this.getOsVersionAsync(),
this.getRealArch(),
])
return stripIndent`
Platform: ${os.platform()}-${osArch} (${version})
Cypress Version: ${util.pkgVersion()}
Cypress Version: ${this.pkgVersion()}
`
},
@@ -493,7 +497,7 @@ const util = {
return filename
}
return path.join(process.cwd(), '..', '..', filename)
return path.join(cwd(), '..', '..', filename)
},
getEnv (varName: string, trim?: boolean): string | undefined {
@@ -556,7 +560,6 @@ const util = {
isPossibleLinuxWithIncorrectDisplay,
getGitHubIssueUrl (number: number): string {
// @ts-expect-error method exists but is not typed
la(is.positive(number), 'github issue should be a positive number', number)
la(_.isInteger(number), 'github issue should be an integer', number)

View File

@@ -2,9 +2,9 @@
"name": "cypress",
"version": "0.0.0-development",
"private": true,
"main": "index.js",
"main": "dist/index.js",
"scripts": {
"build-cli": "tsc && tsx ./scripts/build.ts && tsx ./scripts/post-build.ts",
"build-cli": "tsc && tsc -p tsconfig.esm.json && tsx ./scripts/build.ts && tsx ./scripts/post-build.ts",
"clean": "tsx ./scripts/clean.ts",
"dtslint": "dtslint types",
"postinstall": "patch-package && tsx ./scripts/post-install.ts",
@@ -12,18 +12,17 @@
"prebuild": "yarn postinstall && tsx ./scripts/start-build.ts",
"size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"test": "yarn test-unit",
"test-debug": "node --inspect-brk $(yarn bin mocha)",
"test-debug": "npx vitest --inspect-brk --no-file-parallelism --test-timeout=0",
"test-dependencies": "dependency-check . --missing --no-dev --verbose",
"test-unit": "yarn unit",
"test-watch": "yarn unit --watch",
"types": "yarn dtslint",
"unit": "cross-env BLUEBIRD_DEBUG=1 NODE_ENV=test mocha -r ts-node/register/transpile-only --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json"
"test-unit": "vitest run",
"types": "yarn dtslint"
},
"dependencies": {
"@cypress/request": "^3.0.9",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
"@types/tmp": "^0.2.3",
"arch": "^2.2.0",
"blob-util": "^2.0.2",
"bluebird": "^3.7.2",
@@ -83,32 +82,22 @@
"@types/mocha": "8.0.3",
"@types/sinon": "9.0.9",
"@types/sinon-chai": "3.2.12",
"chai": "3.5.0",
"chai-as-promised": "7.1.1",
"chai-string": "1.5.0",
"cross-env": "7.0.3",
"dependency-check": "4.1.0",
"dtslint": "4.2.1",
"execa-wrap": "1.4.0",
"mocha": "6.2.2",
"mock-fs": "5.4.0",
"mocked-env": "1.3.2",
"nock": "13.2.9",
"proxyquire": "2.1.3",
"resolve-pkg": "2.0.0",
"shelljs": "0.8.5",
"sinon": "7.2.2",
"snap-shot-it": "7.9.10",
"spawn-mock": "1.0.0",
"strip-ansi": "6.0.1",
"tsx": "4.20.5",
"typescript": "~5.9.2"
"typescript": "~5.9.2",
"vitest": "3.2.4"
},
"files": [
"bin",
"lib",
"index.js",
"index.mjs",
"dist",
"types/**/*.d.ts",
"mount-utils",
"vue",
@@ -126,8 +115,8 @@
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./index.mjs",
"require": "./index.js"
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./types/net-stubbing": {
"types": "./types/net-stubbing.d.ts"

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'
import path from 'path'
import shell from 'shelljs'
import fs from '../lib/fs'
import fs from 'fs-extra'
// grab the current version and a few other properties
// from the root package.json
@@ -50,7 +50,7 @@ function preparePackageForNpmRelease (json: any, branchName?: string): any {
keywords,
types: 'types', // typescript types
scripts: {
postinstall: 'node index.js --exec install',
postinstall: 'node dist/index.js --exec install',
size: 't="$(npm pack .)"; wc -c "${t}"; tar tvf "${t}"; rm "${t}";',
},
})
@@ -58,20 +58,20 @@ function preparePackageForNpmRelease (json: any, branchName?: string): any {
return json
}
function makeUserPackageFile (branchName?: string): Promise<any> {
return fs.readJsonAsync(packageJsonSrc)
.then((json: any) => preparePackageForNpmRelease(json, branchName))
.then((json: any) => {
return fs.outputJsonAsync(packageJsonDest, json, {
spaces: 2,
})
.return(json) // returning package json object makes it easy to test
})
async function makeUserPackageFile (branchName?: string): Promise<any> {
const json = await fs.readJson(packageJsonSrc)
const jsonPrepared = preparePackageForNpmRelease(json, branchName)
await fs.outputJson(packageJsonDest, jsonPrepared, {
spaces: 2,
}) // returning package json object makes it easy to test
return jsonPrepared
}
export default makeUserPackageFile
if (!module.parent) {
if (require.main === module) {
makeUserPackageFile(process.env.BRANCH)
.catch((err: any) => {
/* eslint-disable no-console */

View File

@@ -23,16 +23,15 @@ includeTypes.forEach((folder: string) => {
})
// build the project and copy the build files over to the build directory
shell.exec('tsc')
shell.exec('tsc -p tsconfig.json')
shell.exec('tsc -p tsconfig.esm.json')
shell.cp('index.js', 'build/index.js')
shell.cp('index.mjs', 'build/index.mjs')
shell.mkdir('-p', 'build/dist')
shell.cp('dist/*.js', 'build/dist/')
shell.cp('dist/*.mjs', 'build/dist/')
shell.mkdir('-p', 'build/lib')
shell.cp('lib/*.js', 'build/lib/')
shell.mkdir('-p', 'build/dist/exec')
shell.cp('dist/exec/*.js', 'build/dist/exec')
shell.mkdir('-p', 'build/lib/exec')
shell.cp('lib/exec/*.js', 'build/lib/exec')
shell.mkdir('-p', 'build/lib/tasks')
shell.cp('lib/tasks/*.js', 'build/lib/tasks')
shell.mkdir('-p', 'build/dist/tasks')
shell.cp('dist/tasks/*.js', 'build/dist/tasks')

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body {
font-family: "Courier New", Courier, monospace;
padding: 0 1em;
line-height: 1.4;
color: #eee;
background-color: #000;
}
pre {
padding: 0 0;
margin: 0 0;
font-family: "Courier New", Courier, monospace;
}
</style>
</head>
<body><pre>┌─────────┬──────────────┐
<span style="color:#AAA">version<span style="color:#eee"><span style="color:#AAA">last used<span style="color:#eee">
├─────────┼──────────────┤
<span style="color:#0A0">1.2.3<span style="color:#eee"><span style="color:#0AA">3 months ago<span style="color:#eee">
├─────────┼──────────────┤
<span style="color:#0A0">2.3.4<span style="color:#eee"><span style="color:#0AA">5 days ago<span style="color:#eee">
└─────────┴──────────────┘</span></span></span></span></span></span></span></span></span></span></span></span>
</pre></body></html>

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body {
font-family: "Courier New", Courier, monospace;
padding: 0 1em;
line-height: 1.4;
color: #eee;
background-color: #000;
}
pre {
padding: 0 0;
margin: 0 0;
font-family: "Courier New", Courier, monospace;
}
</style>
</head>
<body><pre>┌─────────┬──────────────┐
<span style="color:#AAA">version<span style="color:#eee"><span style="color:#AAA">last used<span style="color:#eee">
├─────────┼──────────────┤
<span style="color:#0A0">1.2.3<span style="color:#eee"><span style="color:#0AA">3 months ago<span style="color:#eee">
├─────────┼──────────────┤
<span style="color:#0A0">2.3.4<span style="color:#eee"> │ unknown │
└─────────┴──────────────┘</span></span></span></span></span></span></span></span></span></span>
</pre></body></html>

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body {
font-family: "Courier New", Courier, monospace;
padding: 0 1em;
line-height: 1.4;
color: #eee;
background-color: #000;
}
pre {
padding: 0 0;
margin: 0 0;
font-family: "Courier New", Courier, monospace;
}
</style>
</head>
<body><pre>┌─────────┬──────────────┬───────┐
<span style="color:#AAA">version<span style="color:#eee"><span style="color:#AAA">last used<span style="color:#eee"><span style="color:#AAA">size<span style="color:#eee">
├─────────┼──────────────┼───────┤
<span style="color:#0A0">1.2.3<span style="color:#eee"><span style="color:#0AA">3 months ago<span style="color:#eee"><span style="color:#555">0.2MB<span style="color:#eee">
├─────────┼──────────────┼───────┤
<span style="color:#0A0">2.3.4<span style="color:#eee"> │ unknown │ <span style="color:#555">0.2MB<span style="color:#eee">
└─────────┴──────────────┴───────┘</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
</pre></body></html>

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`package.json build > outputs expected properties 1`] = `
{
"bugs": {
"url": "https://github.com/cypress-io/cypress/issues",
},
"buildInfo": "replaced by normalizePackageJson",
"description": "Cypress is a next generation front end testing tool built for the modern web",
"engines": "test engines",
"homepage": "https://cypress.io",
"keywords": [
"automation",
"browser",
"cypress",
"cypress.io",
"e2e",
"end-to-end",
"integration",
"component",
"mocks",
"runner",
"spies",
"stubs",
"test",
"testing",
],
"license": "MIT",
"name": "test",
"repository": {
"type": "git",
"url": "https://github.com/cypress-io/cypress.git",
},
"scripts": {
"postinstall": "node dist/index.js --exec install",
"size": "t="$(npm pack .)"; wc -c "\${t}"; tar tvf "\${t}"; rm "\${t}";",
},
"types": "types",
"version": "x.y.z",
}
`;

View File

@@ -1,5 +1,360 @@
exports['shows help for open --foo 1'] = `
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`cli > CYPRESS_INTERNAL_ENV > allows and warns when staging environment 1`] = `
" code: 0
stdout:
-------
⚠ Warning: It looks like you're passing CYPRESS_INTERNAL_ENV=staging
The environment variable "CYPRESS_INTERNAL_ENV" is reserved and should only be used internally.
Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again.
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
"
`;
exports[`cli > CYPRESS_INTERNAL_ENV > catches environment "foo" 1`] = `
" code: 11
stderr:
-------
The environment variable with the reserved name "CYPRESS_INTERNAL_ENV" is set.
Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again.
----------
CYPRESS_INTERNAL_ENV=foo
----------
Platform: xxx
Cypress Version: 1.2.3
-------
"
`;
exports[`cli > cypress --version > individual package versions > handles non-existent binary 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: not installed
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress --version > individual package versions > reports electron and node message 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: 10.10.88
Bundled Node version: 11.10.3"
`;
exports[`cli > cypress --version > individual package versions > reports package and binary message 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress --version > individual package versions > reports package and binary message with npm log silent 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress --version > individual package versions > reports package and binary message with npm log warn 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress --version > individual package versions > reports package version 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress -v > individual package versions > handles non-existent binary 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: not installed
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress -v > individual package versions > reports electron and node message 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: 10.10.88
Bundled Node version: 11.10.3"
`;
exports[`cli > cypress -v > individual package versions > reports package and binary message 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress -v > individual package versions > reports package and binary message with npm log silent 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress -v > individual package versions > reports package and binary message with npm log warn 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress -v > individual package versions > reports package version 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress cache list > prints explanation when no cache 1`] = `"No cached binary versions were found."`;
exports[`cli > cypress run > warns with space-separated --spec 1`] = `
"⚠ Warning: It looks like you're passing --spec a space-separated list of arguments:
"a b c d e f g"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --spec arg1,arg2,arg3
The most common cause of this warning is using an unescaped glob pattern. If you are
trying to pass a glob pattern, escape it using quotes:
cypress run --spec "**/*.spec.js""
`;
exports[`cli > cypress run > warns with space-separated --tag 1`] = `
"⚠ Warning: It looks like you're passing --tag a space-separated list of arguments:
"a b c d e f g"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --tag arg1,arg2,arg3"
`;
exports[`cli > cypress version > individual package versions > handles non-existent binary 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: not installed
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress version > individual package versions > reports electron and node message 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: 10.10.88
Bundled Node version: 11.10.3"
`;
exports[`cli > cypress version > individual package versions > reports package and binary message 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress version > individual package versions > reports package and binary message with npm log silent 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress version > individual package versions > reports package and binary message with npm log warn 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > cypress version > individual package versions > reports package version 1`] = `
"Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found"
`;
exports[`cli > help command > shows help 1`] = `
"
command: bin/cypress help
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
"
`;
exports[`cli > help command > shows help for --help 1`] = `
"
command: bin/cypress --help
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
"
`;
exports[`cli > help command > shows help for -h 1`] = `
"
command: bin/cypress -h
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
"
`;
exports[`cli > unknown command > shows usage and exits 1`] = `
"
command: bin/cypress foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Unknown command "foo"
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
"
`;
exports[`cli > unknown option > shows help 1`] = `
"
command: bin/cypress open --foo
code: 1
failed: true
@@ -45,12 +400,112 @@ exports['shows help for open --foo 1'] = `
stderr:
-------
-------
"
`;
exports[`cli > unknown option > shows help for cache command - no sub-command 1`] = `
"
command: bin/cypress cache
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
prune deletes all cached binaries except for the version currently in
use
--size Used with the list command to show the sizes of the cached
folders
-h, --help display help for command
-------
stderr:
-------
`
-------
"
`;
exports['shows help for run --foo 1'] = `
exports[`cli > unknown option > shows help for cache command - unknown option --foo 1`] = `
"
command: bin/cypress cache --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
prune deletes all cached binaries except for the version currently in
use
--size Used with the list command to show the sizes of the cached
folders
-h, --help display help for command
-------
stderr:
-------
-------
"
`;
exports[`cli > unknown option > shows help for cache command - unknown sub-command foo 1`] = `
"
command: bin/cypress cache foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown command: cache foo
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
prune deletes all cached binaries except for the version currently in
use
--size Used with the list command to show the sizes of the cached
folders
-h, --help display help for command
-------
stderr:
-------
-------
"
`;
exports[`cli > unknown option > shows help for run command 1`] = `
"
command: bin/cypress run --foo
code: 1
failed: true
@@ -98,377 +553,5 @@ exports['shows help for run --foo 1'] = `
-------
-------
`
exports['cli unknown option shows help for cache command - unknown option --foo 1'] = `
command: bin/cypress cache --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
prune deletes all cached binaries except for the version currently in
use
--size Used with the list command to show the sizes of the cached
folders
-h, --help display help for command
-------
stderr:
-------
-------
`
exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = `
command: bin/cypress cache foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown command: cache foo
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
prune deletes all cached binaries except for the version currently in
use
--size Used with the list command to show the sizes of the cached
folders
-h, --help display help for command
-------
stderr:
-------
-------
`
exports['cli unknown option shows help for cache command - no sub-command 1'] = `
command: bin/cypress cache
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
prune deletes all cached binaries except for the version currently in
use
--size Used with the list command to show the sizes of the cached
folders
-h, --help display help for command
-------
stderr:
-------
-------
`
exports['cli help command shows help 1'] = `
command: bin/cypress help
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
`
exports['cli help command shows help for -h 1'] = `
command: bin/cypress -h
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
`
exports['cli help command shows help for --help 1'] = `
command: bin/cypress --help
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
`
exports['cli unknown command shows usage and exits 1'] = `
command: bin/cypress foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Unknown command "foo"
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
`
exports['cli CYPRESS_INTERNAL_ENV allows and warns when staging environment 1'] = `
code: 0
stdout:
-------
⚠ Warning: It looks like you're passing CYPRESS_INTERNAL_ENV=staging
The environment variable "CYPRESS_INTERNAL_ENV" is reserved and should only be used internally.
Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again.
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help display help for command
Commands:
help Shows CLI help and exits
version [options] prints Cypress version
open [options] Opens Cypress in the interactive GUI.
run [options] Runs Cypress tests from the CLI without the GUI
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
info [options] Prints Cypress and system information
-------
stderr:
-------
-------
`
exports['cli CYPRESS_INTERNAL_ENV catches environment "foo" 1'] = `
code: 11
stderr:
-------
The environment variable with the reserved name "CYPRESS_INTERNAL_ENV" is set.
Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again.
----------
CYPRESS_INTERNAL_ENV=foo
----------
Platform: xxx
Cypress Version: 1.2.3
-------
`
exports['cli version and binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found
`
exports['cli version and binary version 2'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found
`
exports['cli version with electron and node 1'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: 10.10.88
Bundled Node version: 11.10.3
`
exports['cli version and binary version with npm log silent'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found
`
exports['cli version and binary version with npm log warn'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
Electron version: not found
Bundled Node version: not found
`
exports['cli version no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
Electron version: not found
Bundled Node version: not found
`
exports['cli cypress run warns with space-separated --spec 1'] = `
⚠ Warning: It looks like you're passing --spec a space-separated list of arguments:
"a b c d e f g"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --spec arg1,arg2,arg3
The most common cause of this warning is using an unescaped glob pattern. If you are
trying to pass a glob pattern, escape it using quotes:
cypress run --spec "**/*.spec.js"
`
exports['cli cypress run warns with space-separated --tag 1'] = `
⚠ Warning: It looks like you're passing --tag a space-separated list of arguments:
"a b c d e f g"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --tag arg1,arg2,arg3
`
exports['prints explanation when no cache'] = `
No cached binary versions were found.
`
"
`;

View File

@@ -0,0 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`cypress > .run > resolves with contents of tmp file 1`] = `
{
"code": 0,
"failingTests": [],
}
`;

View File

@@ -0,0 +1,116 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`errors > .errors.formErrorText > calls solution if a function 1`] = `
"description
a solution
----------
Platform: test platform-x64 (Foo - OsVersion)
Cypress Version: 1.2.3"
`;
exports[`errors > .errors.formErrorText > forms full text for invalid display error 1`] = `
"Cypress verification failed.
Cypress failed to start after spawning a new Xvfb server.
The error logs we received were:
----------
current message
----------
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error above for more detail.
----------
Platform: test platform-x64 (Foo - OsVersion)
Cypress Version: 1.2.3"
`;
exports[`errors > .errors.formErrorText > returns fully formed text message 1`] = `
"Your system is missing the dependency: Xvfb
Install Xvfb and run Cypress again.
Read our documentation on dependencies for more information:
https://on.cypress.io/required-dependencies
If you are using Docker, we provide containers with all required dependencies installed.
----------
Platform: test platform-x64 (Foo - OsVersion)
Cypress Version: 1.2.3"
`;
exports[`errors > getError > forms full message and creates Error object 1`] = `
{
"description": "The Test Runner unexpectedly exited via a exit event with signal SIGKILL",
"solution": "Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.",
}
`;
exports[`errors > getError > forms full message and creates Error object 2`] = `
"The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: test platform-x64 (Foo - OsVersion)
Cypress Version: 1.2.3"
`;
exports[`errors > individual > has the following errors 1`] = `
[
"CYPRESS_RUN_BINARY",
"binaryNotExecutable",
"childProcessKilled",
"failedDownload",
"failedUnzip",
"failedUnzipWindowsMaxPathLength",
"incompatibleHeadlessFlags",
"incompatibleTestTypeFlags",
"incompatibleTestingTypeAndFlag",
"invalidCacheDirectory",
"invalidConfigFile",
"invalidCypressEnv",
"invalidOS",
"invalidRunProjectPath",
"invalidSmokeTestDisplayError",
"invalidTestingType",
"missingApp",
"missingDependency",
"missingXvfb",
"nonZeroExitCodeXvfb",
"notInstalledCI",
"smokeTestFailure",
"unexpected",
"unknownError",
"versionMismatch",
]
`;

View File

@@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`stripIndent > can be used with nested message 1`] = `
"first line
foo
bar
last line"
`;
exports[`stripIndent > removes indent from literal string 1`] = `
"first line
second line
third line
last line"
`;

View File

@@ -0,0 +1,43 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`util > .normalizeModuleOptions > converts config object 1`] = `
{
"config": "{"baseUrl":"http://localhost:2000","watchForFileChanges":false}",
}
`;
exports[`util > .normalizeModuleOptions > converts environment object 1`] = `
{
"env": "{"foo":"bar","magicNumber":1234,"host":"kevin.dev.local"}",
}
`;
exports[`util > .normalizeModuleOptions > converts reporterOptions object 1`] = `
{
"reporterOptions": "{"mochaFile":"results/my-test-output.xml","toConsole":true}",
}
`;
exports[`util > .normalizeModuleOptions > converts specs array 1`] = `
{
"spec": "["a","b","c"]",
}
`;
exports[`util > .normalizeModuleOptions > does not change other properties 1`] = `
{
"foo": "bar",
}
`;
exports[`util > .normalizeModuleOptions > does not convert spec when string 1`] = `
{
"spec": "x,y,z",
}
`;
exports[`util > .normalizeModuleOptions > passes string env unchanged 1`] = `
{
"env": "foo=bar",
}
`;

View File

@@ -0,0 +1,56 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import fs from 'fs-extra'
import semver from 'semver'
import makeUserPackageFile from '../../scripts/build'
vi.mock('fs-extra', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
readJson: vi.fn(),
outputJson: vi.fn(),
},
}
})
describe('package.json build', () => {
beforeEach(function (): void {
// stub package.json in CLI
// with a few test props
// the rest should come from root package.json file
// @ts-expect-error - mockResolvedValue
fs.readJson.mockResolvedValue({
name: 'test',
engines: 'test engines',
})
// @ts-expect-error - mockResolvedValue
fs.outputJson.mockResolvedValue(undefined)
})
it('has a semver version', async () => {
const result = await makeUserPackageFile()
expect(semver.valid(result.version)).toBeTruthy()
})
it('outputs expected properties', async () => {
const result = await makeUserPackageFile()
expect(result.buildInfo).to.include({ stable: false })
expect(result.buildInfo.commitBranch).to.match(/.+/)
expect(result.buildInfo.commitSha).to.match(/[a-f0-9]+/)
const snapshot = {
...result,
version: 'x.y.z',
buildInfo: 'replaced by normalizePackageJson',
}
expect(snapshot).toMatchSnapshot()
})
})

View File

@@ -1,51 +0,0 @@
import '../spec_helper'
import makeUserPackageFile from '../../scripts/build'
import snapshot from '../support/snapshot'
import la from 'lazy-ass'
import is from 'check-more-types'
import fs from '../../lib/fs'
const hasVersion = (json: any): void => {
la(is.semver(json.version), 'cannot find version', json)
}
const normalizePackageJson = (o: any): any => {
expect(o.buildInfo).to.include({ stable: false })
expect(o.buildInfo.commitBranch).to.match(/.+/)
expect(o.buildInfo.commitSha).to.match(/[a-f0-9]+/)
return {
...o,
version: 'x.y.z',
buildInfo: 'replaced by normalizePackageJson',
}
}
describe('package.json build', () => {
beforeEach(function (): void {
// stub package.json in CLI
// with a few test props
// the rest should come from root package.json file
sinon.stub(fs, 'readJsonAsync').resolves({
name: 'test',
engines: 'test engines',
})
sinon.stub(fs, 'outputJsonAsync').resolves()
})
it('version', () => {
return makeUserPackageFile()
.then((result: any) => {
hasVersion(result)
return result
})
})
it('outputs expected properties', () => {
return makeUserPackageFile()
.then(normalizePackageJson)
.then(snapshot)
})
})

830
cli/test/lib/cli.spec.ts Normal file
View File

@@ -0,0 +1,830 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import os from 'os'
import Debug from 'debug'
import execa from 'execa-wrap'
import cli from '../../lib/cli'
import util from '../../lib/util'
import logger from '../../lib/logger'
import info from '../../lib/exec/info'
import run from '../../lib/exec/run'
import open from '../../lib/exec/open'
import cache from '../../lib/tasks/cache'
import state from '../../lib/tasks/state'
import { start as verifyStart } from '../../lib/tasks/verify'
import install from '../../lib/tasks/install'
const debug = Debug('test')
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
},
}
})
vi.mock('../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
logErrorExit1: vi.fn(),
pkgBuildInfo: vi.fn(),
pkgVersion: vi.fn(),
},
}
})
vi.mock('../../lib/exec/run', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../lib/exec/open', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../lib/exec/info', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../lib/tasks/install', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../lib/tasks/verify', () => {
return {
start: vi.fn(),
}
})
vi.mock('../../lib/tasks/cache', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
list: vi.fn(),
},
}
})
vi.mock('../../lib/tasks/state', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getBinaryDir: vi.fn(),
getBinaryPkgAsync: vi.fn(),
},
}
})
const flushPromises = () => {
return new Promise<void>((resolve) => {
setTimeout(resolve, 250)
})
}
describe('cli', () => {
const binaryDir = '/binary/dir'
let exec: (args: string) => Promise<any>
let processExitSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
vi.unstubAllEnvs()
vi.clearAllMocks()
logger.reset()
// @ts-expect-error - mockReturnValue
processExitSpy = vi.spyOn(process, 'exit').mockReturnValue(null)
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
util.logErrorExit1.mockReturnValue(null)
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue({ stable: true })
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
// @ts-expect-error - mockReturnValue
state.getBinaryDir.mockReturnValue(binaryDir)
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
electronVersion: '10.9.8',
electronNodeVersion: '7.7.7',
}
}
throw new Error('not found')
})
exec = (args: string): any => {
const cliArgs = `node test ${args}`.split(' ')
debug('calling cli.init with: %o', cliArgs)
return cli.init(cliArgs)
}
})
describe('unknown option', () => {
// note it shows help for that specific command
it('shows help', async () => {
const result = await execa('bin/cypress', ['open', '--foo'])
expect(result).toMatchSnapshot()
})
it('shows help for run command', async () => {
const result = await execa('bin/cypress', ['run', '--foo'])
expect(result).toMatchSnapshot()
})
it('shows help for cache command - unknown option --foo', async () => {
const result = await execa('bin/cypress', ['cache', '--foo'])
expect(result).toMatchSnapshot()
})
it('shows help for cache command - unknown sub-command foo', async () => {
const result = await execa('bin/cypress', ['cache', 'foo'])
expect(result).toMatchSnapshot()
})
it('shows help for cache command - no sub-command', async () => {
const result = await execa('bin/cypress', ['cache'])
expect(result).toMatchSnapshot()
})
})
describe('help command', () => {
it('shows help', async () => {
const result = await execa('bin/cypress', ['help'])
expect(result).toMatchSnapshot()
})
it('shows help for -h', async () => {
const result = await execa('bin/cypress', ['-h'])
expect(result).toMatchSnapshot()
})
it('shows help for --help', async () => {
const result = await execa('bin/cypress', ['--help'])
expect(result).toMatchSnapshot()
})
})
describe('unknown command', () => {
it('shows usage and exits', async () => {
const result = await execa('bin/cypress', ['foo'])
expect(result).toMatchSnapshot()
})
})
describe('CYPRESS_INTERNAL_ENV', () => {
/**
* Replaces line "Platform: ..." with "Platform: xxx"
* @param {string} s
*/
const replacePlatform = (s: string): string => {
return s.replace(/Platform: .+/, 'Platform: xxx')
}
/**
* Replaces line "Cypress Version: ..." with "Cypress Version: 1.2.3"
* @param {string} s
*/
const replaceCypressVersion = (s: string): string => {
return s.replace(/Cypress Version: .+/, 'Cypress Version: 1.2.3')
}
const sanitizePlatform = (text: any): any => {
return text
// @ts-expect-error
.split(os.eol)
.map(replacePlatform)
.map(replaceCypressVersion)
// @ts-expect-error
.join(os.eol)
}
it('allows and warns when staging environment', async () => {
const options = {
env: {
CYPRESS_INTERNAL_ENV: 'staging',
},
filter: ['code', 'stderr', 'stdout'],
}
const result = await execa('bin/cypress', ['help'], options)
expect(result).toMatchSnapshot()
})
it('catches environment "foo"', async () => {
const options = {
env: {
CYPRESS_INTERNAL_ENV: 'foo',
},
// we are only interested in the exit code
filter: ['code', 'stderr'],
}
const result = await execa('bin/cypress', ['help'], options)
expect(sanitizePlatform(result)).toMatchSnapshot()
})
})
const versionCommands = ['--version', '-v', 'version']
versionCommands.forEach((versionCommand: string) => {
describe(`cypress ${versionCommand}`, () => {
describe('individual package versions', () => {
it('reports just the package version', async () => {
await exec(`${versionCommand} --component package`)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toEqual('1.2.3')
})
it('reports just the binary version', async () => {
await exec(`${versionCommand} --component binary`)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toEqual('X.Y.Z')
})
it('reports just the electron version', async () => {
await exec(`${versionCommand} --component electron`)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toEqual('10.9.8')
})
it('reports just the bundled Node version', async () => {
await exec(`${versionCommand} --component node`)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toEqual('7.7.7')
})
it('handles not found bundled Node version', async () => {
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
electronVersion: '10.9.8',
}
}
throw new Error('not found')
})
await exec(`${versionCommand} --component node`)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toEqual('not found')
})
it('reports package version', async () => {
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
}
}
throw new Error('not found')
})
await exec(versionCommand)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toMatchSnapshot()
})
it('reports package and binary message', async () => {
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
}
}
throw new Error('not found')
})
await exec(versionCommand)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toMatchSnapshot()
})
it('reports electron and node message', async () => {
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
electronVersion: '10.10.88',
electronNodeVersion: '11.10.3',
}
}
throw new Error('not found')
})
await exec(versionCommand)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toMatchSnapshot()
})
it('reports package and binary message with npm log silent', async () => {
vi.stubEnv('npm_config_loglevel', 'silent')
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
}
}
throw new Error('not found')
})
await exec(versionCommand)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toMatchSnapshot()
})
it('reports package and binary message with npm log warn', async () => {
vi.stubEnv('npm_config_loglevel', 'warn')
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: 'X.Y.Z',
}
}
throw new Error('not found')
})
await exec(versionCommand)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toMatchSnapshot()
})
it('handles non-existent binary', async () => {
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return null
}
throw new Error('not found')
})
await exec(versionCommand)
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(0)
expect(logger.print()).toMatchSnapshot()
})
})
})
})
describe('cypress run', () => {
beforeEach(() => {
//@ts-expect-error - mockResolvedValue
run.start.mockResolvedValue(0)
})
it('calls run.start with options + exits with code', async () => {
// @ts-expect-error - mockResolvedValue
run.start.mockResolvedValue(10)
await exec('run')
await flushPromises()
expect(processExitSpy).toHaveBeenCalledWith(10)
})
it('run.start with options + catches errors', async () => {
const err = new Error('foo')
// @ts-expect-error - mockRejectedValue
run.start.mockRejectedValue(err)
await exec('run')
await flushPromises()
expect(util.logErrorExit1).toHaveBeenCalledWith(err)
})
it('calls run with port', async () => {
await exec('run --port 7878')
expect(run.start).toBeCalledWith({ port: '7878' })
})
it('calls run with port with -p arg', async () => {
await exec('run -p 8989')
expect(run.start).toBeCalledWith({ port: '8989' })
})
it('calls run with env variables', async () => {
await exec('run --env foo=bar,host=http://localhost:8888')
expect(run.start).toBeCalledWith({
env: 'foo=bar,host=http://localhost:8888',
})
})
it('calls run with config', async () => {
await exec('run --config watchForFileChanges=false,baseUrl=localhost')
expect(run.start).toBeCalledWith({
config: 'watchForFileChanges=false,baseUrl=localhost',
})
})
it('calls run with key', async () => {
await exec('run --key asdf')
expect(run.start).toBeCalledWith({ key: 'asdf' })
})
it('calls run with --record', async () => {
await exec('run --record')
expect(run.start).toBeCalledWith({ record: true })
})
it('calls run with --record false', async () => {
await exec('run --record false')
expect(run.start).toBeCalledWith({ record: false })
})
it('calls run with relative --project folder', async () => {
await exec('run --project foo/bar')
expect(run.start).toBeCalledWith({ project: 'foo/bar' })
})
it('calls run with absolute --project folder', async () => {
await exec('run --project /tmp/foo/bar')
expect(run.start).toBeCalledWith({ project: '/tmp/foo/bar' })
})
it('calls run with headed', async () => {
await exec('run --headed')
expect(run.start).toBeCalledWith({ headed: true })
})
it('calls run with --no-exit', async () => {
await exec('run --no-exit')
expect(run.start).toBeCalledWith({ exit: false })
})
it('calls run with --parallel', async () => {
await exec('run --parallel')
expect(run.start).toBeCalledWith({ parallel: true })
})
it('calls run with --ci-build-id', async () => {
await exec('run --ci-build-id 123')
expect(run.start).toBeCalledWith({ ciBuildId: '123' })
})
it('calls run with --group', async () => {
await exec('run --group staging')
expect(run.start).toBeCalledWith({ group: 'staging' })
})
it('calls run with spec', async () => {
await exec('run --spec cypress/integration/foo_spec.js')
expect(run.start).toBeCalledWith({
spec: 'cypress/integration/foo_spec.js',
})
})
it('calls run with space-separated --spec', async () => {
await exec('run --spec a b c d e f g')
expect(run.start).toBeCalledWith({ spec: 'a,b,c,d,e,f,g' })
await exec('run --dev bang --spec foo bar baz -P ./')
expect(run.start).toBeCalledWith(expect.objectContaining({ spec: 'foo,bar,baz' }))
})
it('warns with space-separated --spec', async () => {
await exec('run --spec a b c d e f g --dev')
expect(logger.print()).toMatchSnapshot()
})
it('calls run with --tag', async () => {
await exec('run --tag nightly')
expect(run.start).toBeCalledWith({ tag: 'nightly' })
})
it('calls run comma-separated --tag', async () => {
await exec('run --tag nightly,staging')
expect(run.start).toBeCalledWith({ tag: 'nightly,staging' })
})
it('does not remove double quotes from --tag', async () => {
// I think it is a good idea to lock down this behavior
// to make sure we either preserve it or change it in the future
await exec('run --tag "nightly"')
expect(run.start).toBeCalledWith({ tag: '"nightly"' })
})
it('calls run comma-separated --spec', async () => {
await exec('run --spec main_spec.js,view_spec.js')
expect(run.start).toBeCalledWith({ spec: 'main_spec.js,view_spec.js' })
})
it('calls run with space-separated --tag', async () => {
await exec('run --tag a b c d e f g')
expect(run.start).toBeCalledWith({ tag: 'a,b,c,d,e,f,g' })
await exec('run --dev bang --tag foo bar baz -P ./')
expect(run.start).toBeCalledWith(expect.objectContaining({ tag: 'foo,bar,baz' }))
})
it('warns with space-separated --tag', async () => {
await exec('run --tag a b c d e f g --dev')
expect(logger.print()).toMatchSnapshot()
})
it('calls run with space-separated --tag and --spec', async () => {
await exec('run --tag a b c d e f g --spec h i j k l')
expect(run.start).toBeCalledWith({ tag: 'a,b,c,d,e,f,g', spec: 'h,i,j,k,l' })
await exec('run --dev bang --tag foo bar baz -P ./ --spec fizz buzz --headed false')
expect(run.start).toBeCalledWith(expect.objectContaining({ tag: 'foo,bar,baz', spec: 'fizz,buzz' }))
})
it('removes stray double quotes from --ci-build-id and --group', async () => {
await exec('run --ci-build-id "123" --group "staging"')
expect(run.start).toBeCalledWith({ ciBuildId: '123', group: 'staging' })
})
it('calls run with --auto-cancel-after-failures', async () => {
await exec('run --auto-cancel-after-failures 4')
expect(run.start).toBeCalledWith({ autoCancelAfterFailures: '4' })
})
it('calls run with --auto-cancel-after-failures with false', async () => {
await exec('run --auto-cancel-after-failures false')
expect(run.start).toBeCalledWith({ autoCancelAfterFailures: 'false' })
})
it('calls run with --runner-ui', async () => {
await exec('run --runner-ui')
expect(run.start).toBeCalledWith({ runnerUi: true })
})
it('calls run with --no-runner-ui', async () => {
await exec('run --no-runner-ui')
expect(run.start).toBeCalledWith({ runnerUi: false })
})
describe('component-testing', () => {
it('passes to run.start the correct args for component-testing', async () => {
await exec('run --component --dev')
expect(run.start).toHaveBeenNthCalledWith(1, {
component: true,
dev: true,
})
})
})
})
describe('cypress open', () => {
beforeEach(() => {
// @ts-expect-error - mockResolvedValue
open.start.mockResolvedValue(0)
})
it('calls open.start with relative --project folder', async () => {
await exec('open --project foo/bar')
expect(open.start).toBeCalledWith({ project: 'foo/bar' })
})
it('calls open.start with absolute --project folder', async () => {
await exec('open --project /tmp/foo/bar')
expect(open.start).toBeCalledWith({ project: '/tmp/foo/bar' })
})
it('calls open.start with options', async () => {
await exec('open --port 7878')
expect(open.start).toBeCalledWith({ port: '7878' })
})
it('calls open.start with global', async () => {
await exec('open --port 7878 --global')
expect(open.start).toBeCalledWith({ port: '7878', global: true })
})
it('calls open.start + catches errors', async () => {
const err = new Error('foo')
// @ts-expect-error - mockRejectedValue
open.start.mockRejectedValue(err)
await exec('open --port 7878')
await flushPromises()
expect(util.logErrorExit1).toHaveBeenCalledWith(err)
})
describe('component-testing', () => {
it('passes to open.start the correct args for component-testing', async () => {
await exec('open --component --dev')
await flushPromises()
expect(open.start).toHaveBeenNthCalledWith(1, {
component: true,
dev: true,
})
})
})
})
describe('cypress install', () => {
beforeEach(() => {
// @ts-expect-error - mockResolvedValue
install.start.mockResolvedValue(undefined)
})
it('calls install.start without forcing', async () => {
await exec('install')
expect(install.start).not.toBeCalledWith({ force: true })
})
it('calls install.start with force: true when passed', async () => {
await exec('install --force')
expect(install.start).toBeCalledWith({ force: true })
})
it('install calls install.start + catches errors', async () => {
const err = new Error('foo')
// @ts-expect-error - mockRejectedValue
install.start.mockRejectedValue(err)
await exec('install')
await flushPromises()
expect(util.logErrorExit1).toHaveBeenCalledWith(err)
})
})
describe('cypress verify', () => {
beforeEach(() => {
// @ts-expect-error - mockResolvedValue
verifyStart.mockResolvedValue(undefined)
})
it('verify calls verifyStart with force: true', async () => {
await exec('verify')
expect(verifyStart).toBeCalledWith({
force: true,
welcomeMessage: false,
})
})
it('verify calls verifyStart + catches errors', async () => {
const err = new Error('foo')
// @ts-expect-error - mockRejectedValue
verifyStart.mockRejectedValue(err)
await exec('verify')
await flushPromises()
expect(util.logErrorExit1).toHaveBeenCalledWith(err)
})
})
describe('cypress info', () => {
beforeEach(() => {
info.start.mockResolvedValue(undefined)
})
it('calls info start', async () => {
await exec('info')
expect(info.start).toBeCalled()
})
})
describe('cypress cache list', () => {
it('prints explanation when no cache', async () => {
const err: any = new Error()
err.code = 'ENOENT'
// @ts-expect-error - mockRejectedValue
cache.list.mockRejectedValue(err)
await exec('cache list')
await flushPromises()
expect(logger.print()).toMatchSnapshot()
})
it('catches rejection and exits', async () => {
const err = new Error('cache list failed badly')
// @ts-expect-error - mockRejectedValue
cache.list.mockRejectedValue(err)
await exec('cache list')
await flushPromises()
expect(util.logErrorExit1).toHaveBeenCalledWith(err)
})
})
})

View File

@@ -1,692 +0,0 @@
import '../spec_helper'
import os from 'os'
import snapshot from '../support/snapshot'
import Debug from 'debug'
import execa from 'execa-wrap'
import mockedEnv from 'mocked-env'
import { expect } from 'chai'
import mochaBanner from 'mocha-banner'
const debug = Debug('test')
// Import modules dynamically to handle template literal paths
import cli from '../../lib/cli'
import util from '../../lib/util'
import logger from '../../lib/logger'
import info from '../../lib/exec/info'
import run from '../../lib/exec/run'
import open from '../../lib/exec/open'
import cache from '../../lib/tasks/cache'
import state from '../../lib/tasks/state'
import verify from '../../lib/tasks/verify'
import install from '../../lib/tasks/install'
import spawn from '../../lib/exec/spawn'
describe('cli', () => {
mochaBanner.register()
beforeEach(function (): void {
logger.reset()
// @ts-expect-error
sinon.stub(process, 'exit').returns(null)
;(os.platform as any).returns('darwin')
// @ts-expect-error
sinon.stub(util, 'logErrorExit1').returns(null)
sinon.stub(util, 'pkgBuildInfo').returns({ stable: true })
;(this as any).exec = (args: string): any => {
const cliArgs = `node test ${args}`.split(' ')
debug('calling cli.init with: %o', cliArgs)
return cli.init(cliArgs)
}
})
context('unknown option', () => {
// note it shows help for that specific command
it('shows help', () => {
return execa('bin/cypress', ['open', '--foo']).then((result: any) => {
snapshot('shows help for open --foo 1', result)
})
})
it('shows help for run command', () => {
return execa('bin/cypress', ['run', '--foo']).then((result: any) => {
snapshot('shows help for run --foo 1', result)
})
})
it('shows help for cache command - unknown option --foo', () => {
return execa('bin/cypress', ['cache', '--foo']).then(snapshot)
})
it('shows help for cache command - unknown sub-command foo', () => {
return execa('bin/cypress', ['cache', 'foo']).then(snapshot)
})
it('shows help for cache command - no sub-command', () => {
return execa('bin/cypress', ['cache']).then(snapshot)
})
})
context('help command', () => {
it('shows help', () => {
return execa('bin/cypress', ['help']).then(snapshot)
})
it('shows help for -h', () => {
return execa('bin/cypress', ['-h']).then(snapshot)
})
it('shows help for --help', () => {
return execa('bin/cypress', ['--help']).then(snapshot)
})
})
context('unknown command', () => {
it('shows usage and exits', () => {
return execa('bin/cypress', ['foo']).then(snapshot)
})
})
context('CYPRESS_INTERNAL_ENV', () => {
/**
* Replaces line "Platform: ..." with "Platform: xxx"
* @param {string} s
*/
const replacePlatform = (s: string): string => {
return s.replace(/Platform: .+/, 'Platform: xxx')
}
/**
* Replaces line "Cypress Version: ..." with "Cypress Version: 1.2.3"
* @param {string} s
*/
const replaceCypressVersion = (s: string): string => {
return s.replace(/Cypress Version: .+/, 'Cypress Version: 1.2.3')
}
const sanitizePlatform = (text: any): any => {
return text
// @ts-expect-error
.split(os.eol)
.map(replacePlatform)
.map(replaceCypressVersion)
// @ts-expect-error
.join(os.eol)
}
it('allows and warns when staging environment', () => {
const options = {
env: {
CYPRESS_INTERNAL_ENV: 'staging',
},
filter: ['code', 'stderr', 'stdout'],
}
return execa('bin/cypress', ['help'], options).then(snapshot)
})
it('catches environment "foo"', () => {
const options = {
env: {
CYPRESS_INTERNAL_ENV: 'foo',
},
// we are only interested in the exit code
filter: ['code', 'stderr'],
}
return execa('bin/cypress', ['help'], options)
.then(sanitizePlatform)
.then(snapshot)
})
})
;['--version', '-v', 'version'].forEach((versionCommand: string) => {
context(`cypress ${versionCommand}`, () => {
let restoreEnv: any
afterEach(() => {
if (restoreEnv) {
restoreEnv()
restoreEnv = null
}
})
const binaryDir = '/binary/dir'
beforeEach((): void => {
sinon.stub(state, 'getBinaryDir').returns(binaryDir)
})
describe('individual package versions', () => {
beforeEach((): void => {
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon
.stub(state, 'getBinaryPkgAsync')
.withArgs(binaryDir)
.resolves({
version: 'X.Y.Z',
electronVersion: '10.9.8',
electronNodeVersion: '7.7.7',
})
})
it('reports just the package version', function (done) {
(this as any).exec(`${versionCommand} --component package`)
;(process.exit as any).callsFake((exitCode: any) => {
expect(logger.print()).to.equal('1.2.3')
done()
})
})
it('reports just the binary version', function (done) {
(this as any).exec(`${versionCommand} --component binary`)
;(process.exit as any).callsFake(() => {
expect(logger.print()).to.equal('X.Y.Z')
done()
})
})
it('reports just the electron version', function (done) {
(this as any).exec(`${versionCommand} --component electron`)
;(process.exit as any).callsFake(() => {
expect(logger.print()).to.equal('10.9.8')
done()
})
})
it('reports just the bundled Node version', function (done) {
(this as any).exec(`${versionCommand} --component node`)
;(process.exit as any).callsFake(() => {
expect(logger.print()).to.equal('7.7.7')
done()
})
})
it('handles not found bundled Node version', function (done) {
state.getBinaryPkgAsync
.withArgs(binaryDir)
.resolves({
version: 'X.Y.Z',
electronVersion: '10.9.8',
})
;(this as any).exec(`${versionCommand} --component node`)
;(process.exit as any).callsFake(() => {
expect(logger.print()).to.equal('not found')
done()
})
})
})
it('reports package version', function (done) {
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon
.stub(state, 'getBinaryPkgAsync')
.withArgs(binaryDir)
.resolves({
version: 'X.Y.Z',
})
;(this as any).exec(versionCommand)
;(process.exit as any).callsFake(() => {
snapshot('cli version and binary version 1', logger.print(), { allowSharedSnapshot: true })
done()
})
})
it('reports package and binary message', function (done) {
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon.stub(state, 'getBinaryPkgAsync').resolves({ version: 'X.Y.Z' })
;(this as any).exec(versionCommand)
;(process.exit as any).callsFake(() => {
snapshot('cli version and binary version 2', logger.print(), { allowSharedSnapshot: true })
done()
})
})
it('reports electron and node message', function (done) {
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon.stub(state, 'getBinaryPkgAsync').resolves({
version: 'X.Y.Z',
electronVersion: '10.10.88',
electronNodeVersion: '11.10.3',
})
;(this as any).exec(versionCommand)
;(process.exit as any).callsFake(() => {
snapshot('cli version with electron and node 1', logger.print(), { allowSharedSnapshot: true })
done()
})
})
it('reports package and binary message with npm log silent', function (done) {
restoreEnv = mockedEnv({
npm_config_loglevel: 'silent',
})
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon.stub(state, 'getBinaryPkgAsync').resolves({ version: 'X.Y.Z' })
;(this as any).exec(versionCommand)
;(process.exit as any).callsFake(() => {
// should not be empty!
snapshot('cli version and binary version with npm log silent', logger.print(), { allowSharedSnapshot: true })
done()
})
})
it('reports package and binary message with npm log warn', function (done) {
restoreEnv = mockedEnv({
npm_config_loglevel: 'warn',
})
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon.stub(state, 'getBinaryPkgAsync').resolves({
version: 'X.Y.Z',
})
;(this as any).exec(versionCommand)
;(process.exit as any).callsFake(() => {
// should not be empty!
snapshot('cli version and binary version with npm log warn', logger.print(), { allowSharedSnapshot: true })
done()
})
})
it('handles non-existent binary', function (done) {
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon.stub(state, 'getBinaryPkgAsync').resolves(null)
;(this as any).exec(versionCommand)
;(process.exit as any).callsFake(() => {
snapshot('cli version no binary version 1', logger.print(), { allowSharedSnapshot: true })
done()
})
})
})
})
context('cypress run', () => {
beforeEach((): void => {
sinon.stub(run, 'start').resolves(0)
sinon.stub(util, 'exit').withArgs(0)
})
it('calls run.start with options + exits with code', function (done) {
// @ts-expect-error
run.start.resolves(10)
;(this as any).exec('run')
// @ts-expect-error
util.exit.callsFake((code: number) => {
expect(code).to.eq(10)
done()
})
})
it('run.start with options + catches errors', function (done) {
const err = new Error('foo')
// @ts-expect-error
run.start.rejects(err)
;(this as any).exec('run')
// @ts-expect-error
util.logErrorExit1.callsFake((e: Error) => {
expect(e).to.eq(err)
done()
})
})
it('calls run with port', function (): void {
(this as any).exec('run --port 7878')
expect(run.start).to.be.calledWith({ port: '7878' })
})
it('calls run with port with -p arg', function (): void {
(this as any).exec('run -p 8989')
expect(run.start).to.be.calledWith({ port: '8989' })
})
it('calls run with env variables', function (): void {
(this as any).exec('run --env foo=bar,host=http://localhost:8888')
expect(run.start).to.be.calledWith({
env: 'foo=bar,host=http://localhost:8888',
})
})
it('calls run with config', function (): void {
(this as any).exec('run --config watchForFileChanges=false,baseUrl=localhost')
expect(run.start).to.be.calledWith({
config: 'watchForFileChanges=false,baseUrl=localhost',
})
})
it('calls run with key', function (): void {
(this as any).exec('run --key asdf')
expect(run.start).to.be.calledWith({ key: 'asdf' })
})
it('calls run with --record', function (): void {
(this as any).exec('run --record')
expect(run.start).to.be.calledWith({ record: true })
})
it('calls run with --record false', function (): void {
(this as any).exec('run --record false')
expect(run.start).to.be.calledWith({ record: false })
})
it('calls run with relative --project folder', function (): void {
(this as any).exec('run --project foo/bar')
expect(run.start).to.be.calledWith({ project: 'foo/bar' })
})
it('calls run with absolute --project folder', function (): void {
(this as any).exec('run --project /tmp/foo/bar')
expect(run.start).to.be.calledWith({ project: '/tmp/foo/bar' })
})
it('calls run with headed', function (): void {
(this as any).exec('run --headed')
expect(run.start).to.be.calledWith({ headed: true })
})
it('calls run with --no-exit', function (): void {
(this as any).exec('run --no-exit')
expect(run.start).to.be.calledWith({ exit: false })
})
it('calls run with --parallel', function (): void {
(this as any).exec('run --parallel')
expect(run.start).to.be.calledWith({ parallel: true })
})
it('calls run with --ci-build-id', function (): void {
(this as any).exec('run --ci-build-id 123')
expect(run.start).to.be.calledWith({ ciBuildId: '123' })
})
it('calls run with --group', function (): void {
(this as any).exec('run --group staging')
expect(run.start).to.be.calledWith({ group: 'staging' })
})
it('calls run with spec', function (): void {
(this as any).exec('run --spec cypress/integration/foo_spec.js')
expect(run.start).to.be.calledWith({
spec: 'cypress/integration/foo_spec.js',
})
})
it('calls run with space-separated --spec', function (): void {
(this as any).exec('run --spec a b c d e f g')
expect(run.start).to.be.calledWith({ spec: 'a,b,c,d,e,f,g' })
;(this as any).exec('run --dev bang --spec foo bar baz -P ./')
expect(run.start).to.be.calledWithMatch({ spec: 'foo,bar,baz' })
})
it('warns with space-separated --spec', function (done) {
sinon.spy(logger, 'warn')
;(this as any).exec('run --spec a b c d e f g --dev')
snapshot(logger.warn.getCall(0).args[0])
done()
})
it('calls run with --tag', function (): void {
(this as any).exec('run --tag nightly')
expect(run.start).to.be.calledWith({ tag: 'nightly' })
})
it('calls run comma-separated --tag', function (): void {
(this as any).exec('run --tag nightly,staging')
expect(run.start).to.be.calledWith({ tag: 'nightly,staging' })
})
it('does not remove double quotes from --tag', function (): void {
// I think it is a good idea to lock down this behavior
// to make sure we either preserve it or change it in the future
(this as any).exec('run --tag "nightly"')
expect(run.start).to.be.calledWith({ tag: '"nightly"' })
})
it('calls run comma-separated --spec', function (): void {
(this as any).exec('run --spec main_spec.js,view_spec.js')
expect(run.start).to.be.calledWith({ spec: 'main_spec.js,view_spec.js' })
})
it('calls run with space-separated --tag', function (): void {
(this as any).exec('run --tag a b c d e f g')
expect(run.start).to.be.calledWith({ tag: 'a,b,c,d,e,f,g' })
;(this as any).exec('run --dev bang --tag foo bar baz -P ./')
expect(run.start).to.be.calledWithMatch({ tag: 'foo,bar,baz' })
})
it('warns with space-separated --tag', function (done) {
sinon.spy(logger, 'warn')
;(this as any).exec('run --tag a b c d e f g --dev')
snapshot(logger.warn.getCall(0).args[0])
done()
})
it('calls run with space-separated --tag and --spec', function (): void {
(this as any).exec('run --tag a b c d e f g --spec h i j k l')
expect(run.start).to.be.calledWith({ tag: 'a,b,c,d,e,f,g', spec: 'h,i,j,k,l' })
;(this as any).exec('run --dev bang --tag foo bar baz -P ./ --spec fizz buzz --headed false')
expect(run.start).to.be.calledWithMatch({ tag: 'foo,bar,baz', spec: 'fizz,buzz' })
})
it('removes stray double quotes from --ci-build-id and --group', function (): void {
(this as any).exec('run --ci-build-id "123" --group "staging"')
expect(run.start).to.be.calledWith({ ciBuildId: '123', group: 'staging' })
})
it('calls run with --auto-cancel-after-failures', function (): void {
(this as any).exec('run --auto-cancel-after-failures 4')
expect(run.start).to.be.calledWith({ autoCancelAfterFailures: '4' })
})
it('calls run with --auto-cancel-after-failures with false', function (): void {
(this as any).exec('run --auto-cancel-after-failures false')
expect(run.start).to.be.calledWith({ autoCancelAfterFailures: 'false' })
})
it('calls run with --runner-ui', function (): void {
(this as any).exec('run --runner-ui')
expect(run.start).to.be.calledWith({ runnerUi: true })
})
it('calls run with --no-runner-ui', function (): void {
(this as any).exec('run --no-runner-ui')
expect(run.start).to.be.calledWith({ runnerUi: false })
})
})
context('cypress open', () => {
beforeEach((): void => {
sinon.stub(open, 'start').resolves(0)
})
it('calls open.start with relative --project folder', function (): void {
(this as any).exec('open --project foo/bar')
expect(open.start).to.be.calledWith({ project: 'foo/bar' })
})
it('calls open.start with absolute --project folder', function (): void {
(this as any).exec('open --project /tmp/foo/bar')
expect(open.start).to.be.calledWith({ project: '/tmp/foo/bar' })
})
it('calls open.start with options', function (): void {
// sinon.stub(open, 'start').resolves()
(this as any).exec('open --port 7878')
expect(open.start).to.be.calledWith({ port: '7878' })
})
it('calls open.start with global', function (): void {
// sinon.stub(open, 'start').resolves()
(this as any).exec('open --port 7878 --global')
expect(open.start).to.be.calledWith({ port: '7878', global: true })
})
it('calls open.start + catches errors', function (done) {
const err = new Error('foo')
// @ts-expect-error
open.start.rejects(err)
;(this as any).exec('open --port 7878')
// @ts-expect-error
util.logErrorExit1.callsFake((e: Error) => {
expect(e).to.eq(err)
done()
})
})
})
context('cypress install', () => {
it('calls install.start without forcing', function (): void {
sinon.stub(install, 'start').resolves()
;(this as any).exec('install')
expect(install.start).not.to.be.calledWith({ force: true })
})
it('calls install.start with force: true when passed', function (): void {
sinon.stub(install, 'start').resolves()
;(this as any).exec('install --force')
expect(install.start).to.be.calledWith({ force: true })
})
it('install calls install.start + catches errors', function (done) {
const err = new Error('foo')
sinon.stub(install, 'start').rejects(err)
;(this as any).exec('install')
// @ts-expect-error
util.logErrorExit1.callsFake((e: Error) => {
expect(e).to.eq(err)
done()
})
})
})
context('cypress verify', () => {
it('verify calls verify.start with force: true', function (): void {
sinon.stub(verify, 'start').resolves()
;(this as any).exec('verify')
expect(verify.start).to.be.calledWith({
force: true,
welcomeMessage: false,
})
})
it('verify calls verify.start + catches errors', function (done) {
const err = new Error('foo')
sinon.stub(verify, 'start').rejects(err)
;(this as any).exec('verify')
// @ts-expect-error
util.logErrorExit1.callsFake((e: Error) => {
expect(e).to.eq(err)
done()
})
})
})
context('cypress info', () => {
beforeEach((): void => {
sinon.stub(info, 'start').resolves(0)
sinon.stub(util, 'exit').withArgs(0)
})
it('calls info start', function (): void {
(this as any).exec('info')
expect(info.start).to.have.been.calledWith()
})
})
context('cypress cache list', () => {
it('prints explanation when no cache', function (done) {
const err: any = new Error()
err.code = 'ENOENT'
sinon.stub(cache, 'list').rejects(err)
;(this as any).exec('cache list')
;(process.exit as any).callsFake(() => {
snapshot('prints explanation when no cache', logger.print())
done()
})
})
it('catches rejection and exits', function (done) {
const err = new Error('cache list failed badly')
sinon.stub(cache, 'list').rejects(err)
;(this as any).exec('cache list')
// @ts-expect-error
util.logErrorExit1.callsFake((e: Error) => {
expect(e).to.eq(err)
done()
})
})
})
context('component-testing', () => {
beforeEach((): void => {
sinon.stub(spawn, 'start').resolves()
})
it('spawns server with correct args for component-testing', function (): void {
(this as any).exec('open --component --dev')
// @ts-expect-error
expect(spawn.start.firstCall.args[0]).to.include('--testing-type')
// @ts-expect-error
expect(spawn.start.firstCall.args[0]).to.include('component')
})
it('runs server with correct args for component-testing', function (): void {
(this as any).exec('run --component --dev')
// @ts-expect-error
expect(spawn.start.firstCall.args[0]).to.include('--testing-type')
// @ts-expect-error
expect(spawn.start.firstCall.args[0]).to.include('component')
})
})
})

View File

@@ -0,0 +1,305 @@
import { vi, beforeEach, describe, it, expect } from 'vitest'
import os from 'os'
import path from 'path'
import tmp from 'tmp'
import fs from 'fs-extra'
import open from '../../lib/exec/open'
import run from '../../lib/exec/run'
import cypress from '../../lib/cypress'
vi.mock('fs-extra', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
// @ts-expect-error
...actual.default,
readJson: vi.fn(),
},
}
})
vi.mock('tmp', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
// @ts-expect-error
...actual.default,
fileSync: vi.fn(),
},
}
})
vi.mock('../../lib/exec/open', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../lib/exec/run', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
describe('cypress', function () {
beforeEach(function () {
vi.unstubAllEnvs()
vi.resetModules()
})
describe('.open', function () {
it('calls open#start, passing in options', async function () {
await cypress.open({ foo: 'foo' })
expect(open.start).toHaveBeenCalledWith({ foo: 'foo' })
})
it('normalizes config object', async () => {
const config = {
pageLoadTime: 10000,
watchForFileChanges: false,
}
await cypress.open({ config })
expect(open.start).toHaveBeenCalledWith({ config: JSON.stringify(config) })
})
})
describe('.run fails to write results file', function () {
it('resolves with error object', async function () {
const outputPath = path.join(os.tmpdir(), 'cypress/monorepo/cypress_spec/output.json')
// @ts-expect-error - mockImplementation
tmp.fileSync.mockReturnValue({
name: outputPath,
})
// @ts-expect-error - mockImplementation
run.start.mockResolvedValue(2)
// @ts-expect-error - mockImplementation
fs.readJson.mockImplementation(async (args) => {
if (args === outputPath) {
return Promise.resolve(undefined)
}
return Promise.reject(new Error('readJson not expected to fall through to this point'))
})
const results = await cypress.run()
expect(results).toEqual({
status: 'failed',
failures: 2,
message: 'Could not find Cypress test run results',
})
})
})
describe('.run', function () {
let outputPath: string
beforeEach(async function () {
outputPath = path.join(os.tmpdir(), 'cypress/monorepo/cypress_spec/output.json')
// @ts-expect-error
tmp.fileSync.mockReturnValue({
name: outputPath,
})
// @ts-expect-error
run.start.mockResolvedValue(undefined)
// @ts-expect-error - mockImplementation
fs.readJson.mockImplementation(async (args) => {
if (args === outputPath) {
return Promise.resolve({
code: 0,
failingTests: [],
})
}
return Promise.reject(new Error('readJson not expected to fall through to this point'))
})
})
it('calls run#start, passing in options', async () => {
await cypress.run({ spec: 'foo', autoCancelAfterFailures: 4 })
expect(run.start).toHaveBeenCalledWith({
outputPath,
spec: 'foo',
autoCancelAfterFailures: 4,
})
})
it('calls run#start, passing in autoCancelAfterFailures false', async () => {
await cypress.run({ autoCancelAfterFailures: false })
expect(run.start).toHaveBeenCalledWith({
outputPath,
autoCancelAfterFailures: false,
})
})
it('calls run#start, passing in autoCancelAfterFailures 0', async () => {
await cypress.run({ autoCancelAfterFailures: 0 })
expect(run.start).toHaveBeenCalledWith({
outputPath,
autoCancelAfterFailures: 0,
})
})
it('normalizes config object', async () => {
const config = {
pageLoadTime: 10000,
watchForFileChanges: false,
}
await cypress.run({ config })
expect(run.start).toHaveBeenCalledWith({
outputPath,
config: JSON.stringify(config),
})
})
it('normalizes env option if passed an object', async () => {
const env = { foo: 'bar', another: 'one' }
await cypress.run({ env })
expect(run.start).toHaveBeenCalledWith({
outputPath,
env: JSON.stringify(env),
})
})
it('gets random tmp file and passes it to run#start', async () => {
await cypress.run()
expect(run.start).toHaveBeenCalledWith(expect.objectContaining({
outputPath,
}))
})
it('resolves with contents of tmp file', async () => {
const results = await cypress.run()
expect(results).toMatchSnapshot()
})
it('rejects if project is an empty string', async () => {
await expect(cypress.run({ project: '' })).rejects.toThrow()
})
it('rejects if project is true', async () => {
await expect(cypress.run({ project: true })).rejects.toThrow()
})
it('rejects if project is false', async () => {
await expect(cypress.run({ project: false })).rejects.toThrow()
})
it('passes quiet: true', async () => {
await cypress.run({
quiet: true,
})
expect(run.start).toHaveBeenCalledWith(expect.objectContaining({
quiet: true,
}))
})
})
describe('cli', function () {
describe('.parseRunArguments', function () {
it('parses CLI cypress run arguments', async () => {
const args = 'cypress run --browser chrome --spec my/test/spec.js'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).toEqual({
browser: 'chrome',
spec: 'my/test/spec.js',
})
})
it('parses CLI cypress run shorthand arguments', async () => {
const args = 'cypress run -b firefox -p 5005 --headed --quiet'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).toEqual({
browser: 'firefox',
port: 5005,
headed: true,
quiet: true,
})
})
it('coerces --record and --dev', async () => {
const args = 'cypress run --record false --dev true'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).toEqual({
record: false,
dev: true,
})
})
it('coerces --config-file cypress.config.js to string', async () => {
const args = 'cypress run --config-file cypress.config.js'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).toEqual({
configFile: 'cypress.config.js',
})
})
it('parses config', async () => {
const args = 'cypress run --config baseUrl=localhost,video=true'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
// we don't need to convert the config into an object
// since the logic inside cypress.run handles that
expect(options).toEqual({
config: 'baseUrl=localhost,video=true',
})
})
it('parses env', async () => {
const args = 'cypress run --env MY_NUMBER=42,MY_FLAG=true'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
// we don't need to convert the environment into an object
// since the logic inside cypress.run handles that
expect(options).toEqual({
env: 'MY_NUMBER=42,MY_FLAG=true',
})
})
})
})
})

View File

@@ -1,254 +0,0 @@
import '../spec_helper'
import os from 'os'
import path from 'path'
import _ from 'lodash'
import snapshot from '../support/snapshot'
import BluebirdPromise from 'bluebird'
import tmpModule from 'tmp'
import mockfs from 'mock-fs'
import fs from '../../lib/fs'
import open from '../../lib/exec/open'
import run from '../../lib/exec/run'
import cypress from '../../lib/cypress'
const tmp = BluebirdPromise.promisifyAll(tmpModule)
describe('cypress', function () {
beforeEach(function () {
mockfs({})
})
afterEach(() => {
mockfs.restore()
})
context('.open', function () {
beforeEach(function () {
sinon.stub(open, 'start').resolves()
})
const getStartArgs = () => {
expect(open.start).to.be.called
return _.get(open.start, ['lastCall', 'args', 0])
}
it('calls open#start, passing in options', function () {
return cypress.open({ foo: 'foo' })
.then(getStartArgs)
.then((args: any) => {
expect(args.foo).to.equal('foo')
})
})
it('normalizes config object', () => {
const config = {
pageLoadTime: 10000,
watchForFileChanges: false,
}
return cypress.open({ config })
.then(getStartArgs)
.then((args: any) => {
expect(args).to.deep.eq({ config: JSON.stringify(config) })
})
})
})
context('.run fails to write results file', function () {
it('resolves with error object', function () {
const outputPath = path.join(os.tmpdir(), 'cypress/monorepo/cypress_spec/output.json')
// @ts-expect-error - type doesn't exist
sinon.stub(tmp, 'fileAsync').resolves(outputPath)
sinon.stub(run, 'start').resolves(2)
sinon.stub(fs, 'readJsonAsync').withArgs(outputPath).resolves()
return cypress.run().then((result: any) => {
expect(result).to.deep.equal({
status: 'failed',
failures: 2,
message: 'Could not find Cypress test run results',
})
})
})
})
context('.run', function () {
let outputPath: string
beforeEach(function () {
outputPath = path.join(os.tmpdir(), 'cypress/monorepo/cypress_spec/output.json')
// @ts-expect-error - type doesn't exist
sinon.stub(tmp, 'fileAsync').resolves(outputPath)
sinon.stub(run, 'start').resolves()
return fs.outputJsonAsync(outputPath, {
code: 0,
failingTests: [],
})
})
const normalizeCallArgs = (args: any) => {
expect(args.outputPath).to.equal(outputPath)
delete args.outputPath
return args
}
const getStartArgs = () => {
expect(run.start).to.be.called
return normalizeCallArgs(_.get(run.start, ['lastCall', 'args', 0]))
}
it('calls run#start, passing in options', () => {
return cypress.run({ spec: 'foo', autoCancelAfterFailures: 4 })
.then(getStartArgs)
.then((args: any) => {
expect(args.spec).to.equal('foo')
expect(args.autoCancelAfterFailures).to.equal(4)
expect(args.runnerUi).to.be.undefined
})
})
it('calls run#start, passing in autoCancelAfterFailures false', () => {
return cypress.run({ autoCancelAfterFailures: false })
.then(getStartArgs)
.then((args: any) => {
expect(args.autoCancelAfterFailures).to.equal(false)
})
})
it('calls run#start, passing in autoCancelAfterFailures 0', () => {
return cypress.run({ autoCancelAfterFailures: 0 })
.then(getStartArgs)
.then((args: any) => {
expect(args.autoCancelAfterFailures).to.equal(0)
})
})
it('normalizes config object', () => {
const config = {
pageLoadTime: 10000,
watchForFileChanges: false,
}
return cypress.run({ config })
.then(getStartArgs)
.then((args: any) => {
expect(args).to.deep.eq({ config: JSON.stringify(config) })
})
})
it('normalizes env option if passed an object', () => {
const env = { foo: 'bar', another: 'one' }
return cypress.run({ env })
.then(getStartArgs)
.then((args: any) => {
expect(args).to.deep.eq({ env: JSON.stringify(env) })
})
})
it('gets random tmp file and passes it to run#start', function () {
return cypress.run().then(() => {
expect((run.start as any).lastCall.args[0].outputPath).to.equal(outputPath)
})
})
it('resolves with contents of tmp file', () => {
return cypress.run().then(snapshot)
})
it('rejects if project is an empty string', () => {
return expect(cypress.run({ project: '' })).to.be.rejected
})
it('rejects if project is true', () => {
return expect(cypress.run({ project: true })).to.be.rejected
})
it('rejects if project is false', () => {
return expect(cypress.run({ project: false })).to.be.rejected
})
it('passes quiet: true', () => {
const opts = {
quiet: true,
}
return cypress.run(opts)
.then(getStartArgs)
.then((args: any) => {
expect(args).to.deep.eq(opts)
})
})
})
context('cli', function () {
describe('.parseRunArguments', function () {
it('parses CLI cypress run arguments', async () => {
const args = 'cypress run --browser chrome --spec my/test/spec.js'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
browser: 'chrome',
spec: 'my/test/spec.js',
})
})
it('parses CLI cypress run shorthand arguments', async () => {
const args = 'cypress run -b firefox -p 5005 --headed --quiet'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
browser: 'firefox',
port: 5005,
headed: true,
quiet: true,
})
})
it('coerces --record and --dev', async () => {
const args = 'cypress run --record false --dev true'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
record: false,
dev: true,
})
})
it('coerces --config-file cypress.config.js to string', async () => {
const args = 'cypress run --config-file cypress.config.js'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
expect(options).to.deep.equal({
configFile: 'cypress.config.js',
})
})
it('parses config', async () => {
const args = 'cypress run --config baseUrl=localhost,video=true'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
// we don't need to convert the config into an object
// since the logic inside cypress.run handles that
expect(options).to.deep.equal({
config: 'baseUrl=localhost,video=true',
})
})
it('parses env', async () => {
const args = 'cypress run --env MY_NUMBER=42,MY_FLAG=true'.split(' ')
const options = await cypress.cli.parseRunArguments(args)
// we don't need to convert the environment into an object
// since the logic inside cypress.run handles that
expect(options).to.deep.equal({
env: 'MY_NUMBER=42,MY_FLAG=true',
})
})
})
})
})

135
cli/test/lib/errors.spec.ts Normal file
View File

@@ -0,0 +1,135 @@
import { vi, describe, beforeEach, it, expect } from 'vitest'
import os from 'os'
import si from 'systeminformation'
import util from '../../lib/util'
import { errors, getError, formErrorText } from '../../lib/errors'
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
arch: vi.fn(),
release: vi.fn(),
},
}
})
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgVersion: vi.fn(),
},
}
})
describe('errors', function () {
beforeEach(() => {
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('test platform')
// @ts-expect-error mockReturnValue
os.arch.mockReturnValue('x64')
// @ts-expect-error mockReturnValue
os.release.mockReturnValue('release')
// @ts-expect-error mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
// @ts-expect-error mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
})
describe('individual', () => {
it('has the following errors', () => {
return expect(Object.keys(errors).sort()).toMatchSnapshot()
})
})
describe('getError', () => {
it('forms full message and creates Error object', async () => {
const errObject = errors.childProcessKilled('exit', 'SIGKILL')
expect(errObject).toMatchSnapshot()
const e = await getError(errObject)
expect(e).to.be.an('Error')
expect(e).to.have.property('known', true)
expect(e.message).toMatchSnapshot()
})
})
describe('.errors.formErrorText', function () {
it('returns fully formed text message', async () => {
const { missingXvfb } = errors
expect(missingXvfb).to.be.an('object')
const text: string = await formErrorText(missingXvfb)
expect(text).to.be.a('string')
expect(text).toMatchSnapshot()
})
it('calls solution if a function', async () => {
const solution = vi.fn().mockReturnValue('a solution')
const error = {
description: 'description',
solution,
}
const text: string = await formErrorText(error)
expect(text).toMatchSnapshot()
expect(solution).toHaveBeenCalledOnce()
})
it('passes message and previous message', async () => {
const solution = vi.fn().mockReturnValue('a solution')
const error = {
description: 'description',
solution,
}
await formErrorText(error, 'msg', 'prevMsg')
expect(solution).toHaveBeenCalledWith('msg', 'prevMsg')
})
it('expects solution to be a string', async () => {
const error = {
description: 'description',
solution: 42,
}
await expect(formErrorText(error)).rejects.toThrow()
})
it('forms full text for invalid display error', async () => {
const text: string = await formErrorText(errors.invalidSmokeTestDisplayError, 'current message', 'prev message')
expect(text).toMatchSnapshot()
})
})
})

View File

@@ -1,90 +0,0 @@
import '../spec_helper'
import os from 'os'
import snapshot from '../support/snapshot'
import util from '../../lib/util'
import { errors, getError, formErrorText } from '../../lib/errors'
describe('errors', function () {
const { missingXvfb } = errors
beforeEach(function (): void {
sinon.stub(util, 'pkgVersion').returns('1.2.3')
;(os.platform as any).returns('test platform')
})
describe('individual', () => {
it('has the following errors', () => {
return snapshot(Object.keys(errors).sort())
})
})
context('getError', () => {
it('forms full message and creates Error object', () => {
const errObject = errors.childProcessKilled('exit', 'SIGKILL')
snapshot('child kill error object', errObject)
return getError(errObject).then((e: any) => {
expect(e).to.be.an('Error')
expect(e).to.have.property('known', true)
snapshot('Error message', e.message)
})
})
})
context('.errors.formErrorText', function () {
it('returns fully formed text message', () => {
expect(missingXvfb).to.be.an('object')
return formErrorText(missingXvfb)
.then((text: string) => {
expect(text).to.be.a('string')
snapshot(text)
})
})
it('calls solution if a function', () => {
const solution = sinon.stub().returns('a solution')
const error = {
description: 'description',
solution,
}
return formErrorText(error)
.then((text: string) => {
snapshot(text)
expect(solution).to.have.been.calledOnce
})
})
it('passes message and previous message', () => {
const solution = sinon.stub().returns('a solution')
const error = {
description: 'description',
solution,
}
return formErrorText(error, 'msg', 'prevMsg')
.then(() => {
expect(solution).to.have.been.calledWithExactly('msg', 'prevMsg')
})
})
it('expects solution to be a string', () => {
const error = {
description: 'description',
solution: 42,
}
return expect(formErrorText(error)).to.be.rejected
})
it('forms full text for invalid display error', () => {
return formErrorText(errors.invalidSmokeTestDisplayError, 'current message', 'prev message')
.then((text: string) => {
snapshot('invalid display error', text)
})
})
})
})

View File

@@ -1,5 +1,45 @@
exports['cypress info without browsers or vars'] = `
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exec info > logs additional info about pre-releases > logs additional info about pre-releases 1`] = `
"
Proxy Settings: none detected
Environment Variables: none detected
Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (pre-release)
System Platform: linux (Foo - OsVersion)
System Memory: 1.2 GB free 400 MB
This is a pre-release build of Cypress.
Build info:
Commit SHA: abc123
Commit Branch: someBranchName
Commit Date: 2022-02-02T00:00:00.000Z
"
`;
exports[`exec info > logs if unbuilt development > logs additional info about development 1`] = `
"
Proxy Settings: none detected
Environment Variables: none detected
Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (pre-release)
System Platform: linux (Foo - OsVersion)
System Memory: 1.2 GB free 400 MB
This is the development (un-built) Cypress CLI.
"
`;
exports[`exec info > prints collected info without env vars > cypress info without browsers or vars 1`] = `
"
Proxy Settings: none detected
Environment Variables: none detected
@@ -8,16 +48,17 @@ Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (stable)
System Platform: linux (Foo-OsVersion)
System Platform: linux (Foo - OsVersion)
System Memory: 1.2 GB free 400 MB
"
`;
`
exports['cypress info with proxy and vars'] = `
exports[`exec info > prints proxy and cypress env vars > cypress info with proxy and vars 1`] = `
"
Proxy Settings:
PROXY_ENV_VAR1: some proxy variable
PROXY_ENV_VAR2: another proxy variable
HTTP_PROXY: some proxy variable
HTTPS_PROXY: another proxy variable
NO_PROXY: no proxy variable
Learn More: https://on.cypress.io/proxy-configuration
@@ -30,13 +71,13 @@ Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (stable)
System Platform: linux (Foo-OsVersion)
System Platform: linux (Foo - OsVersion)
System Memory: 1.2 GB free 400 MB
"
`;
`
exports['cypress redacts sensitive vars'] = `
exports[`exec info > redacts sensitive cypress variables > cypress redacts sensitive vars 1`] = `
"
Proxy Settings: none detected
Environment Variables:
CYPRESS_ENV_VAR1: my Cypress variable
@@ -49,45 +90,7 @@ Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (stable)
System Platform: linux (Foo-OsVersion)
System Platform: linux (Foo - OsVersion)
System Memory: 1.2 GB free 400 MB
`
exports['logs additional info about pre-releases'] = `
Proxy Settings: none detected
Environment Variables: none detected
Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (pre-release)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
This is a pre-release build of Cypress.
Build info:
Commit SHA: abc123
Commit Branch: someBranchName
Commit Date: 2022-02-02Txx:xx:xx.000Z
`
exports['logs additional info about development'] = `
Proxy Settings: none detected
Environment Variables: none detected
Application Data: /user/app/data/path
Browser Profiles: /user/app/data/path/to/browsers
Binary Caches: /user/path/to/binary/cache
Cypress Version: 0.0.0-development (pre-release)
System Platform: linux (Foo-OsVersion)
System Memory: 1.2 GB free 400 MB
This is the development (un-built) Cypress CLI.
`
"
`;

View File

@@ -0,0 +1,37 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exec run > .processRunOptions > defaults to e2e testingType 1`] = `
[
"--run-project",
undefined,
]
`;
exports[`exec run > .processRunOptions > does not remove --record option when using --browser 1`] = `
[
"--run-project",
undefined,
"--browser",
"test browser",
"--record",
"foo",
]
`;
exports[`exec run > .processRunOptions > passes --browser option 1`] = `
[
"--run-project",
undefined,
"--browser",
"test browser",
]
`;
exports[`exec run > .processRunOptions > passes --record option 1`] = `
[
"--run-project",
undefined,
"--record",
"my record id",
]
`;

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`lib/exec/spawn > .start > detects kill signal > exits with error on SIGKILL 1`] = `
"The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: darwin-x64 (Foo - OsVersion)
Cypress Version: 0.0.0-development"
`;
exports[`lib/exec/spawn > .start > does not force colors and streams when not supported 1`] = `
{
"DEBUG_COLORS": "0",
"FORCE_COLOR": "0",
"FORCE_STDERR_TTY": "0",
"FORCE_STDIN_TTY": "0",
"FORCE_STDOUT_TTY": "0",
}
`;
exports[`lib/exec/spawn > .start > forces colors and streams when supported 1`] = `
{
"DEBUG_COLORS": "1",
"FORCE_COLOR": "1",
"FORCE_STDERR_TTY": "1",
"FORCE_STDIN_TTY": "1",
"FORCE_STDOUT_TTY": "1",
"MOCHA_COLORS": "1",
}
`;

View File

@@ -0,0 +1,203 @@
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'
import os from 'os'
import { Console } from 'console'
import si from 'systeminformation'
import util from '../../../lib/util'
import state from '../../../lib/tasks/state'
import info from '../../../lib/exec/info'
import spawn from '../../../lib/exec/spawn'
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
totalmem: vi.fn(),
freemem: vi.fn(),
},
}
})
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('../../../lib/exec/spawn', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getApplicationDataFolder: vi.fn(),
pkgBuildInfo: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/state', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getCacheDir: vi.fn(),
},
}
})
describe('exec info', () => {
const createStdoutCapture = () => {
const logs: string[] = []
// eslint-disable-next-line no-console
const originalOut = process.stdout.write
vi.spyOn(process.stdout, 'write').mockImplementation((strOrBugger: string | Uint8Array<ArrayBufferLike>) => {
logs.push(strOrBugger as string)
return originalOut(strOrBugger)
})
return () => logs.join('')
}
// Direct console to process.stdout/stderr
let originalConsole: Console
beforeEach(() => {
originalConsole = globalThis.console
// Redirect console output to a custom stream or mock
globalThis.console = new Console(process.stdout, process.stderr)
vi.unstubAllEnvs()
vi.resetAllMocks()
vi.stubEnv('NO_PROXY', undefined)
vi.stubEnv('CYPRESS_COMMERCIAL_RECOMMENDATIONS', undefined)
// common stubs
// @ts-expect-error - mockReturnValue
spawn.start.mockResolvedValue(null)
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error - mockReturnValue
os.totalmem.mockReturnValue(1.2e+9)
// @ts-expect-error - mockReturnValue
os.freemem.mockReturnValue(4e+8)
// @ts-expect-error - mockImplementation
util.getApplicationDataFolder.mockImplementation((args) => {
if (args === 'browsers') {
return '/user/app/data/path/to/browsers'
}
return '/user/app/data/path'
})
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue({
stable: true,
})
// @ts-expect-error - mockReturnValue
state.getCacheDir.mockReturnValue('/user/path/to/binary/cache')
// @ts-expect-error - mockReturnValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
})
afterEach(() => {
globalThis.console = originalConsole // Restore original console
})
it('prints collected info without env vars', async () => {
const output = createStdoutCapture()
await info.start()
expect(output()).toMatchSnapshot('cypress info without browsers or vars')
expect(spawn.start).toBeCalledWith(['--mode=info'], { dev: undefined })
})
it('prints proxy and cypress env vars', async () => {
vi.stubEnv('HTTP_PROXY', 'some proxy variable')
vi.stubEnv('HTTPS_PROXY', 'another proxy variable')
vi.stubEnv('NO_PROXY', 'no proxy variable')
vi.stubEnv('CYPRESS_ENV_VAR1', 'my Cypress variable')
vi.stubEnv('CYPRESS_ENV_VAR2', 'my other Cypress variable')
const output = createStdoutCapture()
await info.start()
expect(output()).toMatchSnapshot('cypress info with proxy and vars')
})
it('redacts sensitive cypress variables', async () => {
vi.stubEnv('CYPRESS_ENV_VAR1', 'my Cypress variable')
vi.stubEnv('CYPRESS_ENV_VAR2', 'my other Cypress variable')
vi.stubEnv('CYPRESS_PROJECT_ID', 'abc123') // not sensitive
vi.stubEnv('CYPRESS_RECORD_KEY', 'really really secret stuff') // should not be printed
const output = createStdoutCapture()
await info.start()
expect(output()).toMatchSnapshot('cypress redacts sensitive vars')
})
it('logs additional info about pre-releases', async () => {
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue({
stable: false,
commitSha: 'abc123',
commitBranch: 'someBranchName',
commitDate: new Date('2022-02-02').toISOString(),
})
const output = createStdoutCapture()
await info.start()
expect(output()).toMatchSnapshot('logs additional info about pre-releases')
})
it('logs if unbuilt development', async () => {
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue(undefined)
const output = createStdoutCapture()
await info.start()
expect(output()).toMatchSnapshot('logs additional info about development')
})
})

View File

@@ -1,94 +0,0 @@
import '../../spec_helper'
import os from 'os'
import snapshot from '../../support/snapshot'
import stdout from '../../support/stdout'
import normalize from '../../support/normalize'
import util from '../../../lib/util'
import state from '../../../lib/tasks/state'
import info from '../../../lib/exec/info'
import spawn from '../../../lib/exec/spawn'
describe('exec info', function () {
beforeEach(function (): void {
sinon.stub(process, 'exit')
// common stubs
sinon.stub(spawn, 'start').resolves()
;(os.platform as any).returns('linux')
sinon.stub(os, 'totalmem').returns(1.2e+9)
sinon.stub(os, 'freemem').returns(4e+8)
sinon.stub(info, 'findProxyEnvironmentVariables').returns({})
sinon.stub(info, 'findCypressEnvironmentVariables').returns({})
sinon.stub(util, 'getApplicationDataFolder')
.withArgs('browsers').returns('/user/app/data/path/to/browsers')
.withArgs().returns('/user/app/data/path')
sinon.stub(util, 'pkgBuildInfo').returns({
stable: true,
})
sinon.stub(state, 'getCacheDir').returns('/user/path/to/binary/cache')
})
const startInfoAndSnapshot = async (snapshotName: string): Promise<void> => {
expect(snapshotName, 'missing snapshot name').to.be.a('string')
const output = stdout.capture()
await info.start()
stdout.restore()
snapshot(snapshotName, normalize(output.toString()))
}
it('prints collected info without env vars', async () => {
await startInfoAndSnapshot('cypress info without browsers or vars')
expect(spawn.start).to.be.calledWith(['--mode=info'], { dev: undefined })
})
it('prints proxy and cypress env vars', async () => {
info.findProxyEnvironmentVariables.returns({
PROXY_ENV_VAR1: 'some proxy variable',
PROXY_ENV_VAR2: 'another proxy variable',
})
info.findCypressEnvironmentVariables.returns({
CYPRESS_ENV_VAR1: 'my Cypress variable',
CYPRESS_ENV_VAR2: 'my other Cypress variable',
})
await startInfoAndSnapshot('cypress info with proxy and vars')
})
it('redacts sensitive cypress variables', async () => {
info.findCypressEnvironmentVariables.returns({
CYPRESS_ENV_VAR1: 'my Cypress variable',
CYPRESS_ENV_VAR2: 'my other Cypress variable',
CYPRESS_PROJECT_ID: 'abc123', // not sensitive
CYPRESS_RECORD_KEY: 'really really secret stuff', // should not be printed
})
await startInfoAndSnapshot('cypress redacts sensitive vars')
})
it('logs additional info about pre-releases', async () => {
// @ts-expect-error - is shorthand stub on a function
util.pkgBuildInfo.returns({
stable: false,
commitSha: 'abc123',
commitBranch: 'someBranchName',
commitDate: new Date('2022-02-02').toISOString(),
})
await startInfoAndSnapshot('logs additional info about pre-releases')
})
it('logs if unbuilt development', async () => {
// @ts-expect-error - is shorthand stub on a function
util.pkgBuildInfo.returns(undefined)
await startInfoAndSnapshot('logs additional info about development')
})
})

View File

@@ -0,0 +1,158 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import util from '../../../lib/util'
import { start as verifyStart } from '../../../lib/tasks/verify'
import spawn from '../../../lib/exec/spawn'
import open from '../../../lib/exec/open'
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
isInstalledGlobally: vi.fn(),
},
}
})
vi.mock('../../../lib/exec/spawn', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/verify', () => {
return {
start: vi.fn(),
}
})
describe('exec open', function () {
describe('.start', function () {
beforeEach(function (): void {
vi.clearAllMocks()
vi.unstubAllEnvs()
// @ts-expect-error - mockReturnValue
util.isInstalledGlobally.mockReturnValue(true)
// @ts-expect-error - mockResolvedValue
verifyStart.mockResolvedValue(undefined)
// @ts-expect-error - mockResolvedValue
spawn.start.mockResolvedValue(undefined)
})
it('verifies download', async () => {
await open.start()
expect(verifyStart).toHaveBeenCalled()
})
it('calls spawn with correct options', async () => {
await open.start({ dev: true })
expect(spawn.start).toHaveBeenCalledWith([], {
detached: false,
dev: true,
})
})
it('spawns with port', async () => {
await open.start({ port: '1234' })
expect(spawn.start).toHaveBeenCalledWith(['--port', '1234'], expect.anything())
})
it('spawns with --env', async () => {
await open.start({ env: 'host=http://localhost:1337,name=brian' })
expect(spawn.start).toHaveBeenCalledWith(
['--env', 'host=http://localhost:1337,name=brian'],
expect.anything(),
)
})
it('spawns with --config', async () => {
await open.start({ config: 'watchForFileChanges=false,baseUrl=localhost' })
expect(spawn.start).toHaveBeenCalledWith(
['--config', 'watchForFileChanges=false,baseUrl=localhost'],
expect.anything(),
)
})
it('spawns with --config-file set', async () => {
await open.start({ configFile: 'special-cypress.config.js' })
expect(spawn.start).toHaveBeenCalledWith(
['--config-file', 'special-cypress.config.js'],
expect.anything(),
)
})
it('spawns with cwd as --project if not installed globally', async () => {
// @ts-expect-error - is shorthand stub on a function
util.isInstalledGlobally.mockReturnValue(false)
await open.start()
expect(spawn.start).toHaveBeenCalledWith(['--project', process.cwd()], expect.anything())
})
it('spawns without --project if not installed globally and passing --global option', async () => {
// @ts-expect-error - is shorthand stub on a function
util.isInstalledGlobally.mockReturnValue(false)
await open.start({ global: true })
expect(spawn.start).not.toHaveBeenCalledWith(
['--project', process.cwd()],
)
})
it('spawns with --project passed in as options even when not installed globally', async () => {
// @ts-expect-error - is shorthand stub on a function
util.isInstalledGlobally.mockReturnValue(false)
await open.start({ project: '/path/to/project' })
expect(spawn.start).toHaveBeenCalledWith(
['--project', '/path/to/project'],
expect.anything(),
)
})
it('spawns with --project if specified and installed globally', async () => {
await open.start({ project: '/path/to/project' })
expect(spawn.start).toHaveBeenCalledWith(
['--project', '/path/to/project'],
expect.anything(),
)
})
it('spawns without --project if not specified and installed globally', async () => {
await open.start()
expect(spawn.start).toHaveBeenCalledWith([], expect.anything())
})
it('spawns without --testing-type when not specified', async () => {
await open.start()
expect(spawn.start).toHaveBeenCalledWith([], expect.anything())
})
it('spawns with --testing-type e2e', async () => {
await open.start({ testingType: 'e2e' })
expect(spawn.start).toHaveBeenCalledWith(['--testing-type', 'e2e'], expect.anything())
})
it('spawns with --testing-type component', async () => {
await open.start({ testingType: 'component' })
expect(spawn.start).toHaveBeenCalledWith(['--testing-type', 'component'], expect.anything())
})
it('throws if --testing-type is invalid', () => {
expect(() => open.processOpenOptions({ testingType: 'randomTestingType' })).toThrow()
})
it('throws if --config-file is false', () => {
expect(() => open.processOpenOptions({ configFile: 'false' })).toThrow()
})
})
})

View File

@@ -1,145 +0,0 @@
import '../../spec_helper'
import util from '../../../lib/util'
import verify from '../../../lib/tasks/verify'
import spawn from '../../../lib/exec/spawn'
import open from '../../../lib/exec/open'
describe('exec open', function () {
context('.start', function () {
beforeEach(function (): void {
sinon.stub(util, 'isInstalledGlobally').returns(true)
sinon.stub(verify, 'start').resolves()
sinon.stub(spawn, 'start').resolves()
})
it('verifies download', function () {
return open.start()
.then(() => {
expect(verify.start).to.be.called
})
})
it('calls spawn with correct options', function () {
return open.start({ dev: true })
.then(() => {
expect(spawn.start).to.be.calledWith([], {
detached: false,
dev: true,
})
})
})
it('spawns with port', function () {
return open.start({ port: '1234' })
.then(() => {
expect(spawn.start).to.be.calledWith(['--port', '1234'])
})
})
it('spawns with --env', function () {
return open.start({ env: 'host=http://localhost:1337,name=brian' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--env', 'host=http://localhost:1337,name=brian'],
)
})
})
it('spawns with --config', function () {
return open.start({ config: 'watchForFileChanges=false,baseUrl=localhost' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--config', 'watchForFileChanges=false,baseUrl=localhost'],
)
})
})
it('spawns with --config-file set', function () {
return open.start({ configFile: 'special-cypress.config.js' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--config-file', 'special-cypress.config.js'],
)
})
})
it('spawns with cwd as --project if not installed globally', function () {
// @ts-expect-error - is shorthand stub on a function
util.isInstalledGlobally.returns(false)
return open.start()
.then(() => {
expect(spawn.start).to.be.calledWith(
['--project', process.cwd()],
)
})
})
it('spawns without --project if not installed globally and passing --global option', function () {
// @ts-expect-error - is shorthand stub on a function
util.isInstalledGlobally.returns(false)
return open.start({ global: true })
.then(() => {
expect(spawn.start).not.to.be.calledWith(
['--project', process.cwd()],
)
})
})
it('spawns with --project passed in as options even when not installed globally', function () {
// @ts-expect-error - is shorthand stub on a function
util.isInstalledGlobally.returns(false)
return open.start({ project: '/path/to/project' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--project', '/path/to/project'],
)
})
})
it('spawns with --project if specified and installed globally', function () {
return open.start({ project: '/path/to/project' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--project', '/path/to/project'],
)
})
})
it('spawns without --project if not specified and installed globally', function () {
return open.start()
.then(() => {
expect(spawn.start).to.be.calledWith([])
})
})
it('spawns without --testing-type when not specified', () => {
return open.start().then(() => {
expect(spawn.start).to.be.calledWith([])
})
})
it('spawns with --testing-type e2e', () => {
return open.start({ testingType: 'e2e' }).then(() => {
expect(spawn.start).to.be.calledWith(['--testing-type', 'e2e'])
})
})
it('spawns with --testing-type component', () => {
return open.start({ testingType: 'component' }).then(() => {
expect(spawn.start).to.be.calledWith(['--testing-type', 'component'])
})
})
it('throws if --testing-type is invalid', () => {
expect(() => open.processOpenOptions({ testingType: 'randomTestingType' })).to.throw()
})
it('throws if --config-file is false', () => {
expect(() => open.processOpenOptions({ configFile: 'false' })).to.throw()
})
})
})

View File

@@ -0,0 +1,243 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import os from 'os'
import util from '../../../lib/util'
import run from '../../../lib/exec/run'
import spawn from '../../../lib/exec/spawn'
import { start as verifyStart } from '../../../lib/tasks/verify'
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
isInstalledGlobally: vi.fn(),
},
}
})
vi.mock('../../../lib/exec/spawn', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/verify', () => {
return {
start: vi.fn(),
}
})
describe('exec run', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.unstubAllEnvs()
// @ts-expect-error - mockReturnValue
util.isInstalledGlobally.mockReturnValue(true)
})
describe('.processRunOptions', () => {
it('allows string --project option', () => {
const args = run.processRunOptions({
project: '/path/to/project',
})
expect(args).toEqual(['--run-project', '/path/to/project'])
})
it('throws an error for empty string --project', () => {
expect(() => run.processRunOptions({ project: '' })).toThrow()
})
it('throws an error for boolean --project', () => {
expect(() => run.processRunOptions({ project: false })).toThrow()
expect(() => run.processRunOptions({ project: true })).toThrow()
})
it('throws an error for --project "false" or "true"', () => {
expect(() => run.processRunOptions({ project: 'false' })).toThrow()
expect(() => run.processRunOptions({ project: 'true' })).toThrow()
})
it('passes --browser option', () => {
const args = run.processRunOptions({
browser: 'test browser',
})
expect(args).toMatchSnapshot()
})
it('passes --record option', () => {
const args = run.processRunOptions({
record: 'my record id',
})
expect(args).toMatchSnapshot()
})
it('does not allow setting paradoxical --headed and --headless flags', () => {
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('linux')
expect(() => run.processRunOptions({ headed: true, headless: true })).toThrow()
})
it('passes --headed according to --headless', () => {
expect(run.processRunOptions({ headless: true })).toEqual([
'--run-project', undefined, '--headed', 'false',
])
})
it('does not remove --record option when using --browser', () => {
const args = run.processRunOptions({
record: 'foo',
browser: 'test browser',
})
expect(args).toMatchSnapshot()
})
it('defaults to e2e testingType', () => {
const args = run.processRunOptions()
expect(args).toMatchSnapshot()
})
it('passes e2e testingType', () => {
expect(run.processRunOptions({ testingType: 'e2e' })).toEqual([
'--run-project', undefined, '--testing-type', 'e2e',
])
})
it('passes component testingType', () => {
expect(run.processRunOptions({ testingType: 'component' })).toEqual([
'--run-project', undefined, '--testing-type', 'component',
])
})
it('throws if testingType is invalid', () => {
expect(() => run.processRunOptions({ testingType: 'randomTestingType' })).toThrow()
})
it('throws if both e2e and component are set', () => {
expect(() => run.processRunOptions({ e2e: true, component: true })).toThrow()
})
it('throws if both testingType and component are set', () => {
expect(() => run.processRunOptions({ testingType: 'component', component: true })).toThrow()
})
it('throws if --config-file is false', () => {
expect(() => run.processRunOptions({ configFile: 'false' })).toThrow()
})
})
describe('.start', () => {
beforeEach(() => {
// @ts-expect-error - mockResolvedValue
spawn.start.mockResolvedValue(undefined)
// @ts-expect-error - mockResolvedValue
verifyStart.mockResolvedValue(undefined)
})
it('verifies cypress', async () => {
await run.start()
expect(verifyStart).toHaveBeenCalledOnce()
})
it('spawns with --key and xvfb', async () => {
await run.start({ port: '1234' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--port', '1234'], expect.anything())
})
it('spawns with --env', async () => {
await run.start({ env: 'host=http://localhost:1337,name=brian' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--env', 'host=http://localhost:1337,name=brian'], expect.anything())
})
it('spawns with --config', async () => {
await run.start({ config: 'watchForFileChanges=false,baseUrl=localhost' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--config', 'watchForFileChanges=false,baseUrl=localhost'], expect.anything())
})
it('spawns with --config-file set', async () => {
await run.start({ configFile: 'special-cypress.config.js' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--config-file', 'special-cypress.config.js'], expect.anything())
})
it('spawns with --record false', async () => {
await run.start({ record: false })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--record', false], expect.anything())
})
it('spawns with --headed true', async () => {
await run.start({ headed: true })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--headed', true], expect.anything())
})
it('spawns with --no-exit', async () => {
await run.start({ exit: false })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--no-exit'], expect.anything())
})
it('spawns with --output-path', async () => {
await run.start({ outputPath: '/path/to/output' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--output-path', '/path/to/output'], expect.anything())
})
it('spawns with --testing-type e2e when given --e2e', async () => {
await run.start({ e2e: true })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--testing-type', 'e2e'], expect.anything())
})
it('spawns with --testing-type component when given --component', async () => {
await run.start({ component: true })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--testing-type', 'component'], expect.anything())
})
it('spawns with --tag value', async () => {
await run.start({ tag: 'nightly' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--tag', 'nightly'], expect.anything())
})
it('spawns with several --tag words unchanged', async () => {
await run.start({ tag: 'nightly, sanity' })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--tag', 'nightly, sanity'], expect.anything())
})
it('spawns with --auto-cancel-after-failures value', async () => {
await run.start({ autoCancelAfterFailures: 4 })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--auto-cancel-after-failures', 4], expect.anything())
})
it('spawns with --auto-cancel-after-failures value false', async () => {
await run.start({ autoCancelAfterFailures: false })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--auto-cancel-after-failures', false], expect.anything())
})
it('spawns with --runner-ui', async () => {
await run.start({ runnerUi: true })
expect(spawn.start).toHaveBeenCalledWith(['--run-project', process.cwd(), '--runner-ui', true], expect.anything())
})
})
})

View File

@@ -1,246 +0,0 @@
import '../../spec_helper'
import os from 'os'
import snapshot from '../../support/snapshot'
import util from '../../../lib/util'
import run from '../../../lib/exec/run'
import spawn from '../../../lib/exec/spawn'
import verify from '../../../lib/tasks/verify'
describe('exec run', function () {
beforeEach(function () {
sinon.stub(util, 'isInstalledGlobally').returns(true)
sinon.stub(process, 'exit')
})
context('.processRunOptions', function () {
it('allows string --project option', () => {
const args = run.processRunOptions({
project: '/path/to/project',
})
expect(args).to.deep.equal(['--run-project', '/path/to/project'])
})
it('throws an error for empty string --project', () => {
expect(() => run.processRunOptions({ project: '' })).to.throw()
})
it('throws an error for boolean --project', () => {
expect(() => run.processRunOptions({ project: false })).to.throw()
expect(() => run.processRunOptions({ project: true })).to.throw()
})
it('throws an error for --project "false" or "true"', () => {
expect(() => run.processRunOptions({ project: 'false' })).to.throw()
expect(() => run.processRunOptions({ project: 'true' })).to.throw()
})
it('passes --browser option', () => {
const args = run.processRunOptions({
browser: 'test browser',
})
snapshot(args)
})
it('passes --record option', () => {
const args = run.processRunOptions({
record: 'my record id',
})
snapshot(args)
})
it('does not allow setting paradoxical --headed and --headless flags', () => {
(os.platform as any).returns('linux')
;(process.exit as any).returns()
expect(() => run.processRunOptions({ headed: true, headless: true })).to.throw()
})
it('passes --headed according to --headless', () => {
expect(run.processRunOptions({ headless: true })).to.deep.eq([
'--run-project', undefined, '--headed', 'false',
])
})
it('does not remove --record option when using --browser', () => {
const args = run.processRunOptions({
record: 'foo',
browser: 'test browser',
})
snapshot(args)
})
it('defaults to e2e testingType', () => {
const args = run.processRunOptions()
snapshot(args)
})
it('passes e2e testingType', () => {
expect(run.processRunOptions({ testingType: 'e2e' })).to.deep.eq([
'--run-project', undefined, '--testing-type', 'e2e',
])
})
it('passes component testingType', () => {
expect(run.processRunOptions({ testingType: 'component' })).to.deep.eq([
'--run-project', undefined, '--testing-type', 'component',
])
})
it('throws if testingType is invalid', () => {
expect(() => run.processRunOptions({ testingType: 'randomTestingType' })).to.throw()
})
it('throws if both e2e and component are set', () => {
expect(() => run.processRunOptions({ e2e: true, component: true })).to.throw()
})
it('throws if both testingType and component are set', () => {
expect(() => run.processRunOptions({ testingType: 'component', component: true })).to.throw()
})
it('throws if --config-file is false', () => {
expect(() => run.processRunOptions({ configFile: 'false' })).to.throw()
})
})
context('.start', function () {
beforeEach(function () {
sinon.stub(spawn, 'start').resolves()
sinon.stub(verify, 'start').resolves()
})
it('verifies cypress', function () {
return run.start()
.then(() => {
expect(verify.start).to.be.calledOnce
})
})
it('spawns with --key and xvfb', function () {
return run.start({ port: '1234' })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--port', '1234'])
})
})
it('spawns with --env', function () {
return run.start({ env: 'host=http://localhost:1337,name=brian' })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--env', 'host=http://localhost:1337,name=brian'])
})
})
it('spawns with --config', function () {
return run.start({ config: 'watchForFileChanges=false,baseUrl=localhost' })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--config', 'watchForFileChanges=false,baseUrl=localhost'])
})
})
it('spawns with --config-file set', function () {
return run.start({ configFile: 'special-cypress.config.js' })
.then(() => {
expect(spawn.start).to.be.calledWith(
['--run-project', process.cwd(), '--config-file', 'special-cypress.config.js'],
)
})
})
it('spawns with --record false', function () {
return run.start({ record: false })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--record', false])
})
})
it('spawns with --headed true', function () {
return run.start({ headed: true })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--headed', true,
])
})
})
it('spawns with --no-exit', function () {
return run.start({ exit: false })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--no-exit',
])
})
})
it('spawns with --output-path', function () {
return run.start({ outputPath: '/path/to/output' })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--output-path', '/path/to/output'])
})
})
it('spawns with --testing-type e2e when given --e2e', function () {
return run.start({ e2e: true })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--testing-type', 'e2e'])
})
})
it('spawns with --testing-type component when given --component', function () {
return run.start({ component: true })
.then(() => {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--testing-type', 'component'])
})
})
it('spawns with --tag value', function () {
return run.start({ tag: 'nightly' })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--tag', 'nightly',
])
})
})
it('spawns with several --tag words unchanged', function () {
return run.start({ tag: 'nightly, sanity' })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--tag', 'nightly, sanity',
])
})
})
it('spawns with --auto-cancel-after-failures value', function () {
return run.start({ autoCancelAfterFailures: 4 })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--auto-cancel-after-failures', 4,
])
})
})
it('spawns with --auto-cancel-after-failures value false', function () {
return run.start({ autoCancelAfterFailures: false })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--auto-cancel-after-failures', false,
])
})
})
it('spawns with --runner-ui', function () {
return run.start({ runnerUi: true })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--runner-ui', true,
])
})
})
})
})

View File

@@ -0,0 +1,796 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import cp from 'child_process'
import os from 'os'
import tty from 'tty'
import path from 'path'
import treeKill from 'tree-kill'
import si from 'systeminformation'
import { EventEmitter as EE } from 'events'
import readline from 'readline'
import createDebug from 'debug'
import { stdin, stdout, stderr } from 'process'
import state from '../../../lib/tasks/state'
import xvfb from '../../../lib/exec/xvfb'
import spawn from '../../../lib/exec/spawn'
import { needsSandbox } from '../../../lib/tasks/verify'
import util from '../../../lib/util'
const flushPromises = () => {
return new Promise<void>((resolve) => {
setTimeout(resolve, 100)
})
}
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
arch: vi.fn(),
},
}
})
vi.mock('readline', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
createInterface: vi.fn(),
},
}
})
vi.mock('process', async (importActual) => {
const actual = await importActual()
return {
stdin: {
// @ts-expect-error
...actual.stdin,
pipe: vi.fn(),
on: vi.fn(),
emit: vi.fn(),
},
stdout: vi.fn(),
stderr: {
// @ts-expect-error
...actual.stderr,
write: vi.fn(),
},
default: {
// @ts-expect-error
...actual.default,
stdin: {
// @ts-expect-error
...actual.default.stdin,
pipe: vi.fn(),
on: vi.fn(),
},
stdout: vi.fn(),
stderr: {
// @ts-expect-error
...actual.default.stderr,
write: vi.fn(),
},
},
}
})
vi.mock('child_process', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
spawn: vi.fn(),
},
}
})
vi.mock('tty', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
isatty: vi.fn(),
},
}
})
vi.mock('tree-kill', () => {
return {
default: vi.fn(),
}
})
vi.mock('../../../lib/exec/xvfb', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
stop: vi.fn(),
isNeeded: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/state', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getBinaryDir: vi.fn(),
getPathToExecutable: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/verify', async () => {
return {
needsSandbox: vi.fn(),
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
supportsColor: vi.fn(),
},
}
})
const debug = createDebug('test')
const cwd = process.cwd()
const execPath = process.execPath
const nodeVersion = process.versions.node
const defaultBinaryDir = '/default/binary/dir'
describe('lib/exec/spawn', function () {
let spawnedProcess: any
let mockReadlineEE: any
beforeEach(function () {
vi.resetAllMocks()
vi.unstubAllEnvs()
vi.stubEnv('DISPLAY', undefined)
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
os.arch.mockReturnValue('x64')
// @ts-expect-error mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
spawnedProcess = new EE()
spawnedProcess.unref = vi.fn().mockReturnValue(undefined)
spawnedProcess.stdin = {
on: vi.fn().mockReturnValue(undefined),
pipe: vi.fn().mockReturnValue(undefined),
}
spawnedProcess.stdout = {
on: vi.fn().mockReturnValue(undefined),
pipe: vi.fn().mockReturnValue(undefined),
}
spawnedProcess.stderr = {
pipe: vi.fn().mockReturnValue(undefined),
on: vi.fn().mockReturnValue(undefined),
}
spawnedProcess.kill = vi.fn()
mockReadlineEE = new EE()
// @ts-expect-error - mockReturnValue
readline.createInterface.mockReturnValue(mockReadlineEE)
// @ts-expect-error - mockReturnValue
cp.spawn.mockReturnValue(spawnedProcess)
// @ts-expect-error - mockReturnValue
xvfb.start.mockResolvedValue(undefined)
// @ts-expect-error - mockReturnValue
xvfb.stop.mockResolvedValue(undefined)
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
state.getBinaryDir.mockReturnValue(defaultBinaryDir)
// @ts-expect-error - mockImplementation
state.getPathToExecutable.mockImplementation((args) => {
if (args === '/default/binary/dir') {
return '/path/to/cypress'
}
})
})
describe('.start', function () {
// ️️⚠️ NOTE ⚠️
// when asserting the calls made to spawn the child Cypress process
// we have to be _very_ careful. Spawn uses process.env object, if an assertion
// fails, it will print the entire process.env object to the logs, which
// might contain sensitive environment variables. Think about what the
// failed assertion might print to the public CI logs and limit
// the environment variables when running tests on CI.
it('passes args + options to spawn', async () => {
// @ts-expect-error - mockReturnValue
needsSandbox.mockReturnValue(false)
// start the process
const startPromise = spawn.start('--foo', { foo: 'bar' })
// simulate the process closing successfully
spawnedProcess.emit('close', 0)
// await the process to complete and return
await startPromise
expect(cp.spawn).toHaveBeenCalledWith('/path/to/cypress', [
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], expect.objectContaining({
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
}))
})
it('uses --no-sandbox when needed', async function () {
// @ts-expect-error - mockReturnValue
needsSandbox.mockReturnValue(true)
const startPromise = spawn.start('--foo', { foo: 'bar' })
spawnedProcess.emit('close', 0)
await startPromise
// skip the options argument: we do not need anything about it
// and also less risk that a failed assertion would dump the
// entire ENV object with possible sensitive variables
// @ts-expect-error - vitest mock
const args = cp.spawn.mock.calls[0].slice(0, 2)
// it is important for "--no-sandbox" to appear before "--" separator
const expectedCliArgs = [
'--no-sandbox',
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
]
expect(args).toEqual(['/path/to/cypress', expectedCliArgs])
})
it('uses npm command when running in dev mode', async () => {
// @ts-expect-error - mockReturnValue
needsSandbox.mockReturnValue(false)
const startPromise = spawn.start('--foo', { dev: true, foo: 'bar' })
spawnedProcess.emit('close', 0)
await startPromise
const p = path.resolve('..', 'scripts', 'start.js')
expect(cp.spawn).toHaveBeenCalledWith('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], expect.objectContaining({
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
}))
})
it('does not pass --no-sandbox when running in dev mode', async function () {
// @ts-expect-error - mockReturnValue
needsSandbox.mockReturnValue(true)
const startPromise = spawn.start('--foo', { dev: true, foo: 'bar' })
spawnedProcess.emit('close', 0)
await startPromise
const p = path.resolve('..', 'scripts', 'start.js')
expect(cp.spawn).toHaveBeenCalledWith('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], expect.objectContaining({
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
}))
})
it('starts xvfb when needed', async () => {
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(true)
const startPromise = spawn.start('--foo')
await flushPromises()
spawnedProcess.emit('close', 0)
await startPromise
expect(xvfb.start).toBeCalled()
})
describe('closes', function () {
['close', 'exit'].forEach((event) => {
it(`if '${event}' event fired`, async () => {
const startPromise = spawn.start('--foo')
spawnedProcess.emit(event, 0)
const code = await startPromise
expect(code).toEqual(0)
})
})
it('if exit event fired and close event fired', async () => {
const startPromise = spawn.start('--foo')
spawnedProcess.emit('exit', 0)
spawnedProcess.emit('close', 0)
const code = await startPromise
expect(code).toEqual(0)
})
})
describe('detects kill signal', async () => {
it('exits with error on SIGKILL', async () => {
try {
const startPromise = spawn.start('--foo')
spawnedProcess.emit('exit', null, 'SIGKILL')
await startPromise
throw new Error('should have hit error handler but did not')
} catch (e) {
expect(e.message).toMatch(/SIGKILL/)
expect(e.message).toMatchSnapshot()
}
})
})
it('does not start xvfb when its not needed', async () => {
const startPromise = spawn.start('--foo')
await flushPromises()
spawnedProcess.emit('close', 0)
await startPromise
expect(xvfb.start).not.toBeCalled()
})
it('stops xvfb when spawn closes', async () => {
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(true)
const startPromise = spawn.start('--foo')
await flushPromises()
spawnedProcess.emit('close', 0)
await startPromise
expect(xvfb.stop).toBeCalled()
})
it('resolves with spawned close code in the message', async () => {
const startPromise = spawn.start('--foo')
spawnedProcess.emit('close', 10)
const code = await startPromise
expect(code).to.equal(10)
})
describe('Linux display', () => {
beforeEach(() => {
vi.stubEnv('DISPLAY', 'test-display')
})
it('retries with xvfb if fails with display exit code', async () => {
// mock display missing
spawnedProcess.stderr.on.mockImplementation((event, callback) => {
if (event === 'data') {
callback('[some noise here] Gtk: cannot open display: 987')
}
})
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
const startPromise = spawn.start('--foo')
// mock display error due to missing display
spawnedProcess.emit('close', 1)
// mock the process actually starting up after xfvb is started
await flushPromises()
spawnedProcess.emit('close', 0)
const code = await startPromise
expect(xvfb.start).toHaveBeenCalledOnce()
expect(xvfb.stop).toHaveBeenCalledOnce()
expect(cp.spawn).toHaveBeenCalledTimes(2)
// second code should be 0 after successfully running with Xvfb
expect(code).toEqual(0)
})
})
it('rejects with error from spawn', async () => {
const msg = 'the error message'
const startPromise = spawn.start('--foo')
spawnedProcess.emit('error', new Error(msg))
try {
await startPromise
throw new Error('should have hit error handler but did not')
} catch (e) {
debug('error message', e.message)
expect(e.message).toMatch(msg)
}
})
it('unrefs if options.detached is true', async () => {
const startPromise = spawn.start(null, { detached: true })
spawnedProcess.emit('close', 0)
await startPromise
expect(spawnedProcess.unref).toHaveBeenCalledOnce()
})
it('does not unref by default', async () => {
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
spawnedProcess.emit('close', 0)
await startPromise
expect(spawnedProcess.unref).not.toHaveBeenCalled()
})
it('sets process.env to options.env', async () => {
vi.stubEnv('FOO', 'bar')
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.env.FOO).toEqual('bar')
})
it('forces colors and streams when supported', async () => {
// @ts-expect-error - mockReturnValue
util.supportsColor.mockReturnValue(true)
// @ts-expect-error - mockReturnValue
tty.isatty.mockReturnValue(true)
const startPromise = spawn.start([], { env: {} })
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.env).toMatchSnapshot()
})
it('sets windowsHide:false property in windows', async () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
const startPromise = spawn.start([], { env: {} })
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.windowsHide).toEqual(false)
})
it('propagates treeKill if SIGINT is detected in windows console', async () => {
spawnedProcess.pid = 7
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
const startPromise = spawn.start([], { env: {} })
spawnedProcess.emit('close', 0)
await startPromise
mockReadlineEE.emit('SIGINT')
// since the import of tree-kill is async inside spawn, we need to wait for it to be imported and called
await flushPromises()
expect(treeKill).toHaveBeenCalledWith(7, 'SIGINT')
})
it('does not set windowsHide property when in darwin', async () => {
const startPromise = spawn.start([], { env: {} })
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.windowsHide).toBeUndefined()
})
it('does not force colors and streams when not supported', async () => {
// @ts-expect-error - mockReturnValue
util.supportsColor.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
tty.isatty.mockReturnValue(false)
const startPromise = spawn.start([], { env: {} })
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.env).toMatchSnapshot()
})
it('pipes when on win32', async () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.stdio).toEqual('pipe')
expect(stdin.pipe).toHaveBeenCalledOnce()
expect(stdin.pipe).toHaveBeenCalledWith(spawnedProcess.stdin)
})
it('inherits when on linux and xvfb isn\'t needed', async () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.stdio).toEqual('inherit')
})
it('uses [inherit, inherit, pipe] when linux and xvfb is needed', async () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(true)
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
await flushPromises()
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.stdio).toEqual(['inherit', 'inherit', 'pipe'])
})
it('uses [inherit, inherit, pipe] on darwin', async () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
await flushPromises()
spawnedProcess.emit('close', 0)
await startPromise
// @ts-expect-error - mock argument
const thirdArg = cp.spawn.mock.calls[0][2]
expect(thirdArg.stdio).to.deep.eq([
'inherit', 'inherit', 'pipe',
])
})
it('writes everything on win32', async () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
const buf1 = Buffer.from('asdf')
// mock display missing
spawnedProcess.stderr.on.mockImplementation((event, callback) => {
if (event === 'data') {
callback(buf1)
}
})
// @ts-expect-error - invalid number of arguments for given type
const startPromise = spawn.start()
spawnedProcess.emit('close', 0)
await startPromise
// validates the child process stderr event handler was called
expect(stderr.write).toHaveBeenCalledWith(buf1)
expect(stdin.pipe).toHaveBeenCalledExactlyOnceWith(spawnedProcess.stdin)
expect(spawnedProcess.stdout.pipe).toHaveBeenCalledExactlyOnceWith(stdout)
})
// https://github.com/cypress-io/cypress/issues/1841
// https://github.com/cypress-io/cypress/issues/5241
const errCodes = ['EPIPE', 'ENOTCONN']
errCodes.forEach((errCode) => {
beforeEach(() => {
// create an EventEmitter and bind it to process.stdin
const stdinEE = new EE()
// @ts-expect-error - mockImplementation
stdin.emit.mockImplementation((event, ...args) => {
stdinEE.emit(event, ...args)
})
// @ts-expect-error - mockImplementation
stdin.on.mockImplementation((event, callback) => {
return stdinEE.on(event, callback)
})
})
it(`catches process.stdin errors and returns when code=${errCode}`, async () => {
expect(() => {
// kick off the mock process
// @ts-expect-error - invalid number of arguments for given type
spawn.start()
const err: any = new Error()
err.code = errCode
return stdin.emit('error', err)
}).not.toThrow()
})
})
it('throws process.stdin errors code!=EPIPE', function () {
expect(() => {
// kick off the mock process
// @ts-expect-error - invalid number of arguments for given type
spawn.start()
const err: any = new Error('wattttt')
err.code = 'FAILWHALE'
return stdin.emit('error', err)
}).toThrow(/wattttt/)
})
})
})

View File

@@ -1,525 +0,0 @@
import '../../spec_helper'
import cp from 'child_process'
import os from 'os'
import tty from 'tty'
import path from 'path'
import { EventEmitter as EE } from 'events'
import mockedEnv from 'mocked-env'
import readline from 'readline'
import createDebug from 'debug'
import snapshot from '../../support/snapshot'
import state from '../../../lib/tasks/state'
import xvfb from '../../../lib/exec/xvfb'
import spawn from '../../../lib/exec/spawn'
import verify from '../../../lib/tasks/verify'
import util from '../../../lib/util'
const debug = createDebug('test')
const cwd = process.cwd()
const execPath = process.execPath
const nodeVersion = process.versions.node
const defaultBinaryDir = '/default/binary/dir'
let mockReadlineEE: any
describe('lib/exec/spawn', function () {
beforeEach(function () {
(os.platform as any).returns('darwin')
sinon.stub(process, 'exit')
;(this as any).spawnedProcess = {
on: sinon.stub().returns(undefined),
unref: sinon.stub().returns(undefined),
stdin: {
on: sinon.stub().returns(undefined),
pipe: sinon.stub().returns(undefined),
},
stdout: {
on: sinon.stub().returns(undefined),
pipe: sinon.stub().returns(undefined),
},
stderr: {
pipe: sinon.stub().returns(undefined),
on: sinon.stub().returns(undefined),
},
kill: sinon.stub(),
// expected by sinon
cancel: sinon.stub(),
}
// process.stdin is both an event emitter and a readable stream
;(this as any).processStdin = new EE()
mockReadlineEE = new EE()
;(this as any).processStdin.pipe = sinon.stub().returns(undefined)
sinon.stub(process, 'stdin').value((this as any).processStdin)
sinon.stub(readline, 'createInterface').returns(mockReadlineEE)
sinon.stub(cp, 'spawn').returns((this as any).spawnedProcess)
sinon.stub(xvfb, 'start').resolves()
sinon.stub(xvfb, 'stop').resolves()
sinon.stub(xvfb, 'isNeeded').returns(false)
sinon.stub(state, 'getBinaryDir').returns(defaultBinaryDir)
sinon.stub(state, 'getPathToExecutable').withArgs(defaultBinaryDir).returns('/path/to/cypress')
})
context('.start', function () {
// ️️⚠️ NOTE ⚠️
// when asserting the calls made to spawn the child Cypress process
// we have to be _very_ careful. Spawn uses process.env object, if an assertion
// fails, it will print the entire process.env object to the logs, which
// might contain sensitive environment variables. Think about what the
// failed assertion might print to the public CI logs and limit
// the environment variables when running tests on CI.
it('passes args + options to spawn', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(false)
return spawn.start('--foo', { foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('/path/to/cypress', [
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('uses --no-sandbox when needed', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(true)
return spawn.start('--foo', { foo: 'bar' })
.then(() => {
// skip the options argument: we do not need anything about it
// and also less risk that a failed assertion would dump the
// entire ENV object with possible sensitive variables
const args = (cp.spawn as any).firstCall.args.slice(0, 2)
// it is important for "--no-sandbox" to appear before "--" separator
const expectedCliArgs = [
'--no-sandbox',
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
]
expect(args).to.deep.equal(['/path/to/cypress', expectedCliArgs])
})
})
it('uses npm command when running in dev mode', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(false)
const p = path.resolve('..', 'scripts', 'start.js')
return spawn.start('--foo', { dev: true, foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('does not pass --no-sandbox when running in dev mode', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(true)
const p = path.resolve('..', 'scripts', 'start.js')
return spawn.start('--foo', { dev: true, foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
'--userNodePath',
execPath,
'--userNodeVersion',
nodeVersion,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('starts xvfb when needed', function () {
(xvfb.isNeeded as any).returns(true)
;(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start('--foo')
.then(() => {
expect(xvfb.start).to.be.calledOnce
})
})
context('closes', function () {
['close', 'exit'].forEach((event) => {
it(`if '${event}' event fired`, function () {
(this as any).spawnedProcess.on.withArgs(event).yieldsAsync(0)
return spawn.start('--foo')
})
})
it('if exit event fired and close event fired', function () {
(this as any).spawnedProcess.on.withArgs('exit').yieldsAsync(0)
;(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start('--foo')
})
})
context('detects kill signal', function () {
it('exits with error on SIGKILL', function () {
(this as any).spawnedProcess.on.withArgs('exit').yieldsAsync(null, 'SIGKILL')
return spawn.start('--foo')
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e: any) => {
debug('error message', e.message)
snapshot(e.message)
})
})
})
it('does not start xvfb when its not needed', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start('--foo')
.then(() => {
expect(xvfb.start).not.to.be.called
})
})
it('stops xvfb when spawn closes', function () {
(xvfb.isNeeded as any).returns(true)
;(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(this as any).spawnedProcess.on.withArgs('close').yields()
return spawn.start('--foo')
.then(() => {
expect(xvfb.stop).to.be.calledOnce
})
})
it('resolves with spawned close code in the message', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(10)
return spawn.start('--foo')
.then((code: any) => {
expect(code).to.equal(10)
})
})
describe('Linux display', () => {
let restore: any
beforeEach(() => {
restore = mockedEnv({
DISPLAY: 'test-display',
})
})
afterEach(() => {
restore()
})
it('retries with xvfb if fails with display exit code', function () {
(this as any).spawnedProcess.on.withArgs('close').onFirstCall().yieldsAsync(1)
;(this as any).spawnedProcess.on.withArgs('close').onSecondCall().yieldsAsync(0)
const buf1 = '[some noise here] Gtk: cannot open display: 987'
;(this as any).spawnedProcess.stderr.on
.withArgs('data')
.yields(buf1)
;(os.platform as any).returns('linux')
return spawn.start('--foo')
.then((code: any) => {
expect(xvfb.start).to.have.been.calledOnce
expect(xvfb.stop).to.have.been.calledOnce
expect(cp.spawn).to.have.been.calledTwice
// second code should be 0 after successfully running with Xvfb
expect(code).to.equal(0)
})
})
})
it('rejects with error from spawn', function () {
const msg = 'the error message'
;(this as any).spawnedProcess.on.withArgs('error').yieldsAsync(new Error(msg))
return spawn.start('--foo')
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e: any) => {
debug('error message', e.message)
expect(e.message).to.include(msg)
})
})
it('unrefs if options.detached is true', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start(null, { detached: true })
.then(() => {
expect((this as any).spawnedProcess.unref).to.be.calledOnce
})
})
it('does not unref by default', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
expect((this as any).spawnedProcess.unref).not.to.be.called
})
})
it('sets process.env to options.env', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
process.env.FOO = 'bar'
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
expect((cp.spawn as any).firstCall.args[2].env.FOO).to.eq('bar')
})
})
it('forces colors and streams when supported', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(util, 'supportsColor').returns(true)
sinon.stub(tty, 'isatty').returns(true)
return spawn.start([], { env: {} })
.then(() => {
snapshot((cp.spawn as any).firstCall.args[2].env)
})
})
it('sets windowsHide:false property in windows', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(os.platform as any).returns('win32')
return spawn.start([], { env: {} })
.then(() => {
expect((cp.spawn as any).firstCall.args[2].windowsHide).to.be.false
})
})
it('propagates treeKill if SIGINT is detected in windows console', async function () {
(this as any).spawnedProcess.pid = 7
;(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(os.platform as any).returns('win32')
const treeKillMock = sinon.stub().returns(0)
const proxyquire = await import('proxyquire')
const spawn = proxyquire.default(`../../../lib/exec/spawn`, { 'tree-kill': treeKillMock }).default
await spawn.start([], { env: {} })
mockReadlineEE.emit('SIGINT')
// since the import of tree-kill is async inside spawn, we need to wait for it to be imported and called
await new Promise<void>((resolve) => {
setTimeout(resolve)
})
expect(treeKillMock).to.have.been.calledWith(7, 'SIGINT')
})
it('does not set windowsHide property when in darwin', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
return spawn.start([], { env: {} })
.then(() => {
expect((cp.spawn as any).firstCall.args[2].windowsHide).to.be.undefined
})
})
it('does not force colors and streams when not supported', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(util, 'supportsColor').returns(false)
sinon.stub(tty, 'isatty').returns(false)
return spawn.start([], { env: {} })
.then(() => {
snapshot((cp.spawn as any).firstCall.args[2].env)
})
})
it('pipes when on win32', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(os.platform as any).returns('win32')
;(xvfb.isNeeded as any).returns(false)
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
expect((cp.spawn as any).firstCall.args[2].stdio).to.deep.eq('pipe')
// parent process STDIN was piped to child process STDIN
expect((this as any).processStdin.pipe, 'process.stdin').to.have.been.calledOnce
.and.to.have.been.calledWith((this as any).spawnedProcess.stdin)
})
})
it('inherits when on linux and xvfb isn\'t needed', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(os.platform as any).returns('linux')
;(xvfb.isNeeded as any).returns(false)
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
expect((cp.spawn as any).firstCall.args[2].stdio).to.deep.eq('inherit')
})
})
it('uses [inherit, inherit, pipe] when linux and xvfb is needed', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(xvfb.isNeeded as any).returns(true)
;(os.platform as any).returns('linux')
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
expect((cp.spawn as any).firstCall.args[2].stdio).to.deep.eq([
'inherit', 'inherit', 'pipe',
])
})
})
it('uses [inherit, inherit, pipe] on darwin', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
;(xvfb.isNeeded as any).returns(false)
;(os.platform as any).returns('darwin')
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
expect((cp.spawn as any).firstCall.args[2].stdio).to.deep.eq([
'inherit', 'inherit', 'pipe',
])
})
})
it('writes everything on win32', function () {
const buf1 = Buffer.from('asdf')
;(this as any).spawnedProcess.stdin.pipe.withArgs(process.stdin)
;(this as any).spawnedProcess.stdout.pipe.withArgs(process.stdout)
;(this as any).spawnedProcess.stderr.on
.withArgs('data')
.yields(buf1)
;(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(process.stderr, 'write').withArgs(buf1)
;(os.platform as any).returns('win32')
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
})
// https://github.com/cypress-io/cypress/issues/1841
// https://github.com/cypress-io/cypress/issues/5241
;['EPIPE', 'ENOTCONN'].forEach((errCode) => {
it(`catches process.stdin errors and returns when code=${errCode}`, function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
let called = false
const fn = () => {
called = true
const err: any = new Error()
err.code = errCode
return process.stdin.emit('error', err)
}
expect(fn).not.to.throw()
expect(called).to.be.true
})
})
})
it('throws process.stdin errors code!=EPIPE', function () {
(this as any).spawnedProcess.on.withArgs('close').yieldsAsync(0)
// @ts-expect-error - invalid number of arguments for given type
return spawn.start()
.then(() => {
const fn = () => {
const err: any = new Error('wattttt')
err.code = 'FAILWHALE'
return process.stdin.emit('error', err)
}
expect(fn).to.throw(/wattttt/)
})
})
})
})

View File

@@ -0,0 +1,139 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import util from '../../../lib/util'
import state from '../../../lib/tasks/state'
import versions from '../../../lib/exec/versions'
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgBuildInfo: vi.fn(),
pkgVersion: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/state', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getBinaryDir: vi.fn(),
getBinaryPkgAsync: vi.fn(),
parseRealPlatformBinaryFolderAsync: vi.fn(),
},
}
})
describe('lib/exec/versions', function () {
const binaryDir = '/cache/1.2.3/Cypress.app'
beforeEach(function (): void {
vi.unstubAllEnvs()
vi.clearAllMocks()
// @ts-expect-error - mockReturnValue
state.getBinaryDir.mockReturnValue(binaryDir)
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: '1.2.3',
electronVersion: '10.1.2',
electronNodeVersion: '12.16.3',
}
}
throw new Error('not found')
})
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('4.5.6')
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue({ stable: true })
})
describe('.getVersions', () => {
it('gets the correct binary and package version', async () => {
const { package: pkg, binary } = await versions.getVersions()
expect(pkg, 'package version').toEqual('4.5.6')
expect(binary, 'binary version').toEqual('1.2.3')
})
it('gets the correct Electron and bundled Node version', async () => {
const { electronVersion, electronNodeVersion } = await versions.getVersions()
expect(electronVersion, 'electron version').toEqual('10.1.2')
expect(electronNodeVersion, 'node version').toEqual('12.16.3')
})
it('gets correct binary version if CYPRESS_RUN_BINARY', async () => {
// @ts-expect-error - mockImplementation
state.parseRealPlatformBinaryFolderAsync.mockResolvedValue('/my/cypress/path')
vi.stubEnv('CYPRESS_RUN_BINARY', '/my/cypress/path')
// @ts-expect-error
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === '/my/cypress/path') {
return {
version: '7.8.9',
}
}
throw new Error('not found')
})
const { package: pkg, binary } = await versions.getVersions()
expect(pkg).toEqual('4.5.6')
expect(binary).toEqual('7.8.9')
})
it('appends pre-release if not stable', async () => {
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue({ stable: false })
const version = await versions.getVersions()
expect(version.package).to.eql('4.5.6 (pre-release)')
})
it('appends development if missing buildInfo', async () => {
// @ts-expect-error - mockReturnValue
util.pkgBuildInfo.mockReturnValue(undefined)
const version = await versions.getVersions()
expect(version.package).to.eql('4.5.6 (development)')
})
it('reports default versions if not found', async () => {
// imagine package.json only has version there
// @ts-expect-error - mockImplementation
state.getBinaryPkgAsync.mockImplementation((args: string) => {
if (args === binaryDir) {
return {
version: '90.9.9',
}
}
throw new Error('not found')
})
const version = await versions.getVersions()
expect(version).toEqual({
'package': '4.5.6',
'binary': '90.9.9',
'electronVersion': 'not found',
'electronNodeVersion': 'not found',
})
})
})
})

View File

@@ -1,89 +0,0 @@
import { expect } from 'chai'
import '../../spec_helper'
import util from '../../../lib/util'
import state from '../../../lib/tasks/state'
import versions from '../../../lib/exec/versions'
describe('lib/exec/versions', function () {
const binaryDir = '/cache/1.2.3/Cypress.app'
beforeEach(function (): void {
sinon.stub(state, 'getBinaryDir').returns(binaryDir)
sinon.stub(state, 'getBinaryPkgAsync').withArgs(binaryDir).resolves({
version: '1.2.3',
electronVersion: '10.1.2',
electronNodeVersion: '12.16.3',
})
sinon.stub(util, 'pkgVersion').returns('4.5.6')
sinon.stub(util, 'pkgBuildInfo').returns({ stable: true })
})
describe('.getVersions', function () {
it('gets the correct binary and package version', function () {
return versions.getVersions().then(({ package: pkg, binary }: any) => {
expect(pkg, 'package version').to.eql('4.5.6')
expect(binary, 'binary version').to.eql('1.2.3')
})
})
it('gets the correct Electron and bundled Node version', function () {
return versions.getVersions().then(({ electronVersion, electronNodeVersion }: any) => {
expect(electronVersion, 'electron version').to.eql('10.1.2')
expect(electronNodeVersion, 'node version').to.eql('12.16.3')
})
})
it('gets correct binary version if CYPRESS_RUN_BINARY', function () {
sinon.stub(state, 'parseRealPlatformBinaryFolderAsync').resolves('/my/cypress/path')
process.env.CYPRESS_RUN_BINARY = '/my/cypress/path'
state.getBinaryPkgAsync
// @ts-expect-error - is shorthand stub on a function
.withArgs('/my/cypress/path')
.resolves({
version: '7.8.9',
})
return versions.getVersions().then(({ package: pkg, binary }: any) => {
expect(pkg).to.eql('4.5.6')
expect(binary).to.eql('7.8.9')
})
})
it('appends pre-release if not stable', async function () {
// @ts-expect-error - is shorthand stub on a function
util.pkgBuildInfo.returns({ stable: false })
const v = await versions.getVersions()
expect(v.package).to.eql('4.5.6 (pre-release)')
})
it('appends development if missing buildInfo', async function () {
// @ts-expect-error - is shorthand stub on a function
util.pkgBuildInfo.returns(undefined)
const v = await versions.getVersions()
expect(v.package).to.eql('4.5.6 (development)')
})
it('reports default versions if not found', function () {
// imagine package.json only has version there
// @ts-expect-error - is shorthand stub on a function
state.getBinaryPkgAsync.withArgs(binaryDir).resolves({
version: '90.9.9',
})
return versions.getVersions().then((versions: any) => {
expect(versions).to.deep.equal({
'package': '4.5.6',
'binary': '90.9.9',
'electronVersion': 'not found',
'electronNodeVersion': 'not found',
})
})
})
})
})

View File

@@ -0,0 +1,63 @@
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'
import os from 'os'
import xvfb from '../../../lib/exec/xvfb'
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
},
}
})
describe('lib/exec/xvfb-integration', function () {
beforeEach(function (): void {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
})
describe('debugXvfb integration', function () {
const { Debug } = xvfb._debugXvfb
const { namespaces } = Debug
beforeEach(() => {
Debug.enable(namespaces)
})
afterEach(() => {
Debug.enable(namespaces)
})
it('outputs when enabled', function () {
const processStderrWriteSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(undefined)
Debug.enable(xvfb._debugXvfb.namespace)
xvfb._xvfb._onStderrData('asdf')
expect(processStderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('cypress:xvfb'))
expect(processStderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('asdf'))
})
it('does not output when disabled', function () {
const processStderrWriteSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(undefined)
Debug.disable()
xvfb._xvfb._onStderrData('asdf')
expect(processStderrWriteSpy).not.toHaveBeenCalledWith(expect.stringContaining('cypress:xvfb'))
expect(processStderrWriteSpy).not.toHaveBeenCalledWith(expect.stringContaining('asdf'))
})
})
describe('xvfbOptions', function () {
it('sets explicit screen', () => {
expect(xvfb._xvfbOptions).toHaveProperty('xvfb_args', expect.arrayContaining(['-screen']))
})
})
})

View File

@@ -0,0 +1,101 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import os from 'os'
import _xvfb from '@cypress/xvfb'
import xvfb from '../../../lib/exec/xvfb'
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
},
}
})
vi.mock(import('@cypress/xvfb'), async () => {
const XVFB_MOCK = vi.fn()
XVFB_MOCK.prototype.start = vi.fn()
return {
default: XVFB_MOCK,
}
})
describe('lib/exec/xvfb', function () {
beforeEach(function (): void {
vi.clearAllMocks()
vi.unstubAllEnvs()
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
})
describe('#start', function () {
it('passes', async () => {
vi.spyOn(_xvfb.prototype, 'start').mockImplementation((cb) => {
// mock a pass
cb()
})
await expect(xvfb.start()).resolves.toBeNull()
})
it('fails with error message', async () => {
const message = 'nope'
vi.spyOn(_xvfb.prototype, 'start').mockImplementation((cb) => {
// mock a failure
cb(new Error(message))
})
await expect(xvfb.start()).rejects.toThrow(message)
})
it('fails when xvfb exited with non zero exit code', async () => {
const e: any = new Error('something bad happened')
e.nonZeroExitCode = true
vi.spyOn(_xvfb.prototype, 'start').mockImplementation((cb) => {
// mock a failure
cb(e)
})
await expect(xvfb.start()).rejects.toThrow(expect.objectContaining({
message: expect.stringContaining('something bad happened'),
known: true,
}))
await expect(xvfb.start()).rejects.toThrow(expect.objectContaining({
message: expect.stringContaining('Xvfb exited with a non zero exit code.'),
known: true,
}))
})
})
describe('#isNeeded', function () {
it('does not need xvfb on osx', function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
expect(xvfb.isNeeded()).toBe(false)
})
it('does not need xvfb on linux when DISPLAY is set', function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
vi.stubEnv('DISPLAY', ':99')
expect(xvfb.isNeeded()).toBe(false)
})
it('does need xvfb on linux when no DISPLAY is set', function () {
vi.stubEnv('DISPLAY', undefined)
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
expect(xvfb.isNeeded()).toBe(true)
})
})
})

View File

@@ -1,111 +0,0 @@
import '../../spec_helper'
import os from 'os'
import xvfb from '../../../lib/exec/xvfb'
describe('lib/exec/xvfb', function () {
beforeEach(function (): void {
(os.platform as any).returns('win32')
})
context('debugXvfb', function () {
const { Debug } = xvfb._debugXvfb
const { namespaces } = Debug
beforeEach(() => {
Debug.enable(namespaces)
})
afterEach(() => {
Debug.enable(namespaces)
})
it('outputs when enabled', function () {
sinon.stub(process.stderr, 'write').returns(undefined)
Debug.enable(xvfb._debugXvfb.namespace)
xvfb._xvfb._onStderrData('asdf')
expect(process.stderr.write).to.be.calledWithMatch('cypress:xvfb')
expect(process.stderr.write).to.be.calledWithMatch('asdf')
})
it('does not output when disabled', function () {
sinon.stub(process.stderr, 'write')
Debug.disable()
xvfb._xvfb._onStderrData('asdf')
expect(process.stderr.write).not.to.be.calledWithMatch('cypress:xvfb')
expect(process.stderr.write).not.to.be.calledWithMatch('asdf')
})
})
context('xvfbOptions', function () {
it('sets explicit screen', () => {
expect(xvfb._xvfbOptions).to.have.property('xvfb_args').that.includes('-screen')
})
})
context('#start', function () {
it('passes', function () {
sinon.stub(xvfb._xvfb, 'startAsync').resolves()
return xvfb.start()
})
it('fails with error message', function () {
const message = 'nope'
sinon.stub(xvfb._xvfb, 'startAsync').rejects(new Error(message))
return xvfb.start()
.then(() => {
throw new Error('Should have thrown an error')
})
.catch((err: Error) => {
expect(err.message).to.include(message)
})
})
it('fails when xvfb exited with non zero exit code', function () {
const e: any = new Error('something bad happened')
e.nonZeroExitCode = true
sinon.stub(xvfb._xvfb, 'startAsync').rejects(e)
return xvfb.start()
.then(() => {
throw new Error('Should have thrown an error')
})
.catch((err: any) => {
expect(err.known).to.be.true
expect(err.message).to.include('something bad happened')
expect(err.message).to.include('Xvfb exited with a non zero exit code.')
})
})
})
context('#isNeeded', function () {
it('does not need xvfb on osx', function () {
(os.platform as any).returns('darwin')
expect(xvfb.isNeeded()).to.be.false
})
it('does not need xvfb on linux when DISPLAY is set', function () {
(os.platform as any).returns('linux')
process.env.DISPLAY = ':99'
expect(xvfb.isNeeded()).to.be.false
})
it('does need xvfb on linux when no DISPLAY is set', function () {
(os.platform as any).returns('linux')
expect(xvfb.isNeeded()).to.be.true
})
})
})

View File

@@ -1,7 +1,6 @@
import '../spec_helper'
import { describe, it, expect } from 'vitest'
import la from 'lazy-ass'
import { stripIndent, stripIndents } from 'common-tags'
import snapshot from '../support/snapshot'
describe('stripIndent', () => {
it('removes indent from literal string', () => {
@@ -13,7 +12,7 @@ describe('stripIndent', () => {
`
// should preserve the structure of the text
snapshot(removed)
expect(removed).toMatchSnapshot()
})
it('can be called as a function', () => {
@@ -42,6 +41,6 @@ describe('stripIndent', () => {
// bar
//
// last line
snapshot(str)
expect(str).toMatchSnapshot()
})
})

View File

@@ -1,79 +1,89 @@
exports['lib/tasks/cache .path lists path to cache 1'] = `
/.cache/Cypress
`
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports['lib/tasks/cache .clear deletes cache folder and everything inside it 1'] = `
[no output]
`
exports[`lib/tasks/cache > .clear > deletes cache folder and everything inside it 1`] = `""`;
exports['lib/tasks/cache .prune deletes cache binaries for all version but the current one 1'] = `
Deleted all binary caches except for the 1.2.3 binary cache.
`
exports['lib/tasks/cache .prune doesn\'t delete any cache binaries 1'] = `
No binary caches found to prune.
`
exports['lib/tasks/cache .prune exits cleanly if cache dir DNE 1'] = `
No Cypress cache was found at /.cache/Cypress. Nothing to prune.
`
exports['lib/tasks/cache .list lists all versions of cached binary 1'] = `
┌─────────┬───────────┐
exports[`lib/tasks/cache > .list > lists all versions of cached binary 1`] = `
"┌─────────┬───────────┐
│ version │ last used │
├─────────┼───────────┤
│ 1.2.3 │ unknown │
├─────────┼───────────┤
│ 2.3.4 │ unknown │
└─────────┴───────────┘
`
"
`;
exports['cache list with silent log level'] = `
┌─────────┬───────────┐
│ version │ last used │
├─────────┼───────────┤
│ 1.2.3 │ unknown │
├─────────┼───────────┤
│ 2.3.4 │ unknown │
└─────────┴───────────┘
`
exports['cache list with warn log level'] = `
┌─────────┬───────────┐
│ version │ last used │
├─────────┼───────────┤
│ 1.2.3 │ unknown │
├─────────┼───────────┤
│ 2.3.4 │ unknown │
└─────────┴───────────┘
`
exports['lib/tasks/cache .list lists all versions of cached binary with last access 1'] = `
┌─────────┬──────────────┐
exports[`lib/tasks/cache > .list > lists all versions of cached binary with last access > list-of-versions 1`] = `
"┌─────────┬──────────────
│ version │ last used │
├─────────┼──────────────┤
│ 1.2.3 │ 3 months ago │
├─────────┼──────────────┤
│ 2.3.4 │ 5 days ago │
└─────────┴──────────────┘
`
"
`;
exports['lib/tasks/cache .list some versions have never been opened 1'] = `
┌─────────┬──────────────
│ version │ last used
├─────────┼──────────────
│ 1.2.3 │ 3 months ago
├─────────┼──────────────
│ 2.3.4 │ unknown
└─────────┴──────────────
`
exports[`lib/tasks/cache > .list > lists all versions of cached binary with npm log level silent > cache list with silent log level 1`] = `
"┌─────────┬───────────┐
│ version │ last used │
├─────────┼───────────┤
│ 1.2.3 │ unknown
├─────────┼───────────┤
│ 2.3.4 │ unknown │
└─────────┴───────────┘
"
`;
exports['lib/tasks/cache .list shows sizes 1'] = `
┌─────────┬──────────────┬───────
exports[`lib/tasks/cache > .list > lists all versions of cached binary with npm log level warn > cache list with warn log level 1`] = `
"┌─────────┬───────────┐
│ version │ last used │
├─────────┼───────────┤
│ 1.2.3 │ unknown │
├─────────┼───────────┤
│ 2.3.4 │ unknown │
└─────────┴───────────┘
"
`;
exports[`lib/tasks/cache > .list > shows sizes > show-size 1`] = `
"┌─────────┬──────────────┬───────┐
│ version │ last used │ size │
├─────────┼──────────────┼───────┤
│ 1.2.3 │ 3 months ago │ 0.2MB │
├─────────┼──────────────┼───────┤
│ 2.3.4 │ unknown │ 0.2MB │
└─────────┴──────────────┴───────┘
`
"
`;
exports[`lib/tasks/cache > .list > some versions have never been opened > second-binary-never-used 1`] = `
"┌─────────┬──────────────┐
│ version │ last used │
├─────────┼──────────────┤
│ 1.2.3 │ 3 months ago │
├─────────┼──────────────┤
│ 2.3.4 │ unknown │
└─────────┴──────────────┘
"
`;
exports[`lib/tasks/cache > .path > lists path to cache 1`] = `
"/.cache/Cypress
"
`;
exports[`lib/tasks/cache > .prune > deletes cache binaries for all version but the current one 1`] = `
"Deleted all binary caches except for the 1.2.3 binary cache.
"
`;
exports[`lib/tasks/cache > .prune > doesn't delete any cache binaries 1`] = `
"No binary caches found to prune.
"
`;
exports[`lib/tasks/cache > .prune > exits cleanly if cache dir DNE 1`] = `
"No Cypress cache was found at /.cache/Cypress. Nothing to prune.
"
`;

View File

@@ -0,0 +1,44 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`lib/tasks/download > catches download status errors and exits > download status errors 1 1`] = `
"Error: The Cypress App could not be downloaded.
Does your workplace require a proxy to be used to access the Internet? If so, you must configure the HTTP_PROXY environment variable before downloading Cypress. Read more: https://on.cypress.io/proxy-configuration
Otherwise, please check network connectivity and try again:
----------
URL: https://download.cypress.io/desktop?platform=OS&arch=x64
404 - Not Found
----------
Platform: OS-x64 (Foo - OsVersion)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/download > download base url from CYPRESS_DOWNLOAD_MIRROR env var > env var > base url from CYPRESS_DOWNLOAD_MIRROR 1 1`] = `"https://cypress.example.com/desktop/0.20.2?platform=OS&arch=ARCH"`;
exports[`lib/tasks/download > download base url from CYPRESS_DOWNLOAD_MIRROR env var > env var with subdirectory > base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory 1 1`] = `"https://cypress.example.com/example/desktop/0.20.2?platform=OS&arch=ARCH"`;
exports[`lib/tasks/download > download base url from CYPRESS_DOWNLOAD_MIRROR env var > env var with subdirectory and trailing slash > base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory and trailing slash 1 1`] = `"https://cypress.example.com/example/desktop/0.20.2?platform=OS&arch=ARCH"`;
exports[`lib/tasks/download > download base url from CYPRESS_DOWNLOAD_MIRROR env var > env var with trailing slash > base url from CYPRESS_DOWNLOAD_MIRROR with trailing slash 1 1`] = `"https://cypress.example.com/desktop/0.20.2?platform=OS&arch=ARCH"`;
exports[`lib/tasks/download > download url > returns custom url from template > desktop url from template 1`] = `"https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip"`;
exports[`lib/tasks/download > download url > returns custom url from template with escaped dollar sign > desktop url from template with escaped dollar sign 1`] = `"https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip"`;
exports[`lib/tasks/download > download url > returns custom url from template with escaped dollar sign wrapped in quote > desktop url from template with escaped dollar sign wrapped in quote 1`] = `"https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip"`;
exports[`lib/tasks/download > download url > returns custom url from template with multiple replacements > desktop url from template with multiple replacements 1`] = `"https://download.cypress.io/desktop/0.20.2/OS/ARCH/cypress-0.20.2-OS-ARCH.zip?referrer=https://download.cypress.io/desktop/0.20.2&version=0.20.2"`;
exports[`lib/tasks/download > download url > returns custom url from template with version > desktop url from template with version 1`] = `"https://mycompany/0.20.2/OS-ARCH/cypress.zip"`;
exports[`lib/tasks/download > download url > returns custom url from template wrapped in quote > desktop url from template wrapped in quote 1`] = `"https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip"`;
exports[`lib/tasks/download > download url > returns latest desktop url > latest desktop url 1 1`] = `"https://download.cypress.io/desktop?platform=OS&arch=ARCH"`;
exports[`lib/tasks/download > download url > returns specific desktop version url > specific version desktop url 1 1`] = `"https://download.cypress.io/desktop/0.20.2?platform=OS&arch=ARCH"`;

View File

@@ -0,0 +1,282 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`/lib/tasks/install > .start > exits with error when installing on unsupported os > error when installing on unsupported os 1`] = `
"Error: The Cypress App could not be installed. Your machine does not meet the operating system requirements.
https://on.cypress.io/app/get-started/install-cypress#System-requirements
----------
Platform: win32-ia32
"
`;
exports[`/lib/tasks/install > .start > is silent when log level is silent > silent install 1 1`] = `
"[STARTED] Task without title.
[SUCCESS] Task without title.
[STARTED] Task without title.
[SUCCESS] Task without title.
[STARTED] Task without title.
[SUCCESS] Task without title.
"
`;
exports[`/lib/tasks/install > .start > non-stable builds > logs a warning about installing a pre-release > pre-release warning 1`] = `
"⚠ Warning: You are installing a pre-release build of Cypress.
Bugs may be present which do not exist in production builds.
This build was created from:
* Commit SHA: 3b7f0b5c59def1e9b5f385bd585c9b2836706c29
* Commit Branch: aBranchName
* Commit Timestamp: 1996-11-27T00:00:00.000Z
Installing Cypress (version: https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-3b7f0b5c59def1e9b5f385bd585c9b2836706c29/cypress.zip)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > override version > as a global install > logs global warning and download > warning installing as global 1 1`] = `
"
Cypress x.x.x is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
⚠ Warning: It looks like you've installed Cypress globally.
The recommended way to install Cypress is as a devDependency per project.
You should probably run these commands:
- npm uninstall -g cypress
- npm install --save-dev cypress
"
`;
exports[`/lib/tasks/install > .start > override version > failed write access to cache directory > logs error on failure > invalid cache directory 1 1`] = `
"Error: Cypress cannot write to the cache directory due to file permissions
See discussion and possible solutions at
https://github.com/cypress-io/cypress/issues/1281
----------
Failed to access /invalid/cache/dir:
EACCES: permission denied, mkdir '/invalid'
----------
Platform: darwin-x64 (Foo - OsVersion)
Cypress Version: 1.2.3
"
`;
exports[`/lib/tasks/install > .start > override version > warns when specifying cypress version in env > specify version in env vars 1 1`] = `
"⚠ Warning: Forcing a binary version different than the default.
The CLI expected to install version: 1.2.3
Instead we will install version: 0.12.1
These versions may not work properly together.
Installing Cypress (version: 0.12.1)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > override version > when getting installed version does not match needed version > logs message and starts download > installed version does not match needed version 1 1`] = `
"
Cypress x.x.x is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > override version > when getting installed version fails > logs message and starts download > continues installing on failure 1 1`] = `
"Installing Cypress (version: 1.2.3)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > override version > when running in CI > uses verbose renderer > installing in ci 1 1`] = `
"
Cypress x.x.x is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
[STARTED] Task without title.
[SUCCESS] Task without title.
[STARTED] Task without title.
[SUCCESS] Task without title.
[STARTED] Task without title.
[SUCCESS] Task without title.
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > override version > when there is no install version > logs message and starts download > installs without existing installation 1 1`] = `
"Installing Cypress (version: 1.2.3)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > override version > when version is already installed > logs 'skipping install' when explicit cypress install > version already installed - cypress install 1 1`] = `
"
Cypress 1.2.3 is installed in /cache/Cypress/1.2.3
Skipping installation:
Pass the --force option if you'd like to reinstall anyway.
"
`;
exports[`/lib/tasks/install > .start > override version > when version is already installed > logs when already installed when run from postInstall > version already installed - postInstall 1 1`] = `
"
Cypress 1.2.3 is installed in /cache/Cypress/1.2.3
"
`;
exports[`/lib/tasks/install > .start > override version > with force: true > logs message and starts download > forcing true always installs 1 1`] = `
"
Cypress 1.2.3 is installed in /cache/Cypress/1.2.3
Installing Cypress (version: 1.2.3)
[STARTED] Task without title.
[TITLE] Downloaded Cypress
[SUCCESS] Downloaded Cypress
[STARTED] Task without title.
[TITLE] Unzipped Cypress
[SUCCESS] Unzipped Cypress
[STARTED] Task without title.
[TITLE] Finished Installation /cache/Cypress/1.2.3
[SUCCESS] Finished Installation /cache/Cypress/1.2.3
You can now open Cypress by running one of the following, depending on your package manager:
- npx cypress open
- yarn cypress open
- pnpm cypress open
https://on.cypress.io/opening-the-app
"
`;
exports[`/lib/tasks/install > .start > skips install > when environment variable is set > skip installation 1 1`] = `
"Note: Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.
"
`;

View File

@@ -1,23 +1,7 @@
exports['lib/tasks/unzip throws when cannot unzip 1'] = `
Error: The Cypress App could not be unzipped.
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
Search for an existing issue or open a GitHub issue at
https://github.com/cypress-io/cypress/issues
----------
Error: end of central directory record signature not found
----------
Platform: darwin-x64 (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['lib/tasks/unzip throws max path length error when cannot unzip due to realpath ENOENT on windows 1'] = `
Error: The Cypress App could not be unzipped.
exports[`lib/tasks/unzip > throws max path length error when cannot unzip due to realpath ENOENT on windows 1`] = `
"Error: The Cypress App could not be unzipped.
This is most likely because the maximum path length is being exceeded on your system.
@@ -29,7 +13,25 @@ Error: failed
----------
Platform: win32-x64 (Foo-OsVersion)
Platform: win32-x64 (Foo - OsVersion)
Cypress Version: 1.2.3
"
`;
`
exports[`lib/tasks/unzip > throws when cannot unzip 1`] = `
"Error: The Cypress App could not be unzipped.
Search for an existing issue or open a GitHub issue at
https://github.com/cypress-io/cypress/issues
----------
Error: end of central directory record signature not found
----------
Platform: darwin-x64 (Foo - OsVersion)
Cypress Version: 1.2.3
"
`;

View File

@@ -0,0 +1,381 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`lib/tasks/verify > logs error and exits when executable cannot be found 1`] = `
"Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app
Please reinstall Cypress by running: cypress install
----------
Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > logs error and exits when no version of Cypress is installed 1`] = `
"Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app
Please reinstall Cypress by running: cypress install
----------
Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > logs error when child process hangs 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Error: Cypress verification timed out.
This command failed with the following output:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222
----------
some stderr
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > logs error when child process returns incorrect stdout (stderr when exists) 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Error: Cypress failed to start.
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error below for more details.
----------
some stderr
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > logs error when child process returns incorrect stdout (stdout when no stderr) 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Error: Cypress failed to start.
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error below for more details.
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > logs warning when installed version does not match verified version 1`] = `
"Found binary version bloop installed in: /cache/Cypress/1.2.3/Cypress.app
⚠ Warning: Binary version bloop does not match the expected package version 1.2.3
These versions may not work properly together.
"
`;
exports[`lib/tasks/verify > on linux > logs error and exits when starting xvfb fails > xvfb fails 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Error: Xvfb exited with a non zero exit code.
There was a problem spawning Xvfb.
This is likely a problem with your system, permissions, or installation of Xvfb.
----------
Error: test without xvfb
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > fails on both retries with our Xvfb on Linux > tried to verify twice, on the first try got the DISPLAY error 1`] = `
"Cypress verification failed.
Cypress failed to start after spawning a new Xvfb server.
The error logs we received were:
----------
[some noise here] Gtk: cannot open display: 987
some other error
again with
some weird indent
----------
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error above for more detail.
----------
Platform: linux-x64 (undefined)
Cypress Version: 1.2.3"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > is silent when logLevel is silent > silent verify 1`] = `""`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > logs an error if Cypress executable does not exist > no Cypress executable 1`] = `
"Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app
Please reinstall Cypress by running: cypress install
----------
Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > logs an error if Cypress executable does not have permissions > Cypress non-executable permission 1`] = `
"Error: Cypress cannot run because this binary file does not have executable permissions here:
/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
Reasons this may happen:
- node was installed as 'root' or with 'sudo'
- the cypress npm package as 'root' or with 'sudo'
Please check that you have the appropriate user permissions.
You can also try clearing the cache with 'cypress cache clear' and reinstalling.
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > logs and runs when current version has not been verified > current version has not been verified 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > logs and runs when installed version is different than package version > different version installed 1`] = `
"Found binary version 7.8.9 installed in: /cache/Cypress/1.2.3/Cypress.app
⚠ Warning: Binary version 7.8.9 does not match the expected package version 1.2.3
These versions may not work properly together.
It looks like this is your first time using Cypress: 7.8.9
Opening Cypress...
"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > logs error when fails smoke test unexpectedly without stderr > fails with no stderr 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Error: Cypress failed to start.
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error below for more details.
----------
Error: EPERM NOT PERMITTED
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > smoke test retries on bad display with our Xvfb > turns off Opening Cypress... > no welcome message 1`] = `
"Found binary version 7.8.9 installed in: /cache/Cypress/1.2.3/Cypress.app
⚠ Warning: Binary version 7.8.9 does not match the expected package version 1.2.3
These versions may not work properly together.
"
`;
exports[`lib/tasks/verify > smoke test with DEBUG output > finds ping value in the verbose output > verbose stdout output 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
"
`;
exports[`lib/tasks/verify > when env var CYPRESS_RUN_BINARY > can log error to user on darwin > darwin: error when invalid CYPRESS_RUN_BINARY 1`] = `
"Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/
This overrides the default Cypress binary path used.
Error: Could not run binary set by environment variable: CYPRESS_RUN_BINARY=/custom/
Ensure the environment variable is a path to the Cypress binary, matching **/Contents/MacOS/Cypress
----------
ENOENT: no such file or directory, stat '/custom/'
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > when env var CYPRESS_RUN_BINARY > can log error to user on linux > linux: error when invalid CYPRESS_RUN_BINARY 1`] = `
"Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/
This overrides the default Cypress binary path used.
Error: Could not run binary set by environment variable: CYPRESS_RUN_BINARY=/custom/
Ensure the environment variable is a path to the Cypress binary, matching **/Cypress
----------
ENOENT: no such file or directory, stat '/custom/'
----------
Platform: linux-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > when env var CYPRESS_RUN_BINARY > can log error to user on win32 > win32: error when invalid CYPRESS_RUN_BINARY 1`] = `
"Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/
This overrides the default Cypress binary path used.
Error: Could not run binary set by environment variable: CYPRESS_RUN_BINARY=/custom/
Ensure the environment variable is a path to the Cypress binary, matching **/Cypress.exe
----------
ENOENT: no such file or directory, stat '/custom/'
----------
Platform: win32-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > when env var CYPRESS_RUN_BINARY > can validate and use executable > valid CYPRESS_RUN_BINARY 1`] = `
"Note: You have set the environment variable:
CYPRESS_RUN_BINARY=/custom/Contents/MacOS/Cypress
This overrides the default Cypress binary path used.
It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
"
`;
exports[`lib/tasks/verify > when running in CI > logs error when binary not found > error binary not found in ci 1`] = `
"Error: The cypress npm package is installed, but the Cypress binary is missing.
We expected the binary to be installed here: /cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress
Reasons it may be missing:
- You're caching 'node_modules' but are not caching this path: /cache/Cypress
- You ran 'npm install' at an earlier build step but did not persist: /cache/Cypress
Properly caching the binary will fix this error and avoid downloading and unzipping Cypress.
Alternatively, you can run 'cypress install' to download the binary again.
https://on.cypress.io/not-installed-ci-error
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > when running in CI > uses verbose renderer > verifying in ci 1`] = `
"It looks like this is your first time using Cypress: 1.2.3
Opening Cypress...
"
`;
exports[`lib/tasks/verify > with force: true > clears verified version from state if verification fails > fails verifying Cypress 1`] = `
"
Error: Cypress failed to start.
This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies
Please refer to the error below for more details.
----------
an error about dependencies
----------
Platform: darwin-x64 (undefined)
Cypress Version: 1.2.3
"
`;
exports[`lib/tasks/verify > with force: true > shows full path to executable when verifying > verification with executable 1`] = `
"
Opening Cypress...
"
`;

View File

@@ -0,0 +1,288 @@
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'
import mockfs from 'mock-fs'
import dayjs from 'dayjs'
import path from 'path'
import fs from 'fs-extra'
import { Console } from 'console'
import state from '../../../lib/tasks/state'
import util from '../../../lib/util'
import cache from '../../../lib/tasks/cache'
vi.mock('fs-extra', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
stat: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/state', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getCacheDir: vi.fn(),
getBinaryDir: vi.fn(),
getPathToExecutable: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgVersion: vi.fn(),
},
}
})
describe('lib/tasks/cache', () => {
const createStdoutCapture = () => {
const logs: string[] = []
// eslint-disable-next-line no-console
const originalOut = process.stdout.write
vi.spyOn(process.stdout, 'write').mockImplementation((strOrBugger: string | Uint8Array<ArrayBufferLike>) => {
logs.push(strOrBugger as string)
return originalOut(strOrBugger)
})
return () => logs.join('')
}
// Direct console to process.stdout/stderr
let originalConsole: Console
beforeEach(() => {
vi.resetAllMocks()
vi.unstubAllEnvs()
originalConsole = globalThis.console
// Redirect console output to a custom stream or mock
globalThis.console = new Console(process.stdout, process.stderr)
})
afterEach(() => {
globalThis.console = originalConsole // Restore original console
})
beforeEach(async function () {
mockfs({
'/.cache/Cypress': {
'1.2.3': {
'Cypress': {
'file1': Buffer.from(new Array(32 * 1024).fill(1)),
'dir': {
'file2': Buffer.from(new Array(128 * 1042).fill(2)),
},
},
},
'2.3.4': {
'Cypress.app': {},
},
},
})
// @ts-expect-error mockReturnValue
state.getCacheDir.mockReturnValue('/.cache/Cypress')
// @ts-expect-error mockReturnValue
state.getBinaryDir.mockReturnValue('/.cache/Cypress')
// @ts-expect-error mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
})
afterEach(() => {
mockfs.restore()
})
describe('.path', () => {
it('lists path to cache', function () {
const output = createStdoutCapture()
cache.path()
const stdout = output()
expect(stdout).to.eql('/.cache/Cypress\n')
expect(stdout).toMatchSnapshot()
})
it('lists path to cache with silent npm loglevel', function () {
const output = createStdoutCapture()
vi.stubEnv('npm_config_loglevel', 'silent')
cache.path()
expect(output()).to.eql('/.cache/Cypress\n')
})
it('lists path to cache with silent npm warn', function () {
const output = createStdoutCapture()
vi.stubEnv('npm_config_loglevel', 'warn')
cache.path()
expect(output()).to.eql('/.cache/Cypress\n')
})
})
describe('.clear', () => {
it('deletes cache folder and everything inside it', async function () {
const output = createStdoutCapture()
await cache.clear()
const exists = await fs.pathExists('/.cache/Cypress')
expect(exists).toEqual(false)
expect(output()).toMatchSnapshot()
})
})
describe('.prune', () => {
it('deletes cache binaries for all version but the current one', async function () {
const output = createStdoutCapture()
await cache.prune()
const checkedInBinaryVersion = util.pkgVersion()
const files = await fs.readdir('/.cache/Cypress')
expect(files.length).to.eq(1)
files.forEach((file: any) => {
expect(file).to.eq(checkedInBinaryVersion)
})
expect(output()).toMatchSnapshot()
})
it('doesn\'t delete any cache binaries', async function () {
const output = createStdoutCapture()
const dir = path.join(state.getCacheDir(), '2.3.4')
await fs.remove(dir)
await cache.prune()
const checkedInBinaryVersion = util.pkgVersion()
const files = await fs.readdir('/.cache/Cypress')
expect(files.length).to.eq(1)
files.forEach((file: any) => {
expect(file).to.eq(checkedInBinaryVersion)
})
expect(output()).toMatchSnapshot()
})
it('exits cleanly if cache dir DNE', async function () {
const output = createStdoutCapture()
await fs.remove(state.getCacheDir())
await cache.prune()
expect(output()).toMatchSnapshot()
})
})
describe('.list', () => {
beforeEach(() => {
// @ts-expect-error mockReturnValue
state.getPathToExecutable.mockReturnValue('/.cache/Cypress/1.2.3/app/cypress')
})
it('lists all versions of cached binary', async function () {
const output = createStdoutCapture()
await cache.list()
expect(output()).toMatchSnapshot()
})
it('lists all versions of cached binary with npm log level silent', async function () {
const output = createStdoutCapture()
vi.stubEnv('npm_config_loglevel', 'silent')
await cache.list()
// log output snapshot should have a grid of versions
expect(output()).toMatchSnapshot('cache list with silent log level')
})
it('lists all versions of cached binary with npm log level warn', async function () {
const output = createStdoutCapture()
vi.stubEnv('npm_config_loglevel', 'warn')
await cache.list()
// log output snapshot should have a grid of versions
expect(output()).toMatchSnapshot('cache list with warn log level')
})
it('lists all versions of cached binary with last access', async function () {
const output = createStdoutCapture()
// @ts-expect-error mockResolvedValueOnce
fs.stat.mockResolvedValueOnce({
atime: dayjs().subtract(3, 'month').valueOf(),
})
// @ts-expect-error mockResolvedValueOnce
fs.stat.mockResolvedValueOnce({
atime: dayjs().subtract(5, 'day').valueOf(),
})
await cache.list()
await expect(output()).toMatchSnapshot('list-of-versions')
})
it('some versions have never been opened', async function () {
const output = createStdoutCapture()
// @ts-expect-error mockResolvedValueOnce
fs.stat.mockResolvedValueOnce({
atime: dayjs().subtract(3, 'month').valueOf(),
})
// the second binary has never been accessed
// @ts-expect-error mockResolvedValueOnce
fs.stat.mockResolvedValueOnce()
await cache.list()
await expect(output()).toMatchSnapshot('second-binary-never-used')
})
it('shows sizes', async function () {
const output = createStdoutCapture()
// @ts-expect-error mockResolvedValueOnce
fs.stat.mockResolvedValueOnce({
atime: dayjs().subtract(3, 'month').valueOf(),
})
// the second binary has never been accessed
// @ts-expect-error mockResolvedValueOnce
fs.stat.mockResolvedValueOnce()
await cache.list(true)
await expect(output()).toMatchSnapshot('show-size')
})
})
})

View File

@@ -1,277 +0,0 @@
import '../../spec_helper'
import mockfs from 'mock-fs'
import stdout from '../../support/stdout'
import snapshot from '../../support/snapshot'
import dayjs from 'dayjs'
import stripAnsi from 'strip-ansi'
import path from 'path'
import termToHtml from 'term-to-html'
import mockedEnv from 'mocked-env'
import fs from '../../../lib/fs'
import state from '../../../lib/tasks/state'
import util from '../../../lib/util'
import cache from '../../../lib/tasks/cache'
const outputHtmlFolder = path.join(__dirname, '..', '..', 'html')
describe('lib/tasks/cache', () => {
let stdoutCapture: any
beforeEach(async function () {
mockfs({
'/.cache/Cypress': {
'1.2.3': {
'Cypress': {
'file1': Buffer.from(new Array(32 * 1024).fill(1)),
'dir': {
'file2': Buffer.from(new Array(128 * 1042).fill(2)),
},
},
},
'2.3.4': {
'Cypress.app': {},
},
},
})
sinon.stub(state, 'getCacheDir').returns('/.cache/Cypress')
sinon.stub(state, 'getBinaryDir').returns('/.cache/Cypress')
sinon.stub(util, 'pkgVersion').returns('1.2.3')
stdoutCapture = stdout.capture()
})
const getSnapshotText = () => {
const output = stdoutCapture.toString().split('\n').slice(0, -1).join('\n')
const stdoutAsString = output || '[no output]'
// first restore the STDOUT, then confirm the value
// otherwise the error might not even appear or appear twice!
stdout.restore()
return stdoutAsString
}
const saveHtml = async (filename: string, html: string) => {
await fs.ensureDirAsync(outputHtmlFolder)
const htmlFilename = path.join(outputHtmlFolder, filename)
await fs.writeFileAsync(htmlFilename, html, 'utf8')
}
afterEach(() => {
mockfs.restore()
})
const defaultSnapshot = (snapshotName?: string) => {
const stdoutAsString = getSnapshotText()
const withoutAnsi = stripAnsi(stdoutAsString)
if (snapshotName) {
snapshot(snapshotName, withoutAnsi)
} else {
snapshot(withoutAnsi)
}
}
const snapshotWithHtml = async (htmlFilename: string) => {
const stdoutAsString = getSnapshotText()
snapshot(stripAnsi(stdoutAsString))
// if the sanitized snapshot matches, let's save the ANSI colors converted into HTML
const html = termToHtml.strings(stdoutAsString, termToHtml.themes.dark.name)
await saveHtml(htmlFilename, html)
}
describe('.path', () => {
let restoreEnv: any
afterEach(() => {
if (restoreEnv) {
restoreEnv()
restoreEnv = null
}
})
it('lists path to cache', function () {
cache.path()
expect(stdoutCapture.toString()).to.eql('/.cache/Cypress\n')
defaultSnapshot()
})
it('lists path to cache with silent npm loglevel', function () {
restoreEnv = mockedEnv({
npm_config_loglevel: 'silent',
})
cache.path()
expect(stdoutCapture.toString()).to.eql('/.cache/Cypress\n')
})
it('lists path to cache with silent npm warn', function () {
restoreEnv = mockedEnv({
npm_config_loglevel: 'warn',
})
cache.path()
expect(stdoutCapture.toString()).to.eql('/.cache/Cypress\n')
})
})
describe('.clear', () => {
it('deletes cache folder and everything inside it', function () {
return cache.clear()
.then(() => {
return fs.pathExistsAsync('/.cache/Cypress')
.then((exists: any) => {
expect(exists).to.eql(false)
defaultSnapshot()
})
})
})
})
describe('.prune', () => {
it('deletes cache binaries for all version but the current one', async function () {
await cache.prune()
const checkedInBinaryVersion = util.pkgVersion()
const files = await fs.readdir('/.cache/Cypress')
expect(files.length).to.eq(1)
files.forEach((file: any) => {
expect(file).to.eq(checkedInBinaryVersion)
})
defaultSnapshot()
})
it('doesn\'t delete any cache binaries', async function () {
const dir = path.join(state.getCacheDir(), '2.3.4')
await fs.removeAsync(dir)
await cache.prune()
const checkedInBinaryVersion = util.pkgVersion()
const files = await fs.readdirAsync('/.cache/Cypress')
expect(files.length).to.eq(1)
files.forEach((file: any) => {
expect(file).to.eq(checkedInBinaryVersion)
})
defaultSnapshot()
})
it('exits cleanly if cache dir DNE', async function () {
await fs.removeAsync(state.getCacheDir())
await cache.prune()
defaultSnapshot()
})
})
describe('.list', () => {
let restoreEnv: any
afterEach(() => {
if (restoreEnv) {
restoreEnv()
restoreEnv = null
}
})
it('lists all versions of cached binary', async function () {
// unknown access times
sinon.stub(state, 'getPathToExecutable').returns('/.cache/Cypress/1.2.3/app/cypress')
await cache.list()
defaultSnapshot()
})
it('lists all versions of cached binary with npm log level silent', async function () {
restoreEnv = mockedEnv({
npm_config_loglevel: 'silent',
})
// unknown access times
sinon.stub(state, 'getPathToExecutable').returns('/.cache/Cypress/1.2.3/app/cypress')
await cache.list()
// log output snapshot should have a grid of versions
defaultSnapshot('cache list with silent log level')
})
it('lists all versions of cached binary with npm log level warn', async function () {
restoreEnv = mockedEnv({
npm_config_loglevel: 'warn',
})
// unknown access times
sinon.stub(state, 'getPathToExecutable').returns('/.cache/Cypress/1.2.3/app/cypress')
await cache.list()
// log output snapshot should have a grid of versions
defaultSnapshot('cache list with warn log level')
})
it('lists all versions of cached binary with last access', async function () {
sinon.stub(state, 'getPathToExecutable').returns('/.cache/Cypress/1.2.3/app/cypress')
const statAsync = sinon.stub(fs, 'statAsync')
statAsync.onFirstCall().resolves({
atime: dayjs().subtract(3, 'month').valueOf(),
})
statAsync.onSecondCall().resolves({
atime: dayjs().subtract(5, 'day').valueOf(),
})
await cache.list()
await snapshotWithHtml('list-of-versions.html')
})
it('some versions have never been opened', async function () {
sinon.stub(state, 'getPathToExecutable').returns('/.cache/Cypress/1.2.3/app/cypress')
const statAsync = sinon.stub(fs, 'statAsync')
statAsync.onFirstCall().resolves({
atime: dayjs().subtract(3, 'month').valueOf(),
})
// the second binary has never been accessed
statAsync.onSecondCall().resolves()
await cache.list()
await snapshotWithHtml('second-binary-never-used.html')
})
it('shows sizes', async function () {
sinon.stub(state, 'getPathToExecutable').returns('/.cache/Cypress/1.2.3/app/cypress')
const statAsync = sinon.stub(fs, 'statAsync')
statAsync.onFirstCall().resolves({
atime: dayjs().subtract(3, 'month').valueOf(),
})
// the second binary has never been accessed
statAsync.onSecondCall().resolves()
await cache.list(true)
await snapshotWithHtml('show-size.html')
})
})
})

View File

@@ -1,3 +1,5 @@
import { describe, it, expect } from 'vitest'
/**
* as of Webpack 5, dependencies that are polyfilled through the Provide plugin must be defined inside the CLI
* in order to guarantee there is a version of the dependency accessible by the cypress CLI, either in the cypress directory
@@ -5,22 +7,25 @@
*/
describe('dependencies', () => {
it('process dependency exists in package.json and is available', async () => {
// @ts-expect-error resolveJsonModule is set to true in tsconfig.json
const { dependencies } = (await import('../../../package.json')).default
expect(dependencies.process).to.be.ok
expect(dependencies.process).toBeDefined()
const process = await import('process')
expect(typeof process).to.equal('object')
expect(typeof process).toEqual('object')
})
it('buffer dependency exists in package.json and is available', async () => {
// @ts-expect-error resolveJsonModule is set to true in tsconfig.json
const { dependencies } = (await import('../../../package.json')).default
expect(dependencies.buffer).to.be.ok
expect(dependencies.buffer).toBeDefined()
const buffer = await import('buffer')
expect(typeof buffer).to.equal('object')
expect(typeof buffer).toEqual('object')
})
})

View File

@@ -0,0 +1,805 @@
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'
import os from 'os'
import si from 'systeminformation'
import path from 'path'
import nock from 'nock'
import hasha from 'hasha'
import createDebug from 'debug'
import execa from 'execa'
import fs from 'fs-extra'
import { Console } from 'console'
import normalize from '../../support/normalize'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import download from '../../../lib/tasks/download'
const debug = createDebug('test')
const downloadDestination = path.join(os.tmpdir(), 'Cypress', 'download', 'cypress.zip')
const version = '1.2.3'
const examplePath = 'test/fixture/example.zip'
vi.mock('execa')
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
arch: vi.fn(),
},
}
})
vi.mock('fs-extra', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
ensureDir: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgVersion: vi.fn(),
cwd: vi.fn(),
},
}
})
describe('lib/tasks/download', function () {
const rootFolder = '/home/user/git'
let options: any
const createStdoutCapture = () => {
const logs: string[] = []
// eslint-disable-next-line no-console
const originalOut = process.stdout.write
vi.spyOn(process.stdout, 'write').mockImplementation((strOrBugger: string | Uint8Array<ArrayBufferLike>) => {
logs.push(strOrBugger as string)
return originalOut(strOrBugger)
})
return () => logs.join('')
}
// Direct console to process.stdout/stderr
let originalConsole: Console
beforeEach(async () => {
vi.resetAllMocks()
vi.unstubAllEnvs()
nock.cleanAll()
// make sure to clear out the cached arch in the util singleton
util._cachedArch = undefined
originalConsole = globalThis.console
// Redirect console output to a custom stream or mock
globalThis.console = new Console(process.stdout, process.stderr)
logger.reset()
options = {
downloadDestination,
version,
}
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('OS')
// @ts-expect-error mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
// @ts-expect-error mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
// @ts-expect-error mockReturnValue
util.cwd.mockReturnValue(rootFolder)
const actualFsExtra = await vi.importActual<typeof import('fs-extra')>('fs-extra')
// @ts-expect-error - mockImplementation to pass through to the actual implementation to prevent issues with request-progress
fs.ensureDir.mockImplementation(actualFsExtra.ensureDir)
})
afterEach(() => {
globalThis.console = originalConsole // Restore original console
})
describe('download url', () => {
it('returns url', () => {
const url = download.getUrl('ARCH')
expect(() => new URL(url)).not.toThrow()
})
it('returns latest desktop url', () => {
const url = download.getUrl('ARCH')
expect(normalize(url)).toMatchSnapshot('latest desktop url 1')
})
it('returns specific desktop version url', () => {
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('specific version desktop url 1')
})
it('returns custom url from template', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', '${endpoint}/${platform}-${arch}/cypress.zip')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('desktop url from template')
})
it('returns custom url from template with version', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', 'https://mycompany/${version}/${platform}-${arch}/cypress.zip')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('desktop url from template with version')
})
it('returns custom url from template with multiple replacements', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', '${endpoint}/${platform}/${arch}/cypress-${version}-${platform}-${arch}.zip?referrer=${endpoint}&version=${version}')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('desktop url from template with multiple replacements')
})
it('returns custom url from template with escaped dollar sign', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', '\\${endpoint}/\\${platform}-\\${arch}/cypress.zip')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('desktop url from template with escaped dollar sign')
})
it('returns custom url from template wrapped in quote', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', '"${endpoint}/${platform}-${arch}/cypress.zip"')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('desktop url from template wrapped in quote')
})
it('returns custom url from template with escaped dollar sign wrapped in quote', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', '"\\${endpoint}/\\${platform}-\\${arch}/cypress.zip"')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('desktop url from template with escaped dollar sign wrapped in quote')
})
it('returns input if it is already an https link', () => {
const url = 'https://somewhere.com'
const result = download.getUrl('ARCH', url)
expect(result).toEqual(url)
})
it('returns input if it is already an http link', () => {
const url = 'http://local.com'
const result = download.getUrl('ARCH', url)
expect(result).toEqual(url)
})
})
describe('download base url from CYPRESS_DOWNLOAD_MIRROR env var', () => {
it('env var', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_MIRROR', 'https://cypress.example.com')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('base url from CYPRESS_DOWNLOAD_MIRROR 1')
})
it('env var with trailing slash', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_MIRROR', 'https://cypress.example.com/')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('base url from CYPRESS_DOWNLOAD_MIRROR with trailing slash 1')
})
it('env var with subdirectory', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_MIRROR', 'https://cypress.example.com/example')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory 1')
})
it('env var with subdirectory and trailing slash', () => {
vi.stubEnv('CYPRESS_DOWNLOAD_MIRROR', 'https://cypress.example.com/example/')
const url = download.getUrl('ARCH', '0.20.2')
expect(normalize(url)).toMatchSnapshot('base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory and trailing slash 1')
})
})
it('saves example.zip to options.downloadDestination', async function () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.1',
})
const onProgress = vi.fn().mockReturnValue(undefined)
const responseVersion = await download.start({
downloadDestination: options.downloadDestination,
version: options.version,
progress: { onProgress },
})
expect(responseVersion).to.eq('0.11.1')
await fs.stat(downloadDestination)
})
describe('verify downloaded file', function () {
let expectedChecksum: string
let expectedFileSize: number
let onProgress: vi.Mock
beforeEach(function () {
expectedChecksum = hasha.fromFileSync(examplePath)
expectedFileSize = fs.statSync(examplePath).size
onProgress = vi.fn().mockReturnValue(undefined)
debug('example file %s should have checksum %s and file size %d',
examplePath, expectedChecksum, expectedFileSize)
})
it('throws if file size is different from expected', async function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
// definitely incorrect file size
'content-length': '10',
})
await expect(download.start({
downloadDestination: options.downloadDestination,
version: options.version,
progress: { onProgress },
})).rejects.toThrow()
})
it('throws if file size is different from expected x-amz-meta-size', async function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
// definitely incorrect file size
'x-amz-meta-size': '10',
})
await expect(download.start({
downloadDestination: options.downloadDestination,
version: options.version,
progress: { onProgress },
})).rejects.toThrow()
})
it('throws if checksum is different from expected', async function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
'x-amz-meta-checksum': 'incorrect-checksum',
})
await expect(download.start({
downloadDestination: options.downloadDestination,
version: options.version,
progress: { onProgress },
})).rejects.toThrow()
})
it('throws if checksum and file size are different from expected', async function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
'x-amz-meta-checksum': 'incorrect-checksum',
'x-amz-meta-size': '10',
})
await expect(download.start({
downloadDestination: options.downloadDestination,
version: options.version,
progress: { onProgress },
})).rejects.toThrow()
})
it('passes when checksum and file size match', async function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
debug('creating read stream for %s', examplePath)
return fs.createReadStream(examplePath)
}, {
'x-amz-meta-checksum': expectedChecksum,
'x-amz-meta-size': String(expectedFileSize),
})
debug('downloading %s to %s for test version %s',
examplePath, options.downloadDestination, options.version)
await download.start({
downloadDestination: options.downloadDestination,
version: options.version,
progress: { onProgress },
})
})
})
it('resolves with response x-version if present', async function () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.1',
})
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('0.11.1')
})
it('handles quadruple redirect with response x-version to the latest if present', async function () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://aws.amazon.com')
.get('/someone.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somebody.zip',
'x-version': '0.11.2',
})
nock('https://aws.amazon.com')
.get('/something.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.4',
})
nock('https://aws.amazon.com')
.get('/somebody.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/something.zip',
'x-version': '0.11.3',
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/someone.zip',
'x-version': '0.11.1',
})
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('0.11.4')
})
it('errors on too many redirects', async function () {
function stubRedirects () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/someone.zip',
'x-version': '0.11.1',
})
nock('https://aws.amazon.com')
.get('/someone.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somebody.zip',
'x-version': '0.11.2',
})
nock('https://aws.amazon.com')
.get('/somebody.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/something.zip',
'x-version': '0.11.3',
})
nock('https://aws.amazon.com')
.get('/something.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somewhat.zip',
'x-version': '0.11.4',
})
nock('https://aws.amazon.com')
.get('/somewhat.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/sometime.zip',
'x-version': '0.11.5',
})
nock('https://aws.amazon.com')
.get('/sometime.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somewhen.zip',
'x-version': '0.11.6',
})
nock('https://aws.amazon.com')
.get('/somewhen.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somewise.zip',
'x-version': '0.11.7',
})
nock('https://aws.amazon.com')
.get('/somewise.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/someways.zip',
'x-version': '0.11.8',
})
nock('https://aws.amazon.com')
.get('/someways.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somerset.zip',
'x-version': '0.11.9',
})
nock('https://aws.amazon.com')
.get('/somerset.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somedeal.zip',
'x-version': '0.11.10',
})
nock('https://aws.amazon.com')
.get('/somedeal.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.11',
})
}
stubRedirects()
try {
await download.start(options)
throw new Error('should have caught')
} catch (error) {
expect(error).to.be.instanceof(Error)
expect(error.message).to.contain('redirect loop')
}
stubRedirects()
// Double check to make sure that raising redirectTTL changes result
const responseVersion = await download.start({ ...options, redirectTTL: 12 })
expect(responseVersion).to.eq('0.11.11')
})
it('can specify cypress version in arguments', async function () {
options.version = '0.13.0'
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/0.13.0')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.13.0',
})
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('0.13.0')
await fs.stat(downloadDestination)
})
describe('architecture detection', () => {
describe('Apple Silicon/M1', () => {
function nockDarwinArm64 () {
return nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query({ arch: 'arm64', platform: 'darwin' })
.reply(200, undefined, {
'x-version': '1.2.3',
})
}
it('downloads darwin-arm64 on M1', async function () {
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error mockReturnValue
os.arch.mockReturnValue('arm64')
nockDarwinArm64()
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('1.2.3')
await fs.stat(downloadDestination)
})
it('downloads darwin-arm64 on M1 translated by Rosetta', async function () {
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error mockReturnValue
os.arch.mockReturnValue('x64')
nockDarwinArm64()
// @ts-expect-error mockImplementation
execa.mockImplementation((command, args, options) => {
if (command === 'sysctl' && args[0] === '-n' && args[1] === 'sysctl.proc_translated') {
return Promise.resolve({
// will force arm64 inside util.getRealArch()
stdout: '1',
})
}
})
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('1.2.3')
await fs.stat(downloadDestination)
})
})
describe('Linux arm64/aarch64', () => {
function nockLinuxArm64 () {
return nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query({ arch: 'arm64', platform: 'linux' })
.reply(200, undefined, {
'x-version': '1.2.3',
})
}
it('downloads linux-arm64 on arm64 processor', async function () {
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error mockReturnValue
os.arch.mockReturnValue('arm64')
nockLinuxArm64()
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('1.2.3')
await fs.stat(downloadDestination)
})
it('downloads linux-arm64 on non-arm64 node running on arm machine', async function () {
// @ts-expect-error mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error mockReturnValue
os.arch.mockReturnValue('x64')
for (const arch of ['aarch64_be', 'aarch64', 'armv8b', 'armv8l']) {
nockLinuxArm64()
// @ts-expect-error mockImplementation
execa.mockImplementation((command, args, options) => {
if (command === 'uname' && args[0] === '-m') {
return Promise.resolve({
// will force arm64 inside util.getRealArch()
stdout: arch,
})
}
})
const responseVersion = await download.start(options)
expect(responseVersion).to.eq('1.2.3')
await fs.stat(downloadDestination)
}
})
})
})
it('catches download status errors and exits', async function () {
const output = createStdoutCapture()
const err: any = new Error()
err.statusCode = 404
err.statusMessage = 'Not Found'
options.version = null
// not really the download error, but the easiest way to
// test the error handling
// @ts-expect-error mockRejectedValue
fs.ensureDir.mockRejectedValue(err)
try {
await download.start(options)
throw new Error('should have caught')
} catch (error) {
expect(error.message).not.toEqual('should have caught')
logger.error(error)
expect(output()).toMatchSnapshot('download status errors 1')
}
})
})
describe('with proxy env vars', () => {
const testUriHttp = 'http://anything.com'
const testUriHttps = 'https://anything.com'
beforeEach(function () {
// prevent ambient environment masking of environment variables referenced in this test
vi.unstubAllEnvs()
// add a default no_proxy which does not match the testUri
vi.stubEnv('NO_PROXY', 'localhost,.org')
})
it('uses http_proxy on http request', () => {
vi.stubEnv('http_proxy', 'http://foo')
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).toEqual('http://foo')
})
it('ignores http_proxy on https request', () => {
vi.stubEnv('http_proxy', 'http://foo')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual(null)
vi.stubEnv('https_proxy', 'https://bar')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual('https://bar')
})
it('falls back to npm_config_proxy', () => {
vi.stubEnv('npm_config_proxy', 'http://foo')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual('http://foo')
vi.stubEnv('npm_config_https_proxy', 'https://bar')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual('https://bar')
vi.stubEnv('https_proxy', 'https://baz')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual('https://baz')
})
it('respects no_proxy on http and https requests', () => {
vi.stubEnv('NO_PROXY', 'localhost,.com')
vi.stubEnv('http_proxy', 'http://foo')
vi.stubEnv('https_proxy', 'https://bar')
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).toBeNull()
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toBeNull()
})
it('ignores no_proxy for npm proxy configs, prefers https over http', () => {
vi.stubEnv('NO_PROXY', 'localhost,.com')
vi.stubEnv('npm_config_proxy', 'http://foo')
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).toEqual('http://foo')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual('http://foo')
vi.stubEnv('npm_config_https_proxy', 'https://bar')
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).toEqual('https://bar')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).toEqual('https://bar')
})
})
describe('with CA and CAFILE env vars', () => {
it('returns undefined if not set', async () => {
const ca = await download.getCA()
expect(ca).toBeUndefined()
})
it('returns CA from npm_config_ca', async () => {
vi.stubEnv('npm_config_ca', 'foo')
const ca = await download.getCA()
expect(ca).toEqual('foo')
})
it('returns CA from npm_config_cafile', async () => {
vi.stubEnv('npm_config_cafile', 'test/fixture/cafile.pem')
const ca = await download.getCA()
expect(ca).toEqual('bar\n')
})
it('returns undefined if failed reading npm_config_cafile', async () => {
vi.stubEnv('npm_config_cafile', 'test/fixture/not-exists.pem')
const ca = await download.getCA()
expect(ca).toBeUndefined()
})
})

View File

@@ -1,723 +0,0 @@
import '../../spec_helper'
import _ from 'lodash'
import os from 'os'
import cp from 'child_process'
import la from 'lazy-ass'
import is from 'check-more-types'
import path from 'path'
import nock from 'nock'
import hasha from 'hasha'
import createDebug from 'debug'
import snapshot from '../../support/snapshot'
import stdout from '../../support/stdout'
import normalize from '../../support/normalize'
import mockSpawnModule from '../../support/spawn-mock'
import fs from '../../../lib/fs'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import download from '../../../lib/tasks/download'
const debug = createDebug('test')
const downloadDestination = path.join(os.tmpdir(), 'Cypress', 'download', 'cypress.zip')
const version = '1.2.3'
const examplePath = 'test/fixture/example.zip'
describe('lib/tasks/download', function () {
before(async function () {
const mochaMain = await import('mocha-banner')
mochaMain.register()
})
const rootFolder = '/home/user/git'
beforeEach(function () {
logger.reset()
;(this as any).stdout = stdout.capture()
;(this as any).options = {
downloadDestination,
version,
}
;(os.platform as any).returns('OS')
sinon.stub(util, 'pkgVersion').returns('1.2.3')
sinon.stub(util, 'cwd').returns(rootFolder)
})
afterEach(function () {
stdout.restore()
})
context('download url', () => {
it('returns url', () => {
const url = download.getUrl('ARCH')
la((is as any).url(url), url)
})
it('returns latest desktop url', () => {
const url = download.getUrl('ARCH')
snapshot('latest desktop url 1', normalize(url))
})
it('returns specific desktop version url', () => {
const url = download.getUrl('ARCH', '0.20.2')
snapshot('specific version desktop url 1', normalize(url))
})
it('returns custom url from template', () => {
process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '${endpoint}/${platform}-${arch}/cypress.zip'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('desktop url from template', normalize(url))
})
it('returns custom url from template with version', () => {
process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = 'https://mycompany/${version}/${platform}-${arch}/cypress.zip'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('desktop url from template with version', normalize(url))
})
it('returns custom url from template with multiple replacements', () => {
process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '${endpoint}/${platform}/${arch}/cypress-${version}-${platform}-${arch}.zip?referrer=${endpoint}&version=${version}'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('desktop url from template with multiple replacements', normalize(url))
})
it('returns custom url from template with escaped dollar sign', () => {
process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '\\${endpoint}/\\${platform}-\\${arch}/cypress.zip'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('desktop url from template with escaped dollar sign', normalize(url))
})
it('returns custom url from template wrapped in quote', () => {
process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '"${endpoint}/${platform}-${arch}/cypress.zip"'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('desktop url from template wrapped in quote', normalize(url))
})
it('returns custom url from template with escaped dollar sign wrapped in quote', () => {
process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '"\\${endpoint}/\\${platform}-\\${arch}/cypress.zip"'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('desktop url from template with escaped dollar sign wrapped in quote', normalize(url))
})
it('returns input if it is already an https link', () => {
const url = 'https://somewhere.com'
const result = download.getUrl('ARCH', url)
expect(result).to.equal(url)
})
it('returns input if it is already an http link', () => {
const url = 'http://local.com'
const result = download.getUrl('ARCH', url)
expect(result).to.equal(url)
})
})
context('download base url from CYPRESS_DOWNLOAD_MIRROR env var', () => {
it('env var', () => {
process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('base url from CYPRESS_DOWNLOAD_MIRROR 1', normalize(url))
})
it('env var with trailing slash', () => {
process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com/'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('base url from CYPRESS_DOWNLOAD_MIRROR with trailing slash 1', normalize(url))
})
it('env var with subdirectory', () => {
process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com/example'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory 1', normalize(url))
})
it('env var with subdirectory and trailing slash', () => {
process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com/example/'
const url = download.getUrl('ARCH', '0.20.2')
snapshot('base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory and trailing slash 1', normalize(url))
})
})
it('saves example.zip to options.downloadDestination', function () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.1',
})
const onProgress = sinon.stub().returns(undefined)
return download.start({
downloadDestination: (this as any).options.downloadDestination,
version: (this as any).options.version,
progress: { onProgress },
})
.then((responseVersion: any) => {
expect(responseVersion).to.eq('0.11.1')
return fs.statAsync(downloadDestination)
})
})
context('verify downloaded file', function () {
before(function () {
(this as any).expectedChecksum = hasha.fromFileSync(examplePath)
;(this as any).expectedFileSize = fs.statSync(examplePath).size
;(this as any).onProgress = sinon.stub().returns(undefined)
debug('example file %s should have checksum %s and file size %d',
examplePath, (this as any).expectedChecksum, (this as any).expectedFileSize)
})
it('throws if file size is different from expected', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
// definitely incorrect file size
'content-length': '10',
})
return expect(download.start({
downloadDestination: (this as any).options.downloadDestination,
version: (this as any).options.version,
progress: { onProgress: (this as any).onProgress },
})).to.be.rejected
})
it('throws if file size is different from expected x-amz-meta-size', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
// definitely incorrect file size
'x-amz-meta-size': '10',
})
return expect(download.start({
downloadDestination: (this as any).options.downloadDestination,
version: (this as any).options.version,
progress: { onProgress: (this as any).onProgress },
})).to.be.rejected
})
it('throws if checksum is different from expected', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
'x-amz-meta-checksum': 'incorrect-checksum',
})
return expect(download.start({
downloadDestination: (this as any).options.downloadDestination,
version: (this as any).options.version,
progress: { onProgress: (this as any).onProgress },
})).to.be.rejected
})
it('throws if checksum and file size are different from expected', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream(examplePath)
}, {
'x-amz-meta-checksum': 'incorrect-checksum',
'x-amz-meta-size': '10',
})
return expect(download.start({
downloadDestination: (this as any).options.downloadDestination,
version: (this as any).options.version,
progress: { onProgress: (this as any).onProgress },
})).to.be.rejected
})
it('passes when checksum and file size match', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
debug('creating read stream for %s', examplePath)
return fs.createReadStream(examplePath)
}, {
'x-amz-meta-checksum': (this as any).expectedChecksum,
'x-amz-meta-size': String((this as any).expectedFileSize),
})
debug('downloading %s to %s for test version %s',
examplePath, (this as any).options.downloadDestination, (this as any).options.version)
return download.start({
downloadDestination: (this as any).options.downloadDestination,
version: (this as any).options.version,
progress: { onProgress: (this as any).onProgress },
})
})
})
it('resolves with response x-version if present', function () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.1',
})
return download.start((this as any).options).then((responseVersion: any) => {
expect(responseVersion).to.eq('0.11.1')
})
})
it('handles quadruple redirect with response x-version to the latest if present', function () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://aws.amazon.com')
.get('/someone.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somebody.zip',
'x-version': '0.11.2',
})
nock('https://aws.amazon.com')
.get('/something.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.4',
})
nock('https://aws.amazon.com')
.get('/somebody.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/something.zip',
'x-version': '0.11.3',
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/someone.zip',
'x-version': '0.11.1',
})
return download.start((this as any).options).then((responseVersion: any) => {
expect(responseVersion).to.eq('0.11.4')
})
})
it('errors on too many redirects', async function () {
function stubRedirects () {
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/someone.zip',
'x-version': '0.11.1',
})
nock('https://aws.amazon.com')
.get('/someone.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somebody.zip',
'x-version': '0.11.2',
})
nock('https://aws.amazon.com')
.get('/somebody.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/something.zip',
'x-version': '0.11.3',
})
nock('https://aws.amazon.com')
.get('/something.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somewhat.zip',
'x-version': '0.11.4',
})
nock('https://aws.amazon.com')
.get('/somewhat.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/sometime.zip',
'x-version': '0.11.5',
})
nock('https://aws.amazon.com')
.get('/sometime.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somewhen.zip',
'x-version': '0.11.6',
})
nock('https://aws.amazon.com')
.get('/somewhen.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somewise.zip',
'x-version': '0.11.7',
})
nock('https://aws.amazon.com')
.get('/somewise.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/someways.zip',
'x-version': '0.11.8',
})
nock('https://aws.amazon.com')
.get('/someways.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somerset.zip',
'x-version': '0.11.9',
})
nock('https://aws.amazon.com')
.get('/somerset.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/somedeal.zip',
'x-version': '0.11.10',
})
nock('https://aws.amazon.com')
.get('/somedeal.zip')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.11.11',
})
}
stubRedirects()
await download.start((this as any).options).catch((error: any) => {
expect(error).to.be.instanceof(Error)
expect(error.message).to.contain('redirect loop')
})
stubRedirects()
// Double check to make sure that raising redirectTTL changes result
await download.start({ ...(this as any).options, redirectTTL: 12 }).then((responseVersion: any) => {
expect(responseVersion).to.eq('0.11.11')
})
})
it('can specify cypress version in arguments', function () {
(this as any).options.version = '0.13.0'
nock('https://aws.amazon.com')
.get('/some.zip')
.reply(200, () => {
return fs.createReadStream(examplePath)
})
nock('https://download.cypress.io')
.get('/desktop/0.13.0')
.query(true)
.reply(302, undefined, {
Location: 'https://aws.amazon.com/some.zip',
'x-version': '0.13.0',
})
return download.start((this as any).options).then((responseVersion: any) => {
expect(responseVersion).to.eq('0.13.0')
return fs.statAsync(downloadDestination)
})
})
context('architecture detection', () => {
context('Apple Silicon/M1', () => {
function nockDarwinArm64 () {
return nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query({ arch: 'arm64', platform: 'darwin' })
.reply(200, undefined, {
'x-version': '1.2.3',
})
}
it('downloads darwin-arm64 on M1', async function () {
(os.platform as any).returns('darwin')
;(os.arch as any).returns('arm64')
nockDarwinArm64()
const responseVersion = await download.start((this as any).options)
expect(responseVersion).to.eq('1.2.3')
await fs.statAsync(downloadDestination)
})
it('downloads darwin-arm64 on M1 translated by Rosetta', async function () {
(os.platform as any).returns('darwin')
;(os.arch as any).returns('x64')
nockDarwinArm64()
sinon.stub(cp, 'spawn').withArgs('sysctl', ['-n', 'sysctl.proc_translated'])
.callsFake(mockSpawnModule.mockSpawn(((cp: any) => {
cp.stdout.write('1')
cp.emit('exit', 0, null)
cp.end()
})))
const responseVersion = await download.start((this as any).options)
expect(responseVersion).to.eq('1.2.3')
await fs.statAsync(downloadDestination)
})
})
context('Linux arm64/aarch64', () => {
function nockLinuxArm64 () {
return nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query({ arch: 'arm64', platform: 'linux' })
.reply(200, undefined, {
'x-version': '1.2.3',
})
}
it('downloads linux-arm64 on arm64 processor', async function () {
(os.platform as any).returns('linux')
;(os.arch as any).returns('arm64')
nockLinuxArm64()
const responseVersion = await download.start((this as any).options)
expect(responseVersion).to.eq('1.2.3')
await fs.statAsync(downloadDestination)
})
it('downloads linux-arm64 on non-arm64 node running on arm machine', async function () {
(os.platform as any).returns('linux')
;(os.arch as any).returns('x64')
sinon.stub(cp, 'spawn')
for (const arch of ['aarch64_be', 'aarch64', 'armv8b', 'armv8l']) {
nockLinuxArm64()
;(cp.spawn as any).withArgs('uname', ['-m'])
.callsFake(mockSpawnModule.mockSpawn(((cp: any) => {
cp.stdout.write(arch)
cp.emit('exit', 0, null)
cp.end()
})))
const responseVersion = await download.start((this as any).options)
expect(responseVersion).to.eq('1.2.3')
await fs.statAsync(downloadDestination)
}
})
})
})
it('catches download status errors and exits', function () {
const ctx = this
const err: any = new Error()
err.statusCode = 404
err.statusMessage = 'Not Found'
;(this as any).options.version = null
// not really the download error, but the easiest way to
// test the error handling
sinon.stub(fs, 'ensureDirAsync').rejects(err)
return download
.start((this as any).options)
.then(() => {
throw new Error('should have caught')
})
.catch((err: any) => {
logger.error(err)
return snapshot('download status errors 1', normalize((ctx as any).stdout.toString()))
})
})
context('with proxy env vars', () => {
const testUriHttp = 'http://anything.com'
const testUriHttps = 'https://anything.com'
beforeEach(function () {
(this as any).env = _.clone(process.env)
// prevent ambient environment masking of environment variables referenced in this test
;([
'NO_PROXY', 'http_proxy',
'https_proxy', 'npm_config_ca', 'npm_config_cafile',
'npm_config_https_proxy', 'npm_config_proxy',
]).forEach((e) => {
delete process.env[e.toLowerCase()]
delete process.env[e.toUpperCase()]
})
// add a default no_proxy which does not match the testUri
process.env.NO_PROXY = 'localhost,.org'
})
afterEach(function () {
process.env = (this as any).env
})
it('uses http_proxy on http request', () => {
process.env.http_proxy = 'http://foo'
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).to.eq('http://foo')
})
it('ignores http_proxy on https request', () => {
process.env.http_proxy = 'http://foo'
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq(null)
process.env.https_proxy = 'https://bar'
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq('https://bar')
})
it('falls back to npm_config_proxy', () => {
process.env.npm_config_proxy = 'http://foo'
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq('http://foo')
process.env.npm_config_https_proxy = 'https://bar'
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq('https://bar')
process.env.https_proxy = 'https://baz'
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq('https://baz')
})
it('respects no_proxy on http and https requests', () => {
process.env.NO_PROXY = 'localhost,.com'
process.env.http_proxy = 'http://foo'
process.env.https_proxy = 'https://bar'
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).to.eq(null)
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq(null)
})
it('ignores no_proxy for npm proxy configs, prefers https over http', () => {
process.env.NO_PROXY = 'localhost,.com'
process.env.npm_config_proxy = 'http://foo'
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).to.eq('http://foo')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq('http://foo')
process.env.npm_config_https_proxy = 'https://bar'
expect(download.getProxyForUrlWithNpmConfig(testUriHttp)).to.eq('https://bar')
expect(download.getProxyForUrlWithNpmConfig(testUriHttps)).to.eq('https://bar')
})
})
context('with CA and CAFILE env vars', () => {
beforeEach(function () {
(this as any).env = _.clone(process.env)
})
afterEach(function () {
process.env = (this as any).env
})
it('returns undefined if not set', () => {
return download.getCA().then((ca: any) => {
expect(ca).to.be.undefined
})
})
it('returns CA from npm_config_ca', () => {
process.env.npm_config_ca = 'foo'
return download.getCA().then((ca: any) => {
expect(ca).to.eqls('foo')
})
})
it('returns CA from npm_config_cafile', () => {
process.env.npm_config_cafile = 'test/fixture/cafile.pem'
return download.getCA().then((ca: any) => {
expect(ca).to.eqls('bar\n')
})
})
it('returns undefined if failed reading npm_config_cafile', () => {
process.env.npm_config_cafile = 'test/fixture/not-exists.pem'
return download.getCA().then((ca: any) => {
expect(ca).to.be.undefined
})
})
})
})

View File

@@ -0,0 +1,694 @@
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'
import os from 'os'
import path from 'path'
import chalk from 'chalk'
import timers from 'timers/promises'
import normalize from '../../support/normalize'
import fs from 'fs-extra'
import si from 'systeminformation'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import download from '../../../lib/tasks/download'
import unzip from '../../../lib/tasks/unzip'
import install from '../../../lib/tasks/install'
import state from '../../../lib/tasks/state'
import { Console } from 'console'
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
arch: vi.fn(),
},
}
})
vi.mock('timers/promises', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
setTimeout: vi.fn(),
},
}
})
vi.mock('fs-extra', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
remove: vi.fn(),
ensureDir: vi.fn(),
pathExists: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgVersion: vi.fn(),
isCi: vi.fn(),
isPostInstall: vi.fn(),
getPlatformInfo: vi.fn(),
isInstalledGlobally: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/download', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/unzip', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
},
}
})
vi.mock('../../../lib/tasks/state', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getVersionDir: vi.fn(),
getBinaryDir: vi.fn(),
getBinaryPkgAsync: vi.fn(),
getCacheDir: vi.fn(),
},
}
})
const packageVersion = '1.2.3'
const downloadDestination = path.join(os.tmpdir(), `cypress-${process.pid}.zip`)
const installDir = '/cache/Cypress/1.2.3'
/**
* NOTE: icons from listr2 do not render if process.stdout.isTTY is false,
* which does not exist when running in a worker thread, which is commonly the case in Vitest.
*
* This means that the test environment implicitly uses the VerboseRenderer as a fallback,
* where as the CLI uses the DefaultRenderer.
*
* This is the main reason the snapshots look different in testing mode vs when running the commands directly
* via the CLI. This also allows us for our snapshot tests to be deterministic because we aren't rerendering icon states.
*
* @see https://listr2.kilic.dev/renderer/renderer.html#frontmatter-title
*/
describe('/lib/tasks/install', function () {
const createStdoutCapture = () => {
const logs: string[] = []
// eslint-disable-next-line no-console
const originalOut = process.stdout.write
vi.spyOn(process.stdout, 'write').mockImplementation((strOrBugger: string | Uint8Array<ArrayBufferLike>) => {
logs.push(strOrBugger as string)
return originalOut(strOrBugger)
})
return () => logs.join('')
}
// Direct console to process.stdout/stderr
let originalConsole: Console
beforeEach(() => {
vi.resetAllMocks()
vi.unstubAllEnvs()
vi.stubEnv('npm_config_loglevel', 'notice')
// allow simpler log message comparison without
// chalk's terminal control strings
chalk.level = 0
originalConsole = globalThis.console
// Redirect console output to a custom stream or mock
globalThis.console = new Console(process.stdout, process.stderr)
})
afterEach(() => {
globalThis.console = originalConsole // Restore original console
chalk.level = 3
})
describe('.start', function () {
beforeEach(async () => {
logger.reset()
// @ts-expect-error - mockReturnValue
util.isCi.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
util.isPostInstall.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue(packageVersion)
// @ts-expect-error - mockResolvedValue
download.start.mockResolvedValue(packageVersion)
// @ts-expect-error - mockResolvedValue
unzip.start.mockResolvedValue(undefined)
// @ts-expect-error - mockReturnValue
state.getVersionDir.mockReturnValue('/cache/Cypress/1.2.3')
// @ts-expect-error - mockReturnValue
state.getBinaryDir.mockReturnValue('/cache/Cypress/1.2.3/Cypress.app')
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue(undefined)
// @ts-expect-error - mockResolvedValue
fs.remove.mockResolvedValue(undefined)
// @ts-expect-error - mockResolvedValue
fs.ensureDir.mockResolvedValue(undefined)
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
os.arch.mockReturnValue('x64')
// @ts-expect-error mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
// @ts-expect-error - mockResolvedValue
timers.setTimeout.mockResolvedValue(undefined)
const actualUtil = (await vi.importActual<typeof import('../../../lib/util')>('../../../lib/util')).default
// @ts-expect-error - mockImplementation
util.getPlatformInfo.mockImplementation(actualUtil.getPlatformInfo)
})
describe('skips install', function () {
it('when environment variable is set', async () => {
const output = createStdoutCapture()
vi.stubEnv('CYPRESS_INSTALL_BINARY', '0')
await install.start()
expect(download.start).not.toHaveBeenCalled()
expect(output()).toMatchSnapshot('skip installation 1')
})
})
describe('non-stable builds', () => {
const buildInfo = {
stable: false,
commitSha: '3b7f0b5c59def1e9b5f385bd585c9b2836706c29',
commitBranch: 'aBranchName',
commitDate: new Date('1996-11-27').toISOString(),
}
it('install from a constructed CDN URL', async function () {
await install.start({ buildInfo })
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version: 'https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-3b7f0b5c59def1e9b5f385bd585c9b2836706c29/cypress.zip',
}))
})
it('logs a warning about installing a pre-release', async function () {
const output = createStdoutCapture()
await install.start({ buildInfo })
expect(output()).toMatchSnapshot('pre-release warning')
})
it('installs to the expected pre-release cache dir', async function () {
const actualState = (await vi.importActual<typeof import('../../../lib/tasks/state')>('../../../lib/tasks/state')).default
// @ts-expect-error - mockImplementation
state.getVersionDir.mockImplementation(actualState.getVersionDir)
await install.start({ buildInfo })
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
installDir: expect.stringMatching(/\/Cypress\/beta\-1\.2\.3\-aBranchName\-3b7f0b5c$/),
}))
})
})
describe('override version', function () {
it('warns when specifying cypress version in env', async function () {
const output = createStdoutCapture()
const version = '0.12.1'
vi.stubEnv('CYPRESS_INSTALL_BINARY', version)
await install.start()
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version,
}))
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
zipFilePath: downloadDestination,
}))
expect(normalize(output())).toMatchSnapshot('specify version in env vars 1')
})
it('trims environment variable before installing', async function () {
// note how the version has extra spaces around it on purpose
const filename = '/tmp/local/file.zip'
const version = ` ${filename} `
vi.stubEnv('CYPRESS_INSTALL_BINARY', version)
// internally, the variable should be trimmed and just filename checked
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === filename) {
return true
}
})
const installDir = state.getVersionDir()
await install.start()
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
zipFilePath: filename,
installDir,
}))
})
it('removes double quotes around the environment variable before installing', async function () {
// note how the version has extra spaces around it on purpose
// and there are double quotes
const filename = '/tmp/local/file.zip'
const version = ` "${filename}" `
vi.stubEnv('CYPRESS_INSTALL_BINARY', version)
// internally, the variable should be trimmed, double quotes removed
// and just filename checked against the file system
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === filename) {
return true
}
})
const installDir = state.getVersionDir()
await install.start()
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
zipFilePath: filename,
installDir,
}))
})
it('can install local binary zip file without download from absolute path', async function () {
const version = '/tmp/local/file.zip'
vi.stubEnv('CYPRESS_INSTALL_BINARY', version)
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === version) {
return true
}
})
const installDir = state.getVersionDir()
await install.start()
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
zipFilePath: version,
installDir,
}))
})
it('can install local binary zip file from relative path', async function () {
const version = './cypress-resources/file.zip'
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === version) {
return true
}
})
vi.stubEnv('CYPRESS_INSTALL_BINARY', version)
const installDir = state.getVersionDir()
await install.start()
expect(download.start).not.toHaveBeenCalled()
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
zipFilePath: path.resolve(version),
installDir,
}))
})
describe('when version is already installed', function () {
beforeEach(function () {
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: packageVersion })
})
it('doesn\'t attempt to download', async function () {
await install.start()
expect(download.start).not.toHaveBeenCalled()
expect(state.getBinaryPkgAsync).toHaveBeenCalledWith('/cache/Cypress/1.2.3/Cypress.app')
})
it('logs \'skipping install\' when explicit cypress install', async function () {
const output = createStdoutCapture()
await install.start()
expect(normalize(output())).toMatchSnapshot('version already installed - cypress install 1')
})
it('logs when already installed when run from postInstall', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockReturnValue
util.isPostInstall.mockReturnValue(true)
await install.start()
expect(normalize(output())).toMatchSnapshot('version already installed - postInstall 1')
})
})
describe('when getting installed version fails', function () {
it('logs message and starts download', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue(null)
await install.start()
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version: packageVersion,
}))
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
installDir,
}))
expect(normalize(output())).toMatchSnapshot('continues installing on failure 1')
})
})
describe('when there is no install version', function () {
it('logs message and starts download', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue(null)
await install.start()
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version: packageVersion,
}))
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
installDir,
}))
// cleans up the zip file
expect(fs.remove).toHaveBeenCalledWith(
downloadDestination,
)
expect(normalize(output())).toMatchSnapshot('installs without existing installation 1')
})
})
describe('when getting installed version does not match needed version', function () {
it('logs message and starts download', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: 'x.x.x' })
await install.start()
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version: packageVersion,
}))
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
installDir,
}))
expect(normalize(output())).toMatchSnapshot('installed version does not match needed version 1')
})
})
describe('with force: true', function () {
it('logs message and starts download', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: packageVersion })
await install.start({ force: true })
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version: packageVersion,
}))
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
installDir,
}))
expect(normalize(output())).toMatchSnapshot('forcing true always installs 1')
})
})
describe('as a global install', function () {
it('logs global warning and download', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockReturnValue
util.isInstalledGlobally.mockReturnValue(true)
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: 'x.x.x' })
await install.start()
expect(download.start).toHaveBeenCalledWith(expect.objectContaining({
version: packageVersion,
}))
expect(unzip.start).toHaveBeenCalledWith(expect.objectContaining({
installDir,
}))
expect(normalize(output())).toMatchSnapshot('warning installing as global 1')
})
})
describe('when running in CI', function () {
it('uses verbose renderer', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockReturnValue
util.isCi.mockReturnValue(true)
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: 'x.x.x' })
await install.start()
expect(normalize(output())).toMatchSnapshot('installing in ci 1')
})
})
describe('failed write access to cache directory', function () {
it('logs error on failure', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
state.getCacheDir.mockReturnValue('/invalid/cache/dir')
const err: any = new Error('EACCES: permission denied, mkdir \'/invalid\'')
err.code = 'EACCES'
// @ts-expect-error - mockRejectedValue
fs.ensureDir.mockRejectedValue(err)
try {
await install.start()
throw new Error('should have caught error')
} catch (err) {
expect(err.message).not.toEqual('should have caught error')
logger.error(err)
expect(normalize(output())).toMatchSnapshot('invalid cache directory 1')
}
})
})
describe('CYPRESS_INSTALL_BINARY is URL or Zip', function () {
it('uses cache when correct version installed given URL', async function () {
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: '1.2.3' })
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
vi.stubEnv('CYPRESS_INSTALL_BINARY', 'www.cypress.io/cannot-download/2.4.5')
await install.start()
expect(download.start).not.toHaveBeenCalled()
})
it('uses cache when mismatch version given URL', async function () {
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: '1.2.3' })
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('4.0.0')
vi.stubEnv('CYPRESS_INSTALL_BINARY', 'www.cypress.io/cannot-download/2.4.5')
await install.start()
expect(download.start).not.toHaveBeenCalled()
})
it('uses cache when correct version installed given Zip', async function () {
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === '/path/to/zip.zip') {
return true
}
})
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: '1.2.3' })
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
vi.stubEnv('CYPRESS_INSTALL_BINARY', '/path/to/zip.zip')
await install.start()
expect(unzip.start).not.toHaveBeenCalled()
})
it('uses cache when mismatch version given Zip ', async function () {
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === '/path/to/zip.zip') {
return true
}
})
// @ts-expect-error - mockResolvedValue
state.getBinaryPkgAsync.mockResolvedValue({ version: '1.2.3' })
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('4.0.0')
vi.stubEnv('CYPRESS_INSTALL_BINARY', '/path/to/zip.zip')
await install.start()
expect(unzip.start).not.toHaveBeenCalled()
})
})
})
it('is silent when log level is silent', async function () {
const output = createStdoutCapture()
vi.stubEnv('npm_config_loglevel', 'silent')
await install.start()
expect(normalize(output())).toMatchSnapshot('silent install 1')
})
it('exits with error when installing on unsupported os', async function () {
const output = createStdoutCapture()
// @ts-expect-error - mockResolvedValue
util.getPlatformInfo.mockResolvedValue('Platform: win32-ia32')
try {
await install.start()
throw new Error('should have caught error')
} catch (err) {
expect(err.message).not.toEqual('should have caught error')
logger.error(err)
expect(normalize(output())).toMatchSnapshot('error when installing on unsupported os')
}
})
})
describe('._getBinaryUrlFromBuildInfo', function () {
const buildInfo = {
commitSha: 'abc123',
commitBranch: 'aBranchName',
}
it('generates the expected URL', () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
expect(install._getBinaryUrlFromBuildInfo('x64', buildInfo)).toEqual(`https://cdn.cypress.io/beta/binary/0.0.0-development/linux-x64/aBranchName-abc123/cypress.zip`)
})
it('overrides win32-arm64 to win32-x64 for pre-release', () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
expect(install._getBinaryUrlFromBuildInfo('arm64', buildInfo))
.to.eq(`https://cdn.cypress.io/beta/binary/0.0.0-development/win32-x64/aBranchName-abc123/cypress.zip`)
})
})
})

View File

@@ -1,520 +0,0 @@
import '../../spec_helper'
import os from 'os'
import path from 'path'
import chalk from 'chalk'
import BluebirdPromise from 'bluebird'
import mockfs from 'mock-fs'
import snapshot from '../../support/snapshot'
import stdout from '../../support/stdout'
import normalize from '../../support/normalize'
import fs from '../../../lib/fs'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import download from '../../../lib/tasks/download'
import unzip from '../../../lib/tasks/unzip'
import install from '../../../lib/tasks/install'
import state from '../../../lib/tasks/state'
const packageVersion = '1.2.3'
const downloadDestination = path.join(os.tmpdir(), `cypress-${process.pid}.zip`)
const installDir = '/cache/Cypress/1.2.3'
describe('/lib/tasks/install', function () {
before(async function () {
const mochaMain = await import('mocha-banner')
mochaMain.register()
})
beforeEach(function () {
(this as any).stdout = stdout.capture()
// allow simpler log message comparison without
// chalk's terminal control strings
chalk.level = 0
})
afterEach(() => {
stdout.restore()
chalk.level = 3
})
context('.start', function () {
beforeEach(function () {
logger.reset()
sinon.stub(util, 'isCi').returns(false)
sinon.stub(util, 'isPostInstall').returns(false)
sinon.stub(util, 'pkgVersion').returns(packageVersion)
sinon.stub(download, 'start').resolves(packageVersion)
sinon.stub(unzip, 'start').resolves()
sinon.stub(BluebirdPromise, 'delay').resolves()
sinon.stub(fs, 'removeAsync').resolves()
sinon.stub(state, 'getVersionDir').returns('/cache/Cypress/1.2.3')
sinon.stub(state, 'getBinaryDir').returns('/cache/Cypress/1.2.3/Cypress.app')
sinon.stub(state, 'getBinaryPkgAsync').resolves()
sinon.stub(fs, 'ensureDirAsync').resolves(undefined)
;(os.platform as any).returns('darwin')
})
describe('skips install', function () {
it('when environment variable is set', function () {
process.env.CYPRESS_INSTALL_BINARY = '0'
return install.start()
.then(() => {
expect(download.start).not.to.be.called
snapshot(
'skip installation 1',
normalize((this as any).stdout.toString()),
)
})
})
})
describe('non-stable builds', () => {
const buildInfo = {
stable: false,
commitSha: '3b7f0b5c59def1e9b5f385bd585c9b2836706c29',
commitBranch: 'aBranchName',
commitDate: new Date('1996-11-27').toISOString(),
}
function runInstall () {
return install.start({ buildInfo })
}
it('install from a constructed CDN URL', async function () {
await runInstall()
expect(download.start).to.be.calledWithMatch({
version: 'https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-3b7f0b5c59def1e9b5f385bd585c9b2836706c29/cypress.zip',
})
})
it('logs a warning about installing a pre-release', async function () {
await runInstall()
snapshot(normalize((this as any).stdout.toString()))
})
it('installs to the expected pre-release cache dir', async function () {
(state.getVersionDir as any).restore()
await runInstall()
expect(unzip.start).to.be.calledWithMatch({ installDir: sinon.match(/\/Cypress\/beta\-1\.2\.3\-aBranchName\-3b7f0b5c$/) })
})
})
describe('override version', function () {
it('warns when specifying cypress version in env', function () {
const version = '0.12.1'
process.env.CYPRESS_INSTALL_BINARY = version
return install.start()
.then(() => {
expect(download.start).to.be.calledWithMatch({
version,
})
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: downloadDestination,
})
snapshot(
'specify version in env vars 1',
normalize((this as any).stdout.toString()),
)
})
})
it('trims environment variable before installing', function () {
// note how the version has extra spaces around it on purpose
const filename = '/tmp/local/file.zip'
const version = ` ${filename} `
process.env.CYPRESS_INSTALL_BINARY = version
// internally, the variable should be trimmed and just filename checked
sinon.stub(fs, 'pathExistsAsync').withArgs(filename).resolves(true)
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: filename,
installDir,
})
})
})
it('removes double quotes around the environment variable before installing', function () {
// note how the version has extra spaces around it on purpose
// and there are double quotes
const filename = '/tmp/local/file.zip'
const version = ` "${filename}" `
process.env.CYPRESS_INSTALL_BINARY = version
// internally, the variable should be trimmed, double quotes removed
// and just filename checked against the file system
sinon.stub(fs, 'pathExistsAsync').withArgs(filename).resolves(true)
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: filename,
installDir,
})
})
})
it('can install local binary zip file without download from absolute path', function () {
const version = '/tmp/local/file.zip'
process.env.CYPRESS_INSTALL_BINARY = version
sinon.stub(fs, 'pathExistsAsync').withArgs(version).resolves(true)
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: version,
installDir,
})
})
})
it('can install local binary zip file from relative path', function () {
const version = './cypress-resources/file.zip'
mockfs({
[version]: 'asdf',
})
process.env.CYPRESS_INSTALL_BINARY = version
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(download.start).not.to.be.called
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: path.resolve(version),
installDir,
})
})
})
describe('when version is already installed', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves({ version: packageVersion })
})
it('doesn\'t attempt to download', function () {
return install.start()
.then(() => {
expect(download.start).not.to.be.called
expect(state.getBinaryPkgAsync).to.be.calledWith('/cache/Cypress/1.2.3/Cypress.app')
})
})
it('logs \'skipping install\' when explicit cypress install', function () {
return install.start()
.then(() => {
return snapshot(
'version already installed - cypress install 1',
normalize((this as any).stdout.toString()),
)
})
})
it('logs when already installed when run from postInstall', function () {
(util.isPostInstall as any).returns(true)
return install.start()
.then(() => {
snapshot(
'version already installed - postInstall 1',
normalize((this as any).stdout.toString()),
)
})
})
})
describe('when getting installed version fails', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves(null)
return install.start()
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'continues installing on failure 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('when there is no install version', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves(null)
return install.start()
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
// cleans up the zip file
expect(fs.removeAsync).to.be.calledWith(
downloadDestination,
)
snapshot(
'installs without existing installation 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('when getting installed version does not match needed version', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves({ version: 'x.x.x' })
return install.start()
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'installed version does not match needed version 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('with force: true', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves({ version: packageVersion })
return install.start({ force: true })
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'forcing true always installs 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('as a global install', function () {
beforeEach(function () {
sinon.stub(util, 'isInstalledGlobally').returns(true)
;(state.getBinaryPkgAsync as any).resolves({ version: 'x.x.x' })
return install.start()
})
it('logs global warning and download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'warning installing as global 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('when running in CI', function () {
beforeEach(function () {
(util.isCi as any).returns(true)
;(state.getBinaryPkgAsync as any).resolves({ version: 'x.x.x' })
return install.start()
})
it('uses verbose renderer', function () {
snapshot(
'installing in ci 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('failed write access to cache directory', function () {
it('logs error on failure', function () {
(os.platform as any).returns('darwin')
sinon.stub(state, 'getCacheDir').returns('/invalid/cache/dir')
const err: any = new Error('EACCES: permission denied, mkdir \'/invalid\'')
err.code = 'EACCES'
;(fs.ensureDirAsync as any).rejects(err)
return install.start()
.then(() => {
throw new Error('should have caught error')
})
.catch((err: any) => {
logger.error(err)
snapshot(
'invalid cache directory 1',
normalize((this as any).stdout.toString()),
)
})
})
})
describe('CYPRESS_INSTALL_BINARY is URL or Zip', function () {
it('uses cache when correct version installed given URL', function () {
(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('1.2.3')
process.env.CYPRESS_INSTALL_BINARY = 'www.cypress.io/cannot-download/2.4.5'
return install.start()
.then(() => {
expect(download.start).to.not.be.called
})
})
it('uses cache when mismatch version given URL ', function () {
(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('4.0.0')
process.env.CYPRESS_INSTALL_BINARY = 'www.cypress.io/cannot-download/2.4.5'
return install.start()
.then(() => {
expect(download.start).to.not.be.called
})
})
it('uses cache when correct version installed given Zip', function () {
sinon.stub(fs, 'pathExistsAsync').withArgs('/path/to/zip.zip').resolves(true)
;(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('1.2.3')
process.env.CYPRESS_INSTALL_BINARY = '/path/to/zip.zip'
return install.start()
.then(() => {
expect(unzip.start).to.not.be.called
})
})
it('uses cache when mismatch version given Zip ', function () {
sinon.stub(fs, 'pathExistsAsync').withArgs('/path/to/zip.zip').resolves(true)
;(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('4.0.0')
process.env.CYPRESS_INSTALL_BINARY = '/path/to/zip.zip'
return install.start()
.then(() => {
expect(unzip.start).to.not.be.called
})
})
})
})
it('is silent when log level is silent', function () {
process.env.npm_config_loglevel = 'silent'
return install.start()
.then(() => {
return snapshot(
'silent install 1',
normalize(`[no output]${(this as any).stdout.toString()}`),
)
})
})
it('exits with error when installing on unsupported os', function () {
sinon.stub(util, 'getPlatformInfo').resolves('Platform: win32-ia32')
return install.start()
.then(() => {
throw new Error('should have caught error')
})
.catch((err: any) => {
logger.error(err)
snapshot(
'error when installing on unsupported os',
normalize((this as any).stdout.toString()),
)
})
})
})
context('._getBinaryUrlFromBuildInfo', function () {
const buildInfo = {
commitSha: 'abc123',
commitBranch: 'aBranchName',
}
it('generates the expected URL', () => {
(os.platform as any).returns('linux')
expect(install._getBinaryUrlFromBuildInfo('x64', buildInfo))
.to.eq(`https://cdn.cypress.io/beta/binary/0.0.0-development/linux-x64/aBranchName-abc123/cypress.zip`)
})
it('overrides win32-arm64 to win32-x64 for pre-release', () => {
(os.platform as any).returns('win32')
expect(install._getBinaryUrlFromBuildInfo('arm64', buildInfo))
.to.eq(`https://cdn.cypress.io/beta/binary/0.0.0-development/win32-x64/aBranchName-abc123/cypress.zip`)
})
})
})

View File

@@ -0,0 +1,478 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import os from 'os'
import path from 'path'
import createDebug from 'debug'
import fs from 'fs-extra'
import { cwd } from 'process'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import state from '../../../lib/tasks/state'
vi.mock('path', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
cwd: vi.fn(),
default: {
// @ts-expect-error
...actual.default,
resolve: vi.fn(),
},
}
})
vi.mock('process', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
cwd: vi.fn(),
default: {
// @ts-expect-error
...actual.default,
cwd: vi.fn(),
},
}
})
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
},
}
})
vi.mock('fs-extra', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pathExists: vi.fn(),
readJson: vi.fn(),
outputJson: vi.fn(),
realpath: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgVersion: vi.fn(),
getCacheDir: vi.fn(),
},
}
})
const debug = createDebug('test')
const cacheDir = path.join('.cache/Cypress')
const versionDir = path.join(cacheDir, '1.2.3')
const binaryDir = path.join(versionDir, 'Cypress.app')
const binaryPkgPath = path.join(
binaryDir,
'Contents',
'Resources',
'app',
'package.json',
)
describe('lib/tasks/state', function () {
beforeEach(async function () {
vi.resetAllMocks()
vi.unstubAllEnvs()
logger.reset()
// @ts-expect-error - mockReturnValue
util.getCacheDir.mockReturnValue(cacheDir)
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue('1.2.3')
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
const actualProcess = (await vi.importActual<typeof import('process')>('process')).default
// @ts-expect-error - mockImplementation
cwd.mockImplementation(() => {
return actualProcess.cwd()
})
const actualPath = (await vi.importActual<typeof import('path')>('path')).default
// @ts-expect-error - mockImplementation
path.resolve.mockImplementation((...args) => {
return actualPath.resolve.apply(actualPath, args)
})
})
describe('.getBinaryPkgVersion', function () {
it('returns version if present', () => {
expect(state.getBinaryPkgVersion({ version: '1.2.3' })).toEqual('1.2.3')
})
it('returns null if passed null', () => {
expect(state.getBinaryPkgVersion(null)).toEqual(null)
})
})
describe('.getBinaryPkgAsync', function () {
it('resolves with loaded file when the file exists', async function () {
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === binaryPkgPath) {
return true
}
})
// @ts-expect-error - mockImplementation
fs.readJson.mockImplementation((args) => {
if (args === binaryPkgPath) {
return { version: '2.0.48' }
}
})
const result = await state.getBinaryPkgAsync(binaryDir)
expect(result).toEqual({ version: '2.0.48' })
})
it('returns null if no version found', async function () {
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === binaryPkgPath) {
return false
}
})
const result = await state.getBinaryPkgAsync(binaryDir)
expect(result).toBeNull()
})
it('returns correct version if passed binaryDir', async function () {
const customBinaryDir = '/custom/binary/dir'
const customBinaryPackageDir =
'/custom/binary/dir/Contents/Resources/app/package.json'
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === customBinaryPackageDir) {
return true
}
})
// @ts-expect-error - mockImplementation
fs.readJson.mockImplementation((args) => {
if (args === customBinaryPackageDir) {
return { version: '3.4.5' }
}
})
const result = await state.getBinaryPkgAsync(customBinaryDir)
expect(result).toEqual({ version: '3.4.5' })
})
})
describe('.getPathToExecutable', function () {
it('resolves path on macOS', function () {
expect(state.getPathToExecutable(state.getBinaryDir())).toEqual(
'.cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress',
)
})
it('resolves path on linux', function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
expect(state.getPathToExecutable(state.getBinaryDir())).toEqual(
'.cache/Cypress/1.2.3/Cypress/Cypress',
)
})
it('resolves path on windows', function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
expect(state.getPathToExecutable(state.getBinaryDir())).toMatch(/\.exe$/)
})
it('resolves from custom binaryDir', function () {
expect(state.getPathToExecutable('home/downloads/cypress.app')).toEqual(
'home/downloads/cypress.app/Contents/MacOS/Cypress',
)
})
})
describe('.getBinaryDir', function () {
it('resolves path on macOS', function () {
expect(state.getBinaryDir()).toEqual(
path.join(versionDir, 'Cypress.app'),
)
})
it('resolves path on linux', function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
expect(state.getBinaryDir()).toEqual(path.join(versionDir, 'Cypress'))
})
it('resolves path on windows', async function () {
vi.doMock('path', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
default: actual.default.win32,
}
})
vi.resetModules()
const stateWithWin32Path = (await import('../../../lib/tasks/state')).default
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
const pathToExec = stateWithWin32Path.getBinaryDir()
expect(pathToExec).to.be.equal(path.win32.join(versionDir, 'Cypress'))
})
it('resolves path to binary/installation directory', function () {
expect(state.getBinaryDir()).toEqual(binaryDir)
})
it('resolves path to binary/installation from version', function () {
expect(state.getBinaryDir('4.5.6')).toEqual(
path.join(cacheDir, '4.5.6', 'Cypress.app'),
)
})
it('rejects on anything else', function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('unknown')
expect(() => {
return state.getBinaryDir()
}).toThrow('Platform: "unknown" is not supported.')
})
})
describe('.getBinaryVerifiedAsync', function () {
it('resolves true if verified', async function () {
// @ts-expect-error - mockResolvedValue
fs.readJson.mockResolvedValue({ verified: true })
const isVerified = await state.getBinaryVerifiedAsync('/asdf')
expect(isVerified).toEqual(true)
})
it('resolves undefined if not verified', async function () {
const err: any = new Error()
err.code = 'ENOENT'
// @ts-expect-error - mockRejectedValue
fs.readJson.mockRejectedValue(err)
const isVerified = await state.getBinaryVerifiedAsync('/asdf')
expect(isVerified).toEqual(undefined)
})
it('can accept custom binaryDir', async function () {
// note how the binary state file is in the runner's parent folder
const customBinaryDir = '/custom/binary/1.2.3/runner'
const binaryStatePath = '/custom/binary/1.2.3/binary_state.json'
// @ts-expect-error - mockImplementation
fs.pathExists.mockImplementation((args) => {
if (args === binaryStatePath) {
return true
}
})
// @ts-expect-error - mockImplementation
fs.readJson.mockImplementation((args) => {
if (args === binaryStatePath) {
return { verified: true }
}
})
const isVerified = await state.getBinaryVerifiedAsync(customBinaryDir)
expect(isVerified).toEqual(true)
})
})
describe('.writeBinaryVerified', function () {
const binaryStateFilename = path.join(versionDir, 'binary_state.json')
it('writes to binary state verified:true', async function () {
// @ts-expect-error - mockResolvedValue
fs.outputJson.mockResolvedValue()
await state.writeBinaryVerifiedAsync(true, binaryDir)
expect(fs.outputJson).toHaveBeenCalledWith(binaryStateFilename, { verified: true }, { spaces: 2 })
})
it('write to binary state verified:false', async function () {
// @ts-expect-error - mockResolvedValue
fs.outputJson.mockResolvedValue()
await state.writeBinaryVerifiedAsync(false, binaryDir)
expect(fs.outputJson).toHaveBeenCalledWith(
binaryStateFilename,
{ verified: false },
{ spaces: 2 },
)
})
})
describe('.getCacheDir', function () {
beforeEach(async function () {
vi.unstubAllEnvs()
})
it('uses cachedir()', function () {
const ret = state.getCacheDir()
expect(ret).toEqual(cacheDir)
})
it('uses env variable CYPRESS_CACHE_FOLDER', function () {
vi.stubEnv('CYPRESS_CACHE_FOLDER', '/path/to/dir')
const ret = state.getCacheDir()
expect(ret).toEqual('/path/to/dir')
})
it('CYPRESS_CACHE_FOLDER resolves from relative path', () => {
vi.stubEnv('CYPRESS_CACHE_FOLDER', './local-cache/folder')
const ret = state.getCacheDir()
expect(ret).toEqual(path.resolve('local-cache/folder'))
})
it('CYPRESS_CACHE_FOLDER resolves from relative path during postinstall', async () => {
vi.stubEnv('CYPRESS_CACHE_FOLDER', './local-cache/folder')
// simulates current folder when running "npm postinstall" hook
// @ts-expect-error - mockReturnValue
cwd.mockReturnValue('/my/project/folder/node_modules/cypress')
// @ts-expect-error - default import
const actualPath = (await vi.importActual<typeof import('path')>('path')).default
// @ts-expect-error - mockImplementation
path.resolve.mockImplementation((...args) => {
return actualPath.resolve('/my/project/folder/node_modules/cypress', args[0])
})
const ret = state.getCacheDir()
debug('returned cache dir %s', ret)
expect(ret).toEqual(actualPath.resolve('/my/project/folder/local-cache/folder'))
})
it('CYPRESS_CACHE_FOLDER resolves from absolute path during postinstall', () => {
vi.stubEnv('CYPRESS_CACHE_FOLDER', '/cache/folder/Cypress')
// simulates current folder when running "npm postinstall" hook
// @ts-expect-error - mockReturnValue
cwd.mockReturnValue('/my/project/folder/node_modules/cypress')
const ret = state.getCacheDir()
debug('returned cache dir %s', ret)
expect(ret).to.eql(path.resolve('/cache/folder/Cypress'))
})
it('resolves ~ with user home folder', () => {
const homeDir = os.homedir()
vi.stubEnv('CYPRESS_CACHE_FOLDER', '~/.cache/Cypress')
const ret = state.getCacheDir()
debug('cache dir is "%s"', ret)
expect(path.isAbsolute(ret), ret).toEqual(true)
expect(ret, '~ has been resolved').not.toContain('~')
expect(ret, 'replaced ~ with home directory').toEqual(`${homeDir}/.cache/Cypress`)
})
})
describe('.parseRealPlatformBinaryFolderAsync', function () {
beforeEach(function () {
// @ts-expect-error - mockImplementation
fs.realpath.mockImplementation((path) => Promise.resolve(path))
})
it('can parse on darwin', async function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
const path = await state.parseRealPlatformBinaryFolderAsync(
'/Documents/Cypress.app/Contents/MacOS/Cypress',
)
expect(path).toEqual('/Documents/Cypress.app')
})
it('can parse on linux', async function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
const path = await state.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress')
expect(path).toEqual('/Documents/Cypress')
})
it('can parse on darwin', async function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
const path = await state.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress.exe')
expect(path).toEqual('/Documents/Cypress')
})
it('throws when invalid on darwin', async function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
const path = await state.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress.exe')
expect(path).toEqual(false)
})
it('throws when invalid on linux', async function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
const path = await state.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress.exe')
expect(path).toEqual(false)
})
it('throws when invalid on windows', async function () {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
const path = await state.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress')
expect(path).toEqual(false)
})
})
})

View File

@@ -1,382 +0,0 @@
import '../../spec_helper'
import os from 'os'
import path from 'path'
import BluebirdPromise from 'bluebird'
import mockfs from 'mock-fs'
import { expect } from 'chai'
import createDebug from 'debug'
import fs from '../../../lib/fs'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import state from '../../../lib/tasks/state'
const debug = createDebug('test')
const cacheDir = path.join('.cache/Cypress')
const versionDir = path.join(cacheDir, '1.2.3')
const binaryDir = path.join(versionDir, 'Cypress.app')
const binaryPkgPath = path.join(
binaryDir,
'Contents',
'Resources',
'app',
'package.json',
)
describe('lib/tasks/state', function () {
beforeEach(function () {
sinon.stub(util, 'getCacheDir').returns(cacheDir)
logger.reset()
sinon.stub(process, 'exit')
sinon.stub(util, 'pkgVersion').returns('1.2.3')
;(os.platform as any).returns('darwin')
})
context('.getBinaryPkgVersion', function () {
it('returns version if present', () => {
expect(state.getBinaryPkgVersion({ version: '1.2.3' })).to.equal('1.2.3')
})
it('returns null if passed null', () => {
expect(state.getBinaryPkgVersion(null)).to.equal(null)
})
})
context('.getBinaryPkgAsync', function () {
it('resolves with loaded file when the file exists', function () {
sinon
.stub(fs, 'pathExistsAsync')
.withArgs(binaryPkgPath)
.resolves(true)
sinon
.stub(fs, 'readJsonAsync')
.withArgs(binaryPkgPath)
.resolves({ version: '2.0.48' })
return state.getBinaryPkgAsync(binaryDir).then((result: any) => {
expect(result).to.deep.equal({ version: '2.0.48' })
})
})
it('returns null if no version found', function () {
sinon.stub(fs, 'pathExistsAsync').resolves(false)
return state
.getBinaryPkgAsync(binaryDir)
.then((result: any) => {
return expect(result).to.equal(null)
})
})
it('returns correct version if passed binaryDir', function () {
const customBinaryDir = '/custom/binary/dir'
const customBinaryPackageDir =
'/custom/binary/dir/Contents/Resources/app/package.json'
sinon
.stub(fs, 'pathExistsAsync')
.withArgs(customBinaryPackageDir)
.resolves(true)
sinon
.stub(fs, 'readJsonAsync')
.withArgs(customBinaryPackageDir)
.resolves({ version: '3.4.5' })
return state
.getBinaryPkgAsync(customBinaryDir)
.then((result: any) => {
return expect(result).to.deep.equal({ version: '3.4.5' })
})
})
})
context('.getPathToExecutable', function () {
it('resolves path on macOS', function () {
const macExecutable =
'.cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress'
expect(state.getPathToExecutable(state.getBinaryDir())).to.equal(
macExecutable,
)
})
it('resolves path on linux', function () {
(os.platform as any).returns('linux')
const linuxExecutable = '.cache/Cypress/1.2.3/Cypress/Cypress'
expect(state.getPathToExecutable(state.getBinaryDir())).to.equal(
linuxExecutable,
)
})
it('resolves path on windows', function () {
(os.platform as any).returns('win32')
expect(state.getPathToExecutable(state.getBinaryDir())).to.match(/\.exe$/)
})
it('resolves from custom binaryDir', function () {
const customBinaryDir = 'home/downloads/cypress.app'
expect(state.getPathToExecutable(customBinaryDir)).to.equal(
'home/downloads/cypress.app/Contents/MacOS/Cypress',
)
})
})
context('.getBinaryDir', function () {
it('resolves path on macOS', function () {
expect(state.getBinaryDir()).to.equal(
path.join(versionDir, 'Cypress.app'),
)
})
it('resolves path on linux', function () {
(os.platform as any).returns('linux')
expect(state.getBinaryDir()).to.equal(path.join(versionDir, 'Cypress'))
})
it('resolves path on windows', async function () {
const proxyquire = await import('proxyquire')
const stateWithWin32Path = proxyquire.default(`../../../lib/tasks/state`, { path: path.win32 }).default
;(os.platform as any).returns('win32')
const pathToExec = stateWithWin32Path.getBinaryDir()
expect(pathToExec).to.be.equal(path.win32.join(versionDir, 'Cypress'))
})
it('resolves path to binary/installation directory', function () {
expect(state.getBinaryDir()).to.equal(binaryDir)
})
it('resolves path to binary/installation from version', function () {
expect(state.getBinaryDir('4.5.6')).to.be.equal(
path.join(cacheDir, '4.5.6', 'Cypress.app'),
)
})
it('rejects on anything else', function () {
(os.platform as any).returns('unknown')
expect(() => {
return state.getBinaryDir().to.throw('Platform: "unknown" is not supported.')
})
})
})
context('.getBinaryVerifiedAsync', function () {
it('resolves true if verified', function () {
sinon.stub(fs, 'readJsonAsync').resolves({ verified: true })
return state
.getBinaryVerifiedAsync('/asdf')
.then((isVerified: any) => {
return expect(isVerified).to.be.equal(true)
})
})
it('resolves undefined if not verified', function () {
const err: any = new Error()
err.code = 'ENOENT'
sinon.stub(fs, 'readJsonAsync').rejects(err)
return state
.getBinaryVerifiedAsync('/asdf')
.then((isVerified: any) => {
return expect(isVerified).to.be.equal(undefined)
})
})
it('can accept custom binaryDir', function () {
// note how the binary state file is in the runner's parent folder
const customBinaryDir = '/custom/binary/1.2.3/runner'
const binaryStatePath = '/custom/binary/1.2.3/binary_state.json'
sinon
.stub(fs, 'pathExistsAsync')
.withArgs(binaryStatePath)
.resolves(true)
sinon
.stub(fs, 'readJsonAsync')
.withArgs(binaryStatePath)
.resolves({ verified: true })
return state
.getBinaryVerifiedAsync(customBinaryDir)
.then((isVerified: any) => {
return expect(isVerified).to.be.equal(true)
})
})
})
context('.writeBinaryVerified', function () {
const binaryStateFilename = path.join(versionDir, 'binary_state.json')
beforeEach(() => {
mockfs({})
})
afterEach(() => {
mockfs.restore()
})
it('writes to binary state verified:true', function () {
sinon.stub(fs, 'outputJsonAsync').resolves()
return state
.writeBinaryVerifiedAsync(true, binaryDir)
.then(
() => {
return expect(fs.outputJsonAsync).to.be.calledWith(
binaryStateFilename,
{ verified: true },
)
},
{ spaces: 2 },
)
})
it('write to binary state verified:false', function () {
sinon.stub(fs, 'outputJsonAsync').resolves()
return state
.writeBinaryVerifiedAsync(false, binaryDir)
.then(() => {
return expect(fs.outputJsonAsync).to.be.calledWith(
binaryStateFilename,
{ verified: false },
{ spaces: 2 },
)
})
})
})
context('.getCacheDir', function () {
it('uses cachedir()', function () {
const ret = state.getCacheDir()
expect(ret).to.equal(cacheDir)
})
it('uses env variable CYPRESS_CACHE_FOLDER', function () {
process.env.CYPRESS_CACHE_FOLDER = '/path/to/dir'
const ret = state.getCacheDir()
expect(ret).to.equal('/path/to/dir')
})
it('CYPRESS_CACHE_FOLDER resolves from relative path', () => {
process.env.CYPRESS_CACHE_FOLDER = './local-cache/folder'
const ret = state.getCacheDir()
expect(ret).to.eql(path.resolve('local-cache/folder'))
})
it('CYPRESS_CACHE_FOLDER resolves from relative path during postinstall', () => {
process.env.CYPRESS_CACHE_FOLDER = './local-cache/folder'
// simulates current folder when running "npm postinstall" hook
sinon.stub(process, 'cwd').returns('/my/project/folder/node_modules/cypress')
const ret = state.getCacheDir()
debug('returned cache dir %s', ret)
expect(ret).to.eql(path.resolve('/my/project/folder/local-cache/folder'))
})
it('CYPRESS_CACHE_FOLDER resolves from absolute path during postinstall', () => {
process.env.CYPRESS_CACHE_FOLDER = '/cache/folder/Cypress'
// simulates current folder when running "npm postinstall" hook
sinon.stub(process, 'cwd').returns('/my/project/folder/node_modules/cypress')
const ret = state.getCacheDir()
debug('returned cache dir %s', ret)
expect(ret).to.eql(path.resolve('/cache/folder/Cypress'))
})
it('resolves ~ with user home folder', () => {
const homeDir = os.homedir()
process.env.CYPRESS_CACHE_FOLDER = '~/.cache/Cypress'
const ret = state.getCacheDir()
debug('cache dir is "%s"', ret)
expect(path.isAbsolute(ret), ret).to.be.true
expect(ret, '~ has been resolved').to.not.contain('~')
expect(ret, 'replaced ~ with home directory').to.equal(`${homeDir}/.cache/Cypress`)
})
})
context('.parseRealPlatformBinaryFolderAsync', function () {
beforeEach(function () {
sinon.stub(fs, 'realpathAsync').callsFake((path: any) => {
return BluebirdPromise.resolve(path)
})
})
it('can parse on darwin', function () {
(os.platform as any).returns('darwin')
return state
.parseRealPlatformBinaryFolderAsync(
'/Documents/Cypress.app/Contents/MacOS/Cypress',
)
.then((path: any) => {
return expect(path).to.eql('/Documents/Cypress.app')
})
})
it('can parse on linux', function () {
(os.platform as any).returns('linux')
return state
.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress')
.then((path: any) => {
return expect(path).to.eql('/Documents/Cypress')
})
})
it('can parse on darwin', function () {
(os.platform as any).returns('win32')
return state
.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress.exe')
.then((path: any) => {
return expect(path).to.eql('/Documents/Cypress')
})
})
it('throws when invalid on darwin', function () {
(os.platform as any).returns('darwin')
return state
.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress.exe')
.then((path: any) => {
return expect(path).to.eql(false)
})
})
it('throws when invalid on linux', function () {
(os.platform as any).returns('linux')
return state
.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress.exe')
.then((path: any) => {
return expect(path).to.eql(false)
})
})
it('throws when invalid on windows', function () {
(os.platform as any).returns('win32')
return state
.parseRealPlatformBinaryFolderAsync('/Documents/Cypress/Cypress')
.then((path: any) => {
return expect(path).to.eql(false)
})
})
})
})

View File

@@ -0,0 +1,393 @@
import { vi, describe, it, beforeEach, afterEach, expect } from 'vitest'
import events from 'events'
import os from 'os'
import path from 'path'
import extract from 'extract-zip'
import cp from 'child_process'
import createDebug from 'debug'
import readline from 'readline'
import fs from 'fs-extra'
import si from 'systeminformation'
import { Console } from 'console'
import normalize from '../../support/normalize'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import unzip from '../../../lib/tasks/unzip'
const debug = createDebug('test')
const version = '1.2.3'
const installDir = path.join(os.tmpdir(), 'Cypress', version)
vi.mock('extract-zip')
vi.mock('child_process')
vi.mock('readline')
vi.mock('fs-extra')
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
arch: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
pkgVersion: vi.fn(),
},
}
})
describe('lib/tasks/unzip', function () {
const createStdoutCapture = () => {
const logs: string[] = []
// eslint-disable-next-line no-console
const originalOut = process.stdout.write
vi.spyOn(process.stdout, 'write').mockImplementation((strOrBugger: string | Uint8Array<ArrayBufferLike>) => {
logs.push(strOrBugger as string)
return originalOut(strOrBugger)
})
return () => logs.join('')
}
let originalConsole: Console
beforeEach(async function () {
vi.resetAllMocks()
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
os.arch.mockReturnValue('x64')
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue(version)
// @ts-expect-error mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
// @ts-expect-error - default import
const actualExtract = (await vi.importActual<typeof import('extract-zip')>('extract-zip')).default
// @ts-expect-error - mockImplementation
extract.mockImplementation(actualExtract)
// @ts-expect-error - default import
const actualChildProcess = (await vi.importActual<typeof import('child_process')>('child_process')).default
// @ts-expect-error - mockImplementation
cp.spawn.mockImplementation(actualChildProcess.spawn)
// @ts-expect-error - mockImplementation
readline.createInterface.mockImplementation(() => {
return {
on: vi.fn(),
}
})
originalConsole = globalThis.console
// Redirect console output to a custom stream or mock
globalThis.console = new Console(process.stdout, process.stderr)
})
afterEach(() => {
globalThis.console = originalConsole // Restore original console
})
it('throws when cannot unzip', async function () {
const stdout = createStdoutCapture()
try {
await unzip.start({
zipFilePath: path.join('test', 'fixture', 'bad_example.zip'),
installDir,
})
} catch (err) {
logger.error(err)
return expect(normalize(stdout())).toMatchSnapshot()
}
throw new Error('should have failed')
})
it('throws max path length error when cannot unzip due to realpath ENOENT on windows', async function () {
const stdout = createStdoutCapture()
const err: any = new Error('failed')
err.code = 'ENOENT'
err.syscall = 'realpath'
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
// @ts-expect-error - mockRejectedValue
fs.ensureDir.mockRejectedValue(err)
try {
await unzip.start({
zipFilePath: path.join('test', 'fixture', 'bad_example.zip'),
installDir,
})
} catch (err) {
logger.error(err)
return expect(normalize(stdout())).toMatchSnapshot()
}
throw new Error('should have failed')
})
it('can really unzip', async function () {
const onProgress = vi.fn().mockReturnValue(undefined)
await unzip.start({
zipFilePath: path.join('test', 'fixture', 'example.zip'),
installDir,
progress: { onProgress },
})
expect(onProgress).toHaveBeenCalled()
await fs.stat(installDir)
})
describe('on linux', () => {
beforeEach(() => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
})
it('can try unzip first then fall back to node unzip', async function () {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
// @ts-expect-error - mockImplementation
extract.mockImplementation((filePath: any, opts: any) => {
debug('unzip extract called with %s', filePath)
expect(filePath, 'zipfile is the same').toEqual(zipFilePath)
return Promise.resolve(undefined)
})
const unzipChildProcess = new events.EventEmitter()
// @ts-expect-error - mocking process
unzipChildProcess.stdout = {
on: vi.fn(),
}
// @ts-expect-error - mocking process
unzipChildProcess.stderr = {
on: vi.fn(),
}
// @ts-expect-error - mockImplementation
cp.spawn.mockImplementation((args: string) => {
if (args === 'unzip') {
return unzipChildProcess
}
})
setTimeout(() => {
debug('emitting unzip error')
unzipChildProcess.emit('error', new Error('unzip fails badly'))
}, 100)
await unzip.start({
zipFilePath,
installDir,
})
debug('checking if unzip was called')
expect(cp.spawn).toHaveBeenCalledExactlyOnceWith('unzip', ['-o', zipFilePath, '-d', installDir])
expect(extract).toHaveBeenCalledExactlyOnceWith(zipFilePath, expect.objectContaining({
dir: installDir,
onEntry: expect.any(Function),
}))
})
it('can try unzip first then fall back to node unzip and fails with an empty error', async function () {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
// @ts-expect-error - mockRejectedValue
extract.mockRejectedValue(undefined)
const unzipChildProcess = new events.EventEmitter()
// @ts-expect-error - mocking process
unzipChildProcess.stdout = {
on: vi.fn(),
}
// @ts-expect-error - mocking process
unzipChildProcess.stderr = {
on: vi.fn(),
}
// @ts-expect-error - mockImplementation
cp.spawn.mockImplementation((args: string) => {
if (args === 'unzip') {
return unzipChildProcess
}
})
setTimeout(() => {
debug('emitting unzip error')
unzipChildProcess.emit('error', new Error('unzip fails badly'))
}, 100)
try {
await unzip.start({
zipFilePath,
installDir,
})
} catch (err: any) {
logger.error(err)
expect(err.message).toMatch('Unknown error with Node extract tool')
return
}
throw new Error('should have failed')
})
it('calls node unzip just once', async function () {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
// @ts-expect-error - mockImplementation
extract.mockImplementation((filePath: any, opts: any) => {
debug('unzip extract called with %s', filePath)
expect(filePath, 'zipfile is the same').toEqual(zipFilePath)
return Promise.resolve(undefined)
})
const unzipChildProcess = new events.EventEmitter()
// @ts-expect-error - mocking process
unzipChildProcess.stdout = {
on: vi.fn(),
}
// @ts-expect-error - mocking process
unzipChildProcess.stderr = {
on: vi.fn(),
}
// @ts-expect-error - mockImplementation
cp.spawn.mockImplementation((args: string) => {
if (args === 'unzip') {
return unzipChildProcess
}
})
setTimeout(() => {
debug('emitting unzip error')
unzipChildProcess.emit('error', new Error('unzip fails badly'))
}, 100)
setTimeout(() => {
debug('emitting unzip close')
unzipChildProcess.emit('close', 1)
}, 110)
await unzip
.start({
zipFilePath,
installDir,
})
debug('checking if unzip was called')
expect(cp.spawn).toHaveBeenCalledExactlyOnceWith('unzip', ['-o', zipFilePath, '-d', installDir])
expect(extract).toHaveBeenCalledExactlyOnceWith(zipFilePath, expect.objectContaining({
dir: installDir,
onEntry: expect.any(Function),
}))
})
})
describe('on Mac', () => {
beforeEach(() => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
})
it('calls node unzip just once', async function () {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
// @ts-expect-error - mockImplementation
extract.mockImplementation((filePath: any, opts: any) => {
debug('unzip extract called with %s', filePath)
expect(filePath, 'zipfile is the same').toEqual(zipFilePath)
return Promise.resolve(undefined)
})
const unzipChildProcess = new events.EventEmitter()
// @ts-expect-error - mocking process
unzipChildProcess.stdout = {
on: vi.fn(),
}
// @ts-expect-error - mocking process
unzipChildProcess.stderr = {
on: vi.fn(),
}
// @ts-expect-error - mockImplementation
cp.spawn.mockImplementation((args: string) => {
if (args === 'ditto') {
return unzipChildProcess
}
})
setTimeout(() => {
debug('emitting ditto error')
unzipChildProcess.emit('error', new Error('ditto fails badly'))
}, 100)
setTimeout(() => {
debug('emitting ditto close')
unzipChildProcess.emit('close', 1)
}, 110)
await unzip.start({
zipFilePath,
installDir,
})
debug('checking if unzip was called')
expect(cp.spawn).toHaveBeenCalledExactlyOnceWith('ditto', ['-xkV', zipFilePath, installDir])
expect(extract).toHaveBeenCalledExactlyOnceWith(zipFilePath, expect.objectContaining({
dir: installDir,
onEntry: expect.any(Function),
}))
})
})
})

View File

@@ -1,283 +0,0 @@
import '../../spec_helper'
import events from 'events'
import os from 'os'
import path from 'path'
import snapshot from '../../support/snapshot'
import cp from 'child_process'
import createDebug from 'debug'
import readline from 'readline'
import stdout from '../../support/stdout'
import normalize from '../../support/normalize'
import fs from '../../../lib/fs'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import unzip from '../../../lib/tasks/unzip'
const debug = createDebug('test')
const version = '1.2.3'
const installDir = path.join(os.tmpdir(), 'Cypress', version)
describe('lib/tasks/unzip', function () {
before(async function () {
const mochaMain = await import('mocha-banner')
mochaMain.register()
})
beforeEach(function () {
(this as any).stdout = stdout.capture()
;(os.platform as any).returns('darwin')
sinon.stub(util, 'pkgVersion').returns(version)
})
afterEach(function () {
stdout.restore()
})
it('throws when cannot unzip', async function () {
try {
await unzip.start({
zipFilePath: path.join('test', 'fixture', 'bad_example.zip'),
installDir,
})
} catch (err) {
logger.error(err)
return snapshot(normalize((this as any).stdout.toString()))
}
throw new Error('should have failed')
})
it('throws max path length error when cannot unzip due to realpath ENOENT on windows', async function () {
const err: any = new Error('failed')
err.code = 'ENOENT'
err.syscall = 'realpath'
;(os.platform as any).returns('win32')
sinon.stub(fs, 'ensureDirAsync').rejects(err)
try {
await unzip.start({
zipFilePath: path.join('test', 'fixture', 'bad_example.zip'),
installDir,
})
} catch (err) {
logger.error(err)
return snapshot(normalize((this as any).stdout.toString()))
}
throw new Error('should have failed')
})
it('can really unzip', function () {
const onProgress = sinon.stub().returns(undefined)
return unzip
.start({
zipFilePath: path.join('test', 'fixture', 'example.zip'),
installDir,
progress: { onProgress },
})
.then(() => {
expect(onProgress).to.be.called
return fs.statAsync(installDir)
})
})
context('on linux', () => {
beforeEach(() => {
(os.platform as any).returns('linux')
})
it('can try unzip first then fall back to node unzip', function (done) {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
sinon.stub(unzip.utils.unzipTools, 'extract').callsFake((filePath: any, opts: any) => {
debug('unzip extract called with %s', filePath)
expect(filePath, 'zipfile is the same').to.equal(zipFilePath)
return new Promise((resolve, reject) => resolve(undefined))
})
const unzipChildProcess = new events.EventEmitter()
;(unzipChildProcess as any).stdout = {
on () {},
}
;(unzipChildProcess as any).stderr = {
on () {},
}
// @ts-expect-error - invalid number of arguments for given type
sinon.stub(cp, 'spawn').withArgs('unzip').returns(unzipChildProcess as any)
setTimeout(() => {
debug('emitting unzip error')
unzipChildProcess.emit('error', new Error('unzip fails badly'))
}, 100)
unzip
.start({
zipFilePath,
installDir,
})
.then(() => {
debug('checking if unzip was called')
expect(cp.spawn, 'unzip spawn').to.have.been.calledWith('unzip')
expect(unzip.utils.unzipTools.extract, 'extract called').to.be.calledWith(zipFilePath)
expect(unzip.utils.unzipTools.extract, 'extract called once').to.be.calledOnce
done()
})
})
it('can try unzip first then fall back to node unzip and fails with an empty error', async function () {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
sinon.stub(unzip.utils.unzipTools, 'extract').callsFake(() => {
return new Promise((_, reject) => reject())
})
const unzipChildProcess = new events.EventEmitter()
;(unzipChildProcess as any).stdout = {
on () {},
}
;(unzipChildProcess as any).stderr = {
on () {},
}
// @ts-expect-error - invalid number of arguments for given type
sinon.stub(cp, 'spawn').withArgs('unzip').returns(unzipChildProcess as any)
setTimeout(() => {
debug('emitting unzip error')
unzipChildProcess.emit('error', new Error('unzip fails badly'))
}, 100)
try {
await unzip
.start({
zipFilePath,
installDir,
})
} catch (err: any) {
logger.error(err)
expect(err.message).to.include('Unknown error with Node extract tool')
return
}
throw new Error('should have failed')
})
it('calls node unzip just once', function (done) {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
sinon.stub(unzip.utils.unzipTools, 'extract').callsFake((filePath: any, opts: any) => {
debug('unzip extract called with %s', filePath)
expect(filePath, 'zipfile is the same').to.equal(zipFilePath)
return new Promise((resolve, reject) => resolve(undefined))
})
const unzipChildProcess = new events.EventEmitter()
;(unzipChildProcess as any).stdout = {
on () {},
}
;(unzipChildProcess as any).stderr = {
on () {},
}
// @ts-expect-error - invalid number of arguments for given type
sinon.stub(cp, 'spawn').withArgs('unzip').returns(unzipChildProcess as any)
setTimeout(() => {
debug('emitting unzip error')
unzipChildProcess.emit('error', new Error('unzip fails badly'))
}, 100)
setTimeout(() => {
debug('emitting unzip close')
unzipChildProcess.emit('close', 1)
}, 110)
unzip
.start({
zipFilePath,
installDir,
})
.then(() => {
debug('checking if unzip was called')
expect(cp.spawn, 'unzip spawn').to.have.been.calledWith('unzip')
expect(unzip.utils.unzipTools.extract, 'extract called').to.be.calledWith(zipFilePath)
expect(unzip.utils.unzipTools.extract, 'extract called once').to.be.calledOnce
done()
})
})
})
context('on Mac', () => {
beforeEach(() => {
(os.platform as any).returns('darwin')
})
it('calls node unzip just once', function (done) {
const zipFilePath = path.join('test', 'fixture', 'example.zip')
sinon.stub(unzip.utils.unzipTools, 'extract').callsFake((filePath: any, opts: any) => {
debug('unzip extract called with %s', filePath)
expect(filePath, 'zipfile is the same').to.equal(zipFilePath)
return new Promise((resolve) => resolve(undefined))
})
const unzipChildProcess = new events.EventEmitter()
;(unzipChildProcess as any).stdout = {
on () {},
}
;(unzipChildProcess as any).stderr = {
on () {},
}
// @ts-expect-error - invalid number of arguments for given type
sinon.stub(cp, 'spawn').withArgs('ditto').returns(unzipChildProcess as any)
sinon.stub(readline, 'createInterface').returns({
on: () => {},
} as any)
setTimeout(() => {
debug('emitting ditto error')
unzipChildProcess.emit('error', new Error('ditto fails badly'))
}, 100)
setTimeout(() => {
debug('emitting ditto close')
unzipChildProcess.emit('close', 1)
}, 110)
unzip
.start({
zipFilePath,
installDir,
})
.then(() => {
debug('checking if unzip was called')
expect(cp.spawn, 'unzip spawn').to.have.been.calledWith('ditto')
expect(unzip.utils.unzipTools.extract, 'extract called').to.be.calledWith(zipFilePath)
expect(unzip.utils.unzipTools.extract, 'extract called once').to.be.calledOnce
done()
})
})
})
})

View File

@@ -0,0 +1,981 @@
import { vi, describe, it, beforeEach, afterEach, expect, MockInstance } from 'vitest'
import path from 'path'
import _ from 'lodash'
import os from 'os'
import { stripIndent } from 'common-tags'
import mockfs from 'mock-fs'
import normalize from '../../support/normalize'
import { geteuid } from 'process'
import { Console } from 'console'
import fs from 'fs-extra'
import si from 'systeminformation'
import _xvfb from '@cypress/xvfb'
import util from '../../../lib/util'
import logger from '../../../lib/logger'
import xvfb from '../../../lib/exec/xvfb'
import { verifyTestRunnerTimeoutMs, start, needsSandbox } from '../../../lib/tasks/verify'
const packageVersion = '1.2.3'
const cacheDir = '/cache/Cypress'
const executablePath = '/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress'
const binaryStatePath = '/cache/Cypress/1.2.3/binary_state.json'
const DEFAULT_VERIFY_TIMEOUT = 30000
vi.mock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
osInfo: vi.fn(),
},
}
})
vi.mock('@cypress/xvfb', async () => {
const XVFB_MOCK = vi.fn()
XVFB_MOCK.prototype.start = vi.fn()
return {
default: XVFB_MOCK,
}
})
vi.mock('lodash', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
random: vi.fn(),
},
}
})
vi.mock('process', async (importActual) => {
const actual = await importActual()
return {
geteuid: vi.fn(),
default: {
// @ts-expect-error
...actual.default,
geteuid: vi.fn(),
},
}
})
vi.mock('os', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
platform: vi.fn(),
release: vi.fn(),
arch: vi.fn(),
},
}
})
vi.mock('../../../lib/exec/xvfb', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
start: vi.fn(),
stop: vi.fn(),
isNeeded: vi.fn(),
startAsync: vi.fn(),
},
}
})
vi.mock('../../../lib/util', async (importActual) => {
const actual = await importActual()
return {
default: {
// @ts-expect-error
...actual.default,
getCacheDir: vi.fn(),
isCi: vi.fn(),
pkgVersion: vi.fn(),
exec: vi.fn(),
getOsVersionAsync: vi.fn(),
isPossibleLinuxWithIncorrectDisplay: vi.fn(),
},
}
})
describe('lib/tasks/verify', () => {
const createStdoutCapture = () => {
const logs: string[] = []
// eslint-disable-next-line no-console
const originalOut = process.stdout.write
vi.spyOn(process.stdout, 'write').mockImplementation((strOrBugger: string | Uint8Array<ArrayBufferLike>) => {
logs.push(strOrBugger as string)
return originalOut(strOrBugger)
})
return () => logs.join('')
}
let spawnedProcess: any
// Direct console to process.stdout/stderr
let originalConsole: Console
beforeEach(() => {
vi.resetAllMocks()
vi.unstubAllEnvs()
vi.stubEnv('npm_config_loglevel', 'notice')
originalConsole = globalThis.console
globalThis.console = new Console(process.stdout, process.stderr)
spawnedProcess = {
code: 0,
stderr: vi.fn(),
stdout: '222',
}
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
os.release.mockReturnValue('0.0.0')
// @ts-expect-error - mockReturnValue
os.arch.mockReturnValue('x64')
// @ts-expect-error mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Foo',
release: 'OsVersion',
})
// @ts-expect-error - mockReturnValue
util.getCacheDir.mockReturnValue(cacheDir)
// @ts-expect-error - mockReturnValue
util.isCi.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
util.pkgVersion.mockReturnValue(packageVersion)
// @ts-expect-error - mockResolvedValue
xvfb.start.mockResolvedValue()
// @ts-expect-error - mockResolvedValue
xvfb.stop.mockResolvedValue()
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
geteuid.mockReturnValue(1000)
// @ts-expect-error - mockReturnValue
_.random.mockReturnValue(222)
// @ts-expect-error - mockImplementation
util.exec.mockImplementation((...args: any) => {
if (args[0] === executablePath && _.isEqual(args[1], ['--no-sandbox', '--smoke-test', '--ping=222'])) {
return Promise.resolve(spawnedProcess)
}
return Promise.reject(new Error('should have caught error'))
})
})
afterEach(() => {
globalThis.console = originalConsole // Restore original console
mockfs.restore()
})
it('has verify task timeout', () => {
expect(verifyTestRunnerTimeoutMs()).to.eql(DEFAULT_VERIFY_TIMEOUT)
})
it('accepts custom verify task timeout', () => {
vi.stubEnv('CYPRESS_VERIFY_TIMEOUT', '500000')
expect(verifyTestRunnerTimeoutMs()).toEqual(500000)
})
it('accepts custom verify task timeout from npm', async () => {
vi.stubEnv('npm_config_CYPRESS_VERIFY_TIMEOUT', '600000')
expect(verifyTestRunnerTimeoutMs()).toEqual(600000)
})
it('falls back to default verify task timeout if custom value is invalid', async () => {
vi.stubEnv('CYPRESS_VERIFY_TIMEOUT', 'foobar')
expect(verifyTestRunnerTimeoutMs()).toEqual(DEFAULT_VERIFY_TIMEOUT)
})
it('returns early when `CYPRESS_SKIP_VERIFY` is set to true', async () => {
vi.stubEnv('CYPRESS_SKIP_VERIFY', 'true')
const result = await start({ listrRenderer: 'silent' })
expect(result).toEqual(undefined)
})
it('logs error and exits when no version of Cypress is installed', async () => {
const output = createStdoutCapture()
try {
await start({ listrRenderer: 'silent' })
throw new Error('should have caught error')
} catch (err) {
expect(err.message).not.toContain('should have caught error')
logger.error(err)
expect(normalize(output())).toMatchSnapshot()
}
})
it('adds --no-sandbox when user is root', async () => {
// make it think the executable exists
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockReturnValue
geteuid.mockReturnValue(0) // user is root
await start({ listrRenderer: 'silent' })
expect(util.exec).toHaveBeenCalledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'], expect.anything())
})
it('adds --no-sandbox when user is non-root', async () => {
// make it think the executable exists
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockReturnValue
geteuid.mockReturnValue(1000) // user is non-root
await start({ listrRenderer: 'silent' })
expect(util.exec).toHaveBeenCalledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'], expect.anything())
})
it('is noop when binary is already verified', async () => {
const output = createStdoutCapture()
// make it think the executable exists and is verified
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
await start({ listrRenderer: 'silent' })
expect(output()).toEqual('')
expect(util.exec).not.toHaveBeenCalled()
})
it('logs warning when installed version does not match verified version', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion: 'bloop',
})
await start({ listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot()
})
it('logs error and exits when executable cannot be found', async () => {
const output = createStdoutCapture()
try {
await start({ listrRenderer: 'silent' })
throw new Error('should have caught error')
} catch (err) {
expect(err.message).not.toContain('should have caught error')
logger.error(err)
expect(normalize(output())).toMatchSnapshot()
}
})
it('logs error when child process hangs', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockRejectedValue
util.exec.mockRejectedValue({
stderr: 'some stderr',
stdout: 'some stdout',
timedOut: true,
})
try {
await start({ smokeTestTimeout: 1, listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot()
}
})
it('logs error when child process returns incorrect stdout (stderr when exists)', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockRejectedValue
util.exec.mockRejectedValue({
stderr: 'some stderr',
stdout: 'some stdout',
code: 0,
})
try {
await start({ smokeTestTimeout: 1, listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot()
}
})
it('logs error when child process returns incorrect stdout (stdout when no stderr)', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockRejectedValue
util.exec.mockRejectedValue({
stdout: 'some stdout',
code: 0,
})
try {
await start({ smokeTestTimeout: 1, listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot()
}
})
describe('FORCE_COLOR', () => {
beforeEach(() => {
// vi.unstubAllEnvs()
vi.stubEnv('FORCE_COLOR', 'true')
})
// @see https://github.com/cypress-io/cypress/issues/28982
it('sets FORCE_COLOR to 0 when piping stdioOptions to to the smoke test to avoid ANSI in binary smoke test', async () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockResolvedValue
util.exec.mockResolvedValue({
stdout: '222',
stderr: '',
})
await start({ listrRenderer: 'silent' })
expect(util.exec).toHaveBeenCalledWith(
executablePath,
['--no-sandbox', '--smoke-test', '--ping=222'],
expect.objectContaining({ env: expect.objectContaining({ FORCE_COLOR: '0' }) }),
)
})
})
describe('with force: true', () => {
beforeEach(() => {
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
})
it('shows full path to executable when verifying', async () => {
const output = createStdoutCapture()
await start({ force: true, listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot('verification with executable')
})
it('clears verified version from state if verification fails', async () => {
const output = createStdoutCapture()
// @ts-expect-error - mockRejectedValue
util.exec.mockRejectedValue({
code: 1,
stderr: 'an error about dependencies',
})
try {
await start({ force: true, listrRenderer: 'silent' })
throw new Error('Should have thrown')
} catch (err) {
logger.error(err)
}
const exists = await fs.pathExists(binaryStatePath)
expect(exists).toEqual(false)
expect(normalize(output())).toMatchSnapshot('fails verifying Cypress')
})
})
describe('smoke test with DEBUG output', () => {
beforeEach(() => {
const stdoutWithDebugOutput = stripIndent`
some debug output
date: more debug output
222
after that more text
`
// @ts-expect-error - mockImplementation
util.exec.mockImplementation((...args: any) => {
if (args[0] === executablePath) {
return Promise.resolve({
stdout: stdoutWithDebugOutput,
})
}
return Promise.reject(new Error('should have caught error'))
})
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
})
it('finds ping value in the verbose output', async () => {
const output = createStdoutCapture()
await start({ listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot('verbose stdout output')
})
})
describe('smoke test retries on bad display with our Xvfb', () => {
let loggerWarnSpy: MockInstance<(...messages: any[]) => void>
beforeEach(() => {
vi.stubEnv('DISPLAY', 'test-display')
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// sinon.spy(logger, 'warn')
loggerWarnSpy = vi.spyOn(logger, 'warn')
})
it('successfully retries with our Xvfb on Linux', async () => {
// initially we think the user has everything set
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
util.isPossibleLinuxWithIncorrectDisplay.mockReturnValue(true)
// @ts-expect-error - mockImplementationOnce
util.exec.mockImplementationOnce((...args: any) => {
const firstSpawnError: any = new Error('')
// this message contains typical Gtk error shown if X11 is incorrect
// like in the case of DISPLAY=987
firstSpawnError.stderr = stripIndent`
[some noise here] Gtk: cannot open display: 987
and maybe a few other lines here with weird indent
`
firstSpawnError.stdout = ''
// the second time the binary returns expected ping
// @ts-expect-error - mockImplementationOnce
util.exec.mockImplementationOnce((...args: any) => {
if (args[0] === executablePath) {
return Promise.resolve({
stdout: '222',
})
}
})
return Promise.reject(firstSpawnError)
})
await start({ listrRenderer: 'silent' })
expect(util.exec).toHaveBeenCalledTimes(2)
// user should have been warned
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining(
'This is likely due to a misconfigured DISPLAY environment variable.',
))
})
it('fails on both retries with our Xvfb on Linux', async () => {
// initially we think the user has everything set
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(false)
// @ts-expect-error - mockReturnValue
util.isPossibleLinuxWithIncorrectDisplay.mockReturnValue(true)
// @ts-expect-error - mockImplementationOnce
util.exec.mockImplementationOnce((...args: any) => {
// @ts-expect-error - mockImplementationOnce
os.platform.mockReturnValue('linux')
expect(xvfb.start).not.toHaveBeenCalled()
const firstSpawnError: any = new Error('')
// this message contains typical Gtk error shown if X11 is incorrect
// like in the case of DISPLAY=987
firstSpawnError.stderr = stripIndent`
[some noise here] Gtk: cannot open display: 987
and maybe a few other lines here with weird indent
`
firstSpawnError.stdout = ''
// the second time it runs, it fails for some other reason
const secondMessage = stripIndent`
[some noise here] Gtk: cannot open display: 987
some other error
again with
some weird indent
`
// @ts-expect-error - mockImplementationOnce
util.exec.mockImplementationOnce((...args: any) => {
if (args[0] === executablePath) {
return Promise.reject(new Error(secondMessage))
}
})
return Promise.reject(firstSpawnError)
})
try {
await start({ listrRenderer: 'silent' })
} catch (e) {
expect(util.exec).toHaveBeenCalledTimes(2)
// second time around we should have called Xvfb
expect(xvfb.start).toHaveBeenCalledOnce
expect(xvfb.stop).toHaveBeenCalledOnce
// user should have been warned
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('DISPLAY was set to: "test-display"'))
expect(e.message).toMatchSnapshot('tried to verify twice, on the first try got the DISPLAY error')
return
}
throw new Error('Should have failed')
})
it('logs an error if Cypress executable does not exist', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: false,
packageVersion,
})
try {
await start({ listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot('no Cypress executable')
return
}
throw new Error('Should have thrown')
})
it('logs an error if Cypress executable does not have permissions', async () => {
const output = createStdoutCapture()
mockfs.restore()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o666 }),
packageVersion,
})
try {
await start({ listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot('Cypress non-executable permission')
return
}
throw new Error('Should have thrown')
})
it('logs and runs when current version has not been verified', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
await start({ listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot('current version has not been verified')
})
it('logs and runs when installed version is different than package version', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion: '7.8.9',
})
await start({ listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot('different version installed')
})
it('is silent when logLevel is silent', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
vi.stubEnv('npm_config_loglevel', 'silent')
await start({ listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot('silent verify')
})
it('turns off Opening Cypress...', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion: '7.8.9',
})
await start({ welcomeMessage: false })
expect(normalize(output())).toMatchSnapshot('no welcome message')
})
it('logs error when fails smoke test unexpectedly without stderr', async () => {
const output = createStdoutCapture()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockRejectedValue
util.exec.mockRejectedValue({
stderr: '',
stdout: '',
message: 'Error: EPERM NOT PERMITTED',
})
try {
await start({ listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot('fails with no stderr')
return
}
throw new Error('Should have thrown')
})
})
describe('on linux', () => {
beforeEach(() => {
// @ts-expect-error - mockReturnValue
xvfb.isNeeded.mockReturnValue(true)
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
})
it('starts xvfb', async () => {
await start({ listrRenderer: 'silent' })
expect(xvfb.start).toHaveBeenCalled()
})
it('stops xvfb on spawned process close', async () => {
await start({ listrRenderer: 'silent' })
expect(xvfb.stop).toHaveBeenCalled()
})
it('logs error and exits when starting xvfb fails', async () => {
const output = createStdoutCapture()
const actualXvfb = (await vi.importActual<typeof import('../../../lib/exec/xvfb')>('../../../lib/exec/xvfb')).default
// @ts-expect-error - mockImplementation to test integration with xvfb module
xvfb.start.mockImplementation(actualXvfb.start)
const err: any = new Error('test without xvfb')
err.nonZeroExitCode = true
err.stack = 'xvfb? no dice'
// stub the xvfb module to test integration
vi.spyOn(_xvfb.prototype, 'start').mockImplementation((cb) => {
// mock a failure
cb(err)
})
try {
await start({ listrRenderer: 'silent' })
} catch (err) {
expect(xvfb.stop).toHaveBeenCalledOnce
logger.error(err)
expect(normalize(output())).toMatchSnapshot('xvfb fails')
return
}
throw new Error('Should have thrown')
})
})
describe('when running in CI', () => {
beforeEach(() => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - mockReturnValue
util.isCi.mockReturnValue(true)
})
it('uses verbose renderer', async () => {
const output = createStdoutCapture()
await start({ listrRenderer: 'silent' })
expect(normalize(output())).toMatchSnapshot('verifying in ci')
})
it('logs error when binary not found', async () => {
const output = createStdoutCapture()
mockfs({})
try {
await start({ listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot('error binary not found in ci')
return
}
throw new Error('Should have thrown')
})
})
describe('when env var CYPRESS_RUN_BINARY', async () => {
it('can validate and use executable', async () => {
const output = createStdoutCapture()
const envBinaryPath = '/custom/Contents/MacOS/Cypress'
const realEnvBinaryPath = `/real${envBinaryPath}`
vi.stubEnv('CYPRESS_RUN_BINARY', envBinaryPath)
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
customDir: '/real/custom',
})
// @ts-expect-error - mockImplementation
util.exec.mockImplementation((...args: any) => {
if (args[0] === realEnvBinaryPath && _.isEqual(args[1], ['--no-sandbox', '--smoke-test', '--ping=222'])) {
return Promise.resolve(spawnedProcess)
}
return Promise.reject(new Error('should have caught error'))
})
await start({ listrRenderer: 'silent' })
expect(util.exec).toHaveBeenCalledWith(realEnvBinaryPath, ['--no-sandbox', '--smoke-test', '--ping=222'], expect.anything())
expect(normalize(output())).toMatchSnapshot('valid CYPRESS_RUN_BINARY')
})
for (const platform of ['darwin', 'linux', 'win32']) {
it(`can log error to user on ${platform}`, async () => {
const output = createStdoutCapture()
vi.stubEnv('CYPRESS_RUN_BINARY', '/custom/')
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue(platform)
try {
await start({ listrRenderer: 'silent' })
} catch (err) {
logger.error(err)
expect(normalize(output())).toMatchSnapshot(`${platform}: error when invalid CYPRESS_RUN_BINARY`)
return
}
throw new Error('Should have thrown')
})
}
})
// tests for when Electron needs "--no-sandbox" CLI flag
describe('.needsSandbox', () => {
it('needs --no-sandbox on Linux as a root', () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error - mockReturnValue
geteuid.mockReturnValue(0)
expect(needsSandbox()).toEqual(true)
})
it('needs --no-sandbox on Linux as a non-root', () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('linux')
// @ts-expect-error - mockReturnValue
geteuid.mockReturnValue(1000)
expect(needsSandbox()).toEqual(true)
})
it('needs --no-sandbox on Mac as a non-root', () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('darwin')
// @ts-expect-error - mockReturnValue
geteuid.mockReturnValue(1000)
expect(needsSandbox()).toEqual(true)
})
it('does not need --no-sandbox on Windows', () => {
// @ts-expect-error - mockReturnValue
os.platform.mockReturnValue('win32')
expect(needsSandbox()).toEqual(false)
})
})
})
// TODO this needs documentation with examples badly.
function createfs ({ alreadyVerified, executable, packageVersion, customDir }: any) {
if (!customDir) {
customDir = '/cache/Cypress/1.2.3/Cypress.app'
}
// binary state is stored one folder higher than the runner itself
// see https://github.com/cypress-io/cypress/issues/6089
const binaryStateFolder = path.join(customDir, '..')
const binaryState = {
verified: alreadyVerified,
}
const binaryStateText = JSON.stringify(binaryState)
let mockFiles: any = {
[binaryStateFolder]: {
'binary_state.json': binaryStateText,
},
[customDir]: {
Contents: {
MacOS: executable
? {
Cypress: executable,
}
: {},
Resources: {
app: {
'package.json': `{"version": "${packageVersion}"}`,
},
},
},
},
}
if (customDir) {
mockFiles['/custom/Contents/MacOS/Cypress'] = mockfs.symlink({
path: '/real/custom/Contents/MacOS/Cypress',
mode: 0o777,
})
}
return mockfs(mockFiles)
}

View File

@@ -1,868 +0,0 @@
/* eslint-disable no-restricted-properties */
import '../../spec_helper'
import path from 'path'
import _ from 'lodash'
import os from 'os'
import cp from 'child_process'
import BluebirdPromise from 'bluebird'
import { stripIndent } from 'common-tags'
import mockfs from 'mock-fs'
import mockedEnv from 'mocked-env'
import Stdout from '../../support/stdout'
import normalize from '../../support/normalize'
import snapshot from '../../support/snapshot'
import mockSpawnModule from '../../support/spawn-mock'
import fs from '../../../lib/fs'
import util from '../../../lib/util'
import logger from '../../../lib/logger'
import xvfb from '../../../lib/exec/xvfb'
import verify from '../../../lib/tasks/verify'
const packageVersion = '1.2.3'
const cacheDir = '/cache/Cypress'
const executablePath = '/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress'
const binaryStatePath = '/cache/Cypress/1.2.3/binary_state.json'
const DEFAULT_VERIFY_TIMEOUT = 30000
let stdout: any
let spawnedProcess: any
/* eslint-disable no-octal */
context('lib/tasks/verify', () => {
before(async function () {
const mochaMain = await import('mocha-banner')
mochaMain.register()
})
beforeEach(() => {
stdout = Stdout.capture()
spawnedProcess = {
code: 0,
stderr: sinon.stub(),
stdout: '222',
}
;(os.platform as any).returns('darwin')
;(os.release as any).returns('0.0.0')
sinon.stub(util, 'getCacheDir').returns(cacheDir)
sinon.stub(util, 'isCi').returns(false)
sinon.stub(util, 'pkgVersion').returns(packageVersion)
sinon.stub(util, 'exec')
sinon.stub(xvfb, 'start').resolves()
sinon.stub(xvfb, 'stop').resolves()
sinon.stub(xvfb, 'isNeeded').returns(false)
sinon.stub(BluebirdPromise.prototype, 'delay').resolves()
sinon.stub(process, 'geteuid').returns(1000)
sinon.stub(_, 'random').returns(222)
util.exec
// @ts-expect-error - is a sinon stub
.withArgs(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'])
.resolves(spawnedProcess)
})
afterEach(() => {
Stdout.restore()
})
it('has verify task timeout', () => {
expect(verify.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.eql(DEFAULT_VERIFY_TIMEOUT)
})
it('accepts custom verify task timeout', async () => {
process.env.CYPRESS_VERIFY_TIMEOUT = '500000'
const proxyquire = await import('proxyquire')
const newVerifyInstance = proxyquire.default(`../../../lib/tasks/verify`, {}).default
expect(newVerifyInstance.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.eql(500000)
})
it('accepts custom verify task timeout from npm', async () => {
process.env.npm_config_CYPRESS_VERIFY_TIMEOUT = '500000'
const proxyquire = await import('proxyquire')
const newVerifyInstance = proxyquire.default(`../../../lib/tasks/verify`, {}).default
expect(newVerifyInstance.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.eql(500000)
})
it('falls back to default verify task timeout if custom value is invalid', async () => {
process.env.CYPRESS_VERIFY_TIMEOUT = 'foobar'
const proxyquire = await import('proxyquire')
const newVerifyInstance = proxyquire.default(`../../../lib/tasks/verify`, {}).default
expect(newVerifyInstance.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.eql(DEFAULT_VERIFY_TIMEOUT)
})
it('returns early when `CYPRESS_SKIP_VERIFY` is set to true', async () => {
process.env.CYPRESS_SKIP_VERIFY = 'true'
const proxyquire = await import('proxyquire')
const newVerifyInstance = proxyquire.default(`../../../lib/tasks/verify`, {}).default
return newVerifyInstance.start({ listrRenderer: 'silent' }).then((result: any) => {
expect(result).to.eq(undefined)
})
})
it('logs error and exits when no version of Cypress is installed', () => {
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('should have caught error')
})
.catch((err: any) => {
logger.error(err)
snapshot(
'no version of Cypress installed 1',
normalize(stdout.toString()),
)
})
})
it('adds --no-sandbox when user is root', () => {
// make it think the executable exists
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
;(process.geteuid as any).returns(0) // user is root
// @ts-expect-error - is a sinon stub
util.exec.resolves({
stdout: '222',
stderr: '',
})
return verify.start({ listrRenderer: 'silent' })
.then(() => {
expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'])
})
})
it('adds --no-sandbox when user is non-root', () => {
// make it think the executable exists
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
;(process.geteuid as any).returns(1000) // user is non-root
// @ts-expect-error - is a sinon stub
util.exec.resolves({
stdout: '222',
stderr: '',
})
return verify.start({ listrRenderer: 'silent' })
.then(() => {
expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'])
})
})
it('is noop when binary is already verified', () => {
// make it think the executable exists and is verified
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
return verify.start({ listrRenderer: 'silent' }).then(() => {
// nothing should have been logged to stdout
// since no verification took place
expect(stdout.toString()).to.be.empty
expect(util.exec).not.to.be.called
})
})
it('logs warning when installed version does not match verified version', () => {
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion: 'bloop',
})
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('should have caught error')
})
.catch(() => {
return snapshot(
'warning installed version does not match verified version 1',
normalize(stdout.toString()),
)
})
})
it('logs error and exits when executable cannot be found', () => {
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('should have caught error')
})
.catch((err: any) => {
logger.error(err)
snapshot('executable cannot be found 1', normalize(stdout.toString()))
})
})
it('logs error when child process hangs', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - invalid number of arguments for given type
sinon.stub(cp, 'spawn').withArgs('/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress').callsFake(mockSpawnModule.mockSpawn((cp: any) => {
cp.stderr.write('some stderr')
cp.stdout.write('some stdout')
}))
// @ts-expect-error - is a sinon stub
util.exec.restore()
return verify
.start({ smokeTestTimeout: 1, listrRenderer: 'silent' })
.catch((err: any) => {
logger.error(err)
})
.then(() => {
snapshot(normalize(stdout.toString()))
})
})
it('logs error when child process returns incorrect stdout (stderr when exists)', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
sinon.stub(cp, 'spawn').callsFake(mockSpawnModule.mockSpawn((cp: any) => {
cp.stderr.write('some stderr')
cp.stdout.write('some stdout')
cp.emit('exit', 0, null)
cp.end()
}))
// @ts-expect-error - is a sinon stub
util.exec.restore()
return verify
.start({ listrRenderer: 'silent' })
.catch((err: any) => {
logger.error(err)
})
.then(() => {
snapshot(normalize(stdout.toString()))
})
})
it('logs error when child process returns incorrect stdout (stdout when no stderr)', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
sinon.stub(cp, 'spawn').callsFake(mockSpawnModule.mockSpawn((cp: any) => {
cp.stdout.write('some stdout')
cp.emit('exit', 0, null)
cp.end()
}))
// @ts-expect-error - is a sinon stub
util.exec.restore()
return verify
.start({ listrRenderer: 'silent' })
.catch((err: any) => {
logger.error(err)
})
.then(() => {
snapshot(normalize(stdout.toString()))
})
})
describe('FORCE_COLOR', () => {
let previousForceColors: any
beforeEach(() => {
previousForceColors = process.env.FORCE_COLOR
process.env.FORCE_COLOR = 'true' as any
})
afterEach(() => {
process.env.FORCE_COLOR = previousForceColors
})
// @see https://github.com/cypress-io/cypress/issues/28982
it('sets FORCE_COLOR to 0 when piping stdioOptions to to the smoke test to avoid ANSI in binary smoke test', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - is a sinon stub
util.exec.resolves({
stdout: '222',
stderr: '',
})
return verify.start({ listrRenderer: 'silent' })
.then(() => {
expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'],
sinon.match({
env: {
FORCE_COLOR: '0',
},
}))
})
})
})
describe('with force: true', () => {
beforeEach(() => {
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
})
it('shows full path to executable when verifying', () => {
return verify.start({ force: true, listrRenderer: 'silent' }).then(() => {
snapshot('verification with executable 1', normalize(stdout.toString()))
})
})
it('clears verified version from state if verification fails', () => {
// @ts-expect-error - is a sinon stub
util.exec.restore()
sinon
.stub(util, 'exec')
.withArgs(executablePath)
.rejects({
code: 1,
stderr: 'an error about dependencies',
})
return verify
.start({ force: true, listrRenderer: 'silent' })
.then(() => {
throw new Error('Should have thrown')
})
.catch((err: any) => {
logger.error(err)
})
.then(() => {
return fs.pathExistsAsync(binaryStatePath)
})
.then((exists: any) => {
return expect(exists).to.eq(false)
})
.then(() => {
return snapshot(
'fails verifying Cypress 1',
normalize(stdout.toString()),
)
})
})
})
describe('smoke test with DEBUG output', () => {
beforeEach(() => {
const stdoutWithDebugOutput = stripIndent`
some debug output
date: more debug output
222
after that more text
`
// @ts-expect-error - is a sinon stub
util.exec.withArgs(executablePath).resolves({
stdout: stdoutWithDebugOutput,
})
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
})
it('finds ping value in the verbose output', () => {
return verify.start({ listrRenderer: 'silent' }).then(() => {
snapshot('verbose stdout output 1', normalize(stdout.toString()))
})
})
})
describe('smoke test retries on bad display with our Xvfb', () => {
let restore: any
beforeEach(() => {
restore = mockedEnv({
DISPLAY: 'test-display',
})
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - is a sinon stub
util.exec.restore()
sinon.spy(logger, 'warn')
})
afterEach(() => {
restore()
})
it('successfully retries with our Xvfb on Linux', () => {
// initially we think the user has everything set
// @ts-expect-error - is a sinon stub
xvfb.isNeeded.returns(false)
sinon.stub(util, 'isPossibleLinuxWithIncorrectDisplay').returns(true)
// @ts-expect-error - is a sinon stub
sinon.stub(util, 'exec').callsFake(() => {
const firstSpawnError: any = new Error('')
// this message contains typical Gtk error shown if X11 is incorrect
// like in the case of DISPLAY=987
firstSpawnError.stderr = stripIndent`
[some noise here] Gtk: cannot open display: 987
and maybe a few other lines here with weird indent
`
firstSpawnError.stdout = ''
// the second time the binary returns expected ping
// @ts-expect-error - is a sinon stub
util.exec.withArgs(executablePath).resolves({
stdout: '222',
})
return BluebirdPromise.reject(firstSpawnError)
})
return verify.start({ listrRenderer: 'silent' }).then(() => {
expect(util.exec).to.have.been.calledTwice
// user should have been warned
expect(logger.warn).to.have.been.calledWithMatch(
'This is likely due to a misconfigured DISPLAY environment variable.',
)
})
})
it('fails on both retries with our Xvfb on Linux', () => {
// initially we think the user has everything set
// @ts-expect-error - is a sinon stub
xvfb.isNeeded.returns(false)
sinon.stub(util, 'isPossibleLinuxWithIncorrectDisplay').returns(true)
// @ts-expect-error - is a sinon stub
sinon.stub(util, 'exec').callsFake(() => {
(os.platform as any).returns('linux')
expect(xvfb.start).to.not.have.been.called
const firstSpawnError: any = new Error('')
// this message contains typical Gtk error shown if X11 is incorrect
// like in the case of DISPLAY=987
firstSpawnError.stderr = stripIndent`
[some noise here] Gtk: cannot open display: 987
and maybe a few other lines here with weird indent
`
firstSpawnError.stdout = ''
// the second time it runs, it fails for some other reason
const secondMessage = stripIndent`
[some noise here] Gtk: cannot open display: 987
some other error
again with
some weird indent
`
// @ts-expect-error - is a sinon stub
util.exec.withArgs(executablePath).rejects(new Error(secondMessage))
return BluebirdPromise.reject(firstSpawnError)
})
return verify.start({ listrRenderer: 'silent' }).then(() => {
throw new Error('Should have failed')
})
.catch((e: any) => {
expect(util.exec).to.have.been.calledTwice
// second time around we should have called Xvfb
expect(xvfb.start).to.have.been.calledOnce
expect(xvfb.stop).to.have.been.calledOnce
// user should have been warned
expect(logger.warn).to.have.been.calledWithMatch('DISPLAY was set to: "test-display"')
snapshot('tried to verify twice, on the first try got the DISPLAY error', e.message)
})
})
})
it('logs an error if Cypress executable does not exist', () => {
createfs({
alreadyVerified: false,
executable: false,
packageVersion,
})
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('Should have thrown')
})
.catch((err: any) => {
stdout = Stdout.capture()
logger.error(err)
return snapshot('no Cypress executable 1', normalize(stdout.toString()))
})
})
it('logs an error if Cypress executable does not have permissions', () => {
mockfs.restore()
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o666 }),
packageVersion,
})
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('Should have thrown')
})
.catch((err: any) => {
stdout = Stdout.capture()
logger.error(err)
return snapshot(
'Cypress non-executable permissions 1',
normalize(stdout.toString()),
)
})
})
it('logs and runs when current version has not been verified', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
return verify.start({ listrRenderer: 'silent' }).then(() => {
return snapshot(
'current version has not been verified 1',
normalize(stdout.toString()),
)
})
})
it('logs and runs when installed version is different than package version', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion: '7.8.9',
})
return verify.start({ listrRenderer: 'silent' }).then(() => {
return snapshot(
'different version installed 1',
normalize(stdout.toString()),
)
})
})
it('is silent when logLevel is silent', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
process.env.npm_config_loglevel = 'silent'
return verify.start({ listrRenderer: 'silent' }).then(() => {
return snapshot(
'silent verify 1',
normalize(`[no output]${stdout.toString()}`),
)
})
})
it('turns off Opening Cypress...', () => {
createfs({
alreadyVerified: true,
executable: mockfs.file({ mode: 0o777 }),
packageVersion: '7.8.9',
})
return verify
.start({
welcomeMessage: false,
})
.then(() => {
return snapshot('no welcome message 1', normalize(stdout.toString()))
})
})
it('logs error when fails smoke test unexpectedly without stderr', () => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - is a sinon stub
util.exec.restore()
sinon.stub(util, 'exec').rejects({
stderr: '',
stdout: '',
message: 'Error: EPERM NOT PERMITTED',
})
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('Should have thrown')
})
.catch((err: any) => {
stdout = Stdout.capture()
logger.error(err)
return snapshot('fails with no stderr 1', normalize(stdout.toString()))
})
})
describe('on linux', () => {
beforeEach(() => {
// @ts-expect-error - is a sinon stub
xvfb.isNeeded.returns(true)
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
})
it('starts xvfb', () => {
return verify.start({ listrRenderer: 'silent' }).then(() => {
expect(xvfb.start).to.be.called
})
})
it('stops xvfb on spawned process close', () => {
return verify.start({ listrRenderer: 'silent' }).then(() => {
expect(xvfb.stop).to.be.called
})
})
it('logs error and exits when starting xvfb fails', () => {
const err: any = new Error('test without xvfb')
// @ts-expect-error - is a sinon stub
xvfb.start.restore()
err.nonZeroExitCode = true
err.stack = 'xvfb? no dice'
sinon.stub(xvfb._xvfb, 'startAsync').rejects(err)
return verify.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('should have thrown')
})
.catch((err: any) => {
expect(xvfb.stop).to.be.calledOnce
logger.error(err)
snapshot('xvfb fails 1', normalize(stdout.toString()))
})
})
})
describe('when running in CI', () => {
beforeEach(() => {
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
})
// @ts-expect-error - is a sinon stub
util.isCi.returns(true)
})
it('uses verbose renderer', () => {
return verify.start({ listrRenderer: 'silent' }).then(() => {
snapshot('verifying in ci 1', normalize(stdout.toString()))
})
})
it('logs error when binary not found', () => {
mockfs({})
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('Should have thrown')
})
.catch((err: any) => {
logger.error(err)
snapshot('error binary not found in ci 1', normalize(stdout.toString()))
})
})
})
describe('when env var CYPRESS_RUN_BINARY', () => {
it('can validate and use executable', () => {
const envBinaryPath = '/custom/Contents/MacOS/Cypress'
const realEnvBinaryPath = `/real${envBinaryPath}`
process.env.CYPRESS_RUN_BINARY = envBinaryPath
createfs({
alreadyVerified: false,
executable: mockfs.file({ mode: 0o777 }),
packageVersion,
customDir: '/real/custom',
})
util.exec
// @ts-expect-error - is a sinon stub
.withArgs(realEnvBinaryPath, ['--no-sandbox', '--smoke-test', '--ping=222'])
.resolves(spawnedProcess)
return verify.start({ listrRenderer: 'silent' }).then(() => {
// @ts-expect-error - is a sinon stub
expect(util.exec.firstCall.args[0]).to.equal(realEnvBinaryPath)
snapshot('valid CYPRESS_RUN_BINARY 1', normalize(stdout.toString()))
})
})
_.each(['darwin', 'linux', 'win32'], (platform) => {
return it('can log error to user', () => {
process.env.CYPRESS_RUN_BINARY = '/custom/'
;(os.platform as any).returns(platform)
return verify
.start({ listrRenderer: 'silent' })
.then(() => {
throw new Error('Should have thrown')
})
.catch((err: any) => {
logger.error(err)
snapshot(
`${platform}: error when invalid CYPRESS_RUN_BINARY 1`,
normalize(stdout.toString()),
)
})
})
})
})
// tests for when Electron needs "--no-sandbox" CLI flag
context('.needsSandbox', () => {
it('needs --no-sandbox on Linux as a root', () => {
(os.platform as any).returns('linux')
;(process.geteuid as any).returns(0) // user is root
expect(verify.needsSandbox()).to.be.true
})
it('needs --no-sandbox on Linux as a non-root', () => {
(os.platform as any).returns('linux')
;(process.geteuid as any).returns(1000) // user is non-root
expect(verify.needsSandbox()).to.be.true
})
it('needs --no-sandbox on Mac as a non-root', () => {
(os.platform as any).returns('darwin')
;(process.geteuid as any).returns(1000) // user is non-root
expect(verify.needsSandbox()).to.be.true
})
it('does not need --no-sandbox on Windows', () => {
(os.platform as any).returns('win32')
expect(verify.needsSandbox()).to.be.false
})
})
})
// TODO this needs documentation with examples badly.
function createfs ({ alreadyVerified, executable, packageVersion, customDir }: any) {
if (!customDir) {
customDir = '/cache/Cypress/1.2.3/Cypress.app'
}
// binary state is stored one folder higher than the runner itself
// see https://github.com/cypress-io/cypress/issues/6089
const binaryStateFolder = path.join(customDir, '..')
const binaryState = {
verified: alreadyVerified,
}
const binaryStateText = JSON.stringify(binaryState)
let mockFiles: any = {
[binaryStateFolder]: {
'binary_state.json': binaryStateText,
},
[customDir]: {
Contents: {
MacOS: executable
? {
Cypress: executable,
}
: {},
Resources: {
app: {
'package.json': `{"version": "${packageVersion}"}`,
},
},
},
},
}
if (customDir) {
mockFiles['/custom/Contents/MacOS/Cypress'] = mockfs.symlink({
path: '/real/custom/Contents/MacOS/Cypress',
mode: 0o777,
})
}
return mockfs(mockFiles)
}

733
cli/test/lib/util.spec.ts Normal file
View File

@@ -0,0 +1,733 @@
import { vi, describe, it, beforeEach, expect } from 'vitest'
import hasha from 'hasha'
import la from 'lazy-ass'
import util from '../../lib/util'
import logger from '../../lib/logger'
describe('util', () => {
beforeEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})
describe('.isBrokenGtkDisplay', () => {
it('detects only GTK message', () => {
const text = '[some noise here] Gtk: cannot open display: 99'
expect(util.isBrokenGtkDisplay(text)).toEqual(true)
// and not for the other messages
expect(util.isBrokenGtkDisplay('display was set incorrectly')).toEqual(false)
})
})
describe('.getGitHubIssueUrl', () => {
it('returns url for issue number', () => {
const url = util.getGitHubIssueUrl(4034)
expect(url).toEqual('https://github.com/cypress-io/cypress/issues/4034')
})
it('throws for anything but a positive integer', () => {
// @ts-expect-error
expect(() => util.getGitHubIssueUrl('4024')).toThrow()
expect(() => util.getGitHubIssueUrl(-5)).toThrow()
expect(() => util.getGitHubIssueUrl(5.19)).toThrow()
})
})
describe('.stdoutLineMatches', () => {
it('is a function', () => {
expect(util.stdoutLineMatches).toBeTypeOf('function')
})
it('matches entire output', () => {
const line = '444'
expect(util.stdoutLineMatches(line, line)).toEqual(true)
})
it('matches a line in output', () => {
const line = '444'
const stdout = ['start', line, 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).toEqual(true)
})
it('matches a trimmed line in output', () => {
const line = '444'
const stdout = ['start', ` ${line} `, 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).toEqual(true)
})
it('does not find match', () => {
const line = '445'
const stdout = ['start', '444', 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).toEqual(false)
})
})
describe('.normalizeModuleOptions', () => {
it('does not change other properties', () => {
const options = {
foo: 'bar',
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('passes string env unchanged', () => {
const options = {
env: 'foo=bar',
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts environment object', () => {
const options = {
env: {
foo: 'bar',
magicNumber: 1234,
host: 'kevin.dev.local',
},
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts config object', () => {
const options = {
config: {
baseUrl: 'http://localhost:2000',
watchForFileChanges: false,
},
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts reporterOptions object', () => {
const options = {
reporterOptions: {
mochaFile: 'results/my-test-output.xml',
toConsole: true,
},
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts specs array', () => {
const options = {
spec: [
'a', 'b', 'c',
],
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('does not convert spec when string', () => {
const options = {
spec: 'x,y,z',
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
})
describe('.supportsColor', () => {
beforeEach(() => {
// make sure CI is undefined when running in CircleCI to get deterministic results
vi.stubEnv('CI', undefined)
})
it('is true on obj return for stdout and stderr', async () => {
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: true,
stderr: true,
},
}
})
const utils = (await import('../../lib/util')).default
expect(utils.supportsColor()).toEqual(true)
})
it('is false on false return for stdout', async () => {
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: false,
stderr: true,
},
}
})
const utils = (await import('../../lib/util')).default
expect(utils.supportsColor()).toEqual(false)
})
it('is false on false return for stderr', async () => {
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: true,
stderr: false,
},
}
})
const util = (await import('../../lib/util')).default
expect(util.supportsColor()).toEqual(false)
})
it('is true when running in CI', async () => {
vi.stubEnv('CI', '1')
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: false,
stderr: false,
},
}
})
const util = (await import('../../lib/util')).default
expect(util.supportsColor()).toEqual(true)
})
it('is false when NO_COLOR has been set', async () => {
vi.stubEnv('CI', '1')
vi.stubEnv('NO_COLOR', '1')
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: true,
stderr: true,
},
}
})
const util = (await import('../../lib/util')).default
expect(util.supportsColor()).toEqual(false)
})
})
describe('.getEnvOverrides', () => {
it('returns object with colors + process overrides', async () => {
// force supportColors to return true
vi.stubEnv('CI', '1')
vi.doMock('tty', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
isatty: vi.fn(),
},
}
})
const tty = (await import('tty')).default
// @ts-expect-error mockImplementation
tty.isatty.mockReturnValue(true)
const util = (await import('../../lib/util')).default
expect(util.getEnvOverrides()).toEqual({
FORCE_STDIN_TTY: '1',
FORCE_STDOUT_TTY: '1',
FORCE_STDERR_TTY: '1',
FORCE_COLOR: '1',
DEBUG_COLORS: '1',
MOCHA_COLORS: '1',
})
// force supportColors to return false
vi.stubEnv('CI', undefined)
vi.stubEnv('NO_COLOR', '1')
// @ts-expect-error - mockImplementation
tty.isatty.mockReturnValue(false)
expect(util.getEnvOverrides()).toEqual({
FORCE_STDIN_TTY: '0',
FORCE_STDOUT_TTY: '0',
FORCE_STDERR_TTY: '0',
FORCE_COLOR: '0',
DEBUG_COLORS: '0',
})
})
})
describe('.getForceTty', () => {
it('forces when each stream is a tty', async () => {
vi.doMock('tty', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
isatty: vi.fn(),
},
}
})
const tty = (await import('tty')).default
// @ts-expect-error mockImplementation
tty.isatty.mockImplementation((args) => {
if (args === 0 || args === 1 || args === 2) {
return true
}
return false
})
const util = (await import('../../lib/util')).default
expect(util.getForceTty()).toEqual({
FORCE_STDIN_TTY: true,
FORCE_STDOUT_TTY: true,
FORCE_STDERR_TTY: true,
})
// @ts-expect-error mockImplementation
tty.isatty.mockReturnValue(false)
expect(util.getForceTty()).toEqual({
FORCE_STDIN_TTY: false,
FORCE_STDOUT_TTY: false,
FORCE_STDERR_TTY: false,
})
})
})
describe('.getOriginalNodeOptions', () => {
it('copy NODE_OPTIONS to ORIGINAL_NODE_OPTIONS', () => {
vi.stubEnv('NODE_OPTIONS', '--require foo.js')
// @ts-expect-error - bad type
expect(util.getOriginalNodeOptions({})).toEqual({
ORIGINAL_NODE_OPTIONS: '--require foo.js',
})
})
})
describe('.exit', () => {
it('calls process.exit', () => {
// @ts-expect-error wrong signature for process.exit
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined)
util.exit(2)
util.exit(0)
expect(processExitSpy).toHaveBeenCalledWith(2)
expect(processExitSpy).toHaveBeenCalledWith(0)
})
})
describe('.logErrorExit1', () => {
it('calls logger.error and process.exit', () => {
const err = new Error('foo')
// @ts-expect-error wrong signature for process.exit
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined)
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined)
util.logErrorExit1(err)
expect(processExitSpy).toHaveBeenCalledWith(1)
expect(loggerErrorSpy).toHaveBeenCalledWith('foo')
})
})
describe('.isSemver', () => {
it('is true with 3-digit version', () => {
expect(util.isSemver('1.2.3')).toEqual(true)
})
it('is true with 2-digit version', () => {
expect(util.isSemver('1.2')).toEqual(true)
})
it('is true with 1-digit version', () => {
expect(util.isSemver('1')).toEqual(true)
})
it('is false with URL', () => {
expect(util.isSemver('www.cypress.io/download/1.2.3')).toEqual(false)
})
it('is false with file path', () => {
expect(util.isSemver('0/path/1.2.3/mypath/2.3')).toEqual(false)
})
})
describe('.calculateEta', () => {
it('Remaining eta is same as elapsed when 50%', () => {
expect(util.calculateEta(50, 1000)).toEqual(1000)
})
it('Remaining eta is 0 when 100%', () => {
expect(util.calculateEta(100, 500)).toEqual(0)
})
})
describe('.convertPercentToPercentage', () => {
it('converts to 100 when 1', () => {
expect(util.convertPercentToPercentage(1)).toEqual(100)
})
it('strips out extra decimals', () => {
expect(util.convertPercentToPercentage(0.37892)).toEqual(38)
})
it('returns 0 if null num', () => {
expect(util.convertPercentToPercentage(null)).toEqual(0)
})
})
describe('.printNodeOptions', () => {
describe('NODE_OPTIONS is not set', () => {
it('does nothing if debug is not enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = false
util.printNodeOptions(log)
expect(log).not.toHaveBeenCalled()
})
it('prints message when debug is enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = true
util.printNodeOptions(log)
expect(log).toHaveBeenCalledWith('NODE_OPTIONS is not set')
})
})
describe('NODE_OPTIONS is set', () => {
beforeEach(() => {
vi.stubEnv('NODE_OPTIONS', 'foo')
})
it('does nothing if debug is not enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = false
util.printNodeOptions(log)
expect(log).not.toHaveBeenCalled()
})
it('prints value when debug is enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = true
util.printNodeOptions(log)
expect(log).toHaveBeenCalledWith('NODE_OPTIONS=%s', 'foo')
})
})
})
describe('.getOsVersionAsync', () => {
beforeEach(() => {
vi.doMock('os', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
platform: vi.fn(),
release: vi.fn(),
},
}
})
vi.doMock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
osInfo: vi.fn(),
},
}
})
})
it('calls os.release when systeminformation fails', async () => {
const os = (await import('os')).default
const si = (await import('systeminformation')).default
// @ts-expect-error - mockReturnValue
os.release.mockReturnValue('some-release')
// @ts-expect-error - mockRejectedValue
si.osInfo.mockRejectedValue(new Error('systeminformation failed'))
const util = (await import('../../lib/util')).default
const result = await util.getOsVersionAsync()
expect(result).toEqual('some-release')
expect(os.release).toHaveBeenCalled()
expect(si.osInfo).toHaveBeenCalled()
})
it('uses systeminformation when it succeeds', async () => {
const os = (await import('os')).default
const si = (await import('systeminformation')).default
// @ts-expect-error - mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Ubuntu',
release: '22.04',
})
const util = (await import('../../lib/util')).default
const result = await util.getOsVersionAsync()
expect(result).toEqual('Ubuntu - 22.04')
expect(si.osInfo).toHaveBeenCalled()
expect(os.release).not.toHaveBeenCalled()
})
it('falls back to os.release when systeminformation returns incomplete data', async () => {
const os = (await import('os')).default
const si = (await import('systeminformation')).default
// @ts-expect-error - mockResolvedValue
os.release.mockReturnValue('5.15.0')
// @ts-expect-error - mockResolvedValue
si.osInfo.mockResolvedValue({
distro: 'Ubuntu',
// missing release property
})
const util = (await import('../../lib/util')).default
const result = await util.getOsVersionAsync()
expect(result).toEqual('5.15.0')
expect(si.osInfo).toHaveBeenCalled()
expect(os.release).toHaveBeenCalled()
})
})
describe('dequote', () => {
it('removes double quotes', () => {
expect(util.dequote('"foo"')).toEqual('foo')
})
it('keeps single quotes', () => {
expect(util.dequote('\'foo\'')).toEqual('\'foo\'')
})
it('keeps unbalanced double quotes', () => {
expect(util.dequote('"foo')).toEqual('"foo')
})
it('keeps inner double quotes', () => {
expect(util.dequote('a"b"c')).toEqual('a"b"c')
})
it('passes empty strings', () => {
expect(util.dequote('')).toEqual('')
})
it('keeps single double quote character', () => {
expect(util.dequote('"')).toEqual('"')
})
})
describe('.getEnv', () => {
it('reads from package.json config', () => {
vi.stubEnv('npm_package_config_CYPRESS_FOO', 'bar')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('reads from .npmrc config', () => {
vi.stubEnv('npm_config_CYPRESS_FOO', 'bar')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('reads from env var', () => {
vi.stubEnv('CYPRESS_FOO', 'bar')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('prefers env var over .npmrc config', () => {
vi.stubEnv('CYPRESS_FOO', 'bar')
vi.stubEnv('npm_config_CYPRESS_FOO', 'baz')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('prefers env var over .npmrc config even if it\'s an empty string', () => {
vi.stubEnv('CYPRESS_FOO', '')
vi.stubEnv('npm_config_CYPRESS_FOO', 'baz')
expect(util.getEnv('CYPRESS_FOO')).toEqual('')
})
it('prefers .npmrc config over package config', () => {
vi.stubEnv('npm_package_config_CYPRESS_FOO', 'baz')
vi.stubEnv('npm_config_CYPRESS_FOO', 'bloop')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bloop')
})
it('prefers .npmrc config over package config even if it\'s an empty string', () => {
vi.stubEnv('npm_package_config_CYPRESS_FOO', 'baz')
vi.stubEnv('npm_config_CYPRESS_FOO', '')
expect(util.getEnv('CYPRESS_FOO')).toEqual('')
})
it('npm config set should work', () => {
vi.stubEnv('npm_config_cypress_foo_foo', 'bazz')
expect(util.getEnv('CYPRESS_FOO_FOO')).toEqual('bazz')
})
it('throws on non-string name', () => {
expect(() => util.getEnv()).toThrow()
expect(() => util.getEnv(42)).toThrow()
})
describe('with trim = true', () => {
it('trims returned string', () => {
vi.stubEnv('FOO', ' bar ')
expect(util.getEnv('FOO', true)).toEqual('bar')
})
it('removes quotes from the returned string', () => {
vi.stubEnv('FOO', ' "bar" ')
expect(util.getEnv('FOO', true)).toEqual('bar')
})
it('removes only single level of double quotes', () => {
vi.stubEnv('FOO', ' ""bar"" ')
expect(util.getEnv('FOO', true)).toEqual('"bar"')
})
it('keeps unbalanced double quote', () => {
vi.stubEnv('FOO', ' "bar ')
expect(util.getEnv('FOO', true)).toEqual('"bar')
})
it('trims but does not remove single quotes', () => {
vi.stubEnv('FOO', ' \'bar\' ')
expect(util.getEnv('FOO', true)).toEqual('\'bar\'')
})
it('keeps whitespace inside removed quotes', () => {
vi.stubEnv('FOO', '"foo.txt "')
expect(util.getEnv('FOO', true)).toEqual('foo.txt ')
})
})
})
describe('.getFileChecksum', () => {
it('computes same hash as Hasha SHA512', async () => {
const [checksum, expectedChecksum] = await Promise.all([
util.getFileChecksum(__filename),
hasha.fromFile(__filename, { algorithm: 'sha512' }),
])
la(checksum === expectedChecksum, 'our computed checksum', checksum,
'is different from expected', expectedChecksum)
})
})
describe('parseOpts', () => {
it('passes normal options and strips unknown ones', () => {
const result = util.parseOpts({
unknownOptions: true,
group: 'my group name',
ciBuildId: 'my ci build id',
})
expect(result).toEqual({
group: 'my group name',
ciBuildId: 'my ci build id',
})
})
it('removes leftover double quotes', () => {
const result = util.parseOpts({
group: '"my group name"',
ciBuildId: '"my ci build id"',
})
expect(result).toEqual({
group: 'my group name',
ciBuildId: 'my ci build id',
})
})
it('leaves unbalanced double quotes', () => {
const result = util.parseOpts({
group: 'my group name"',
ciBuildId: '"my ci build id',
})
expect(result).toEqual({
group: 'my group name"',
ciBuildId: '"my ci build id',
})
})
it('works with unspecified options', () => {
const result = util.parseOpts({
// notice that "group" option is missing
ciBuildId: '"my ci build id"',
})
expect(result).toEqual({
ciBuildId: 'my ci build id',
})
})
})
})

View File

@@ -1,619 +0,0 @@
import '../spec_helper'
import os from 'os'
import tty from 'tty'
import snapshot from '../support/snapshot'
import mockedEnv from 'mocked-env'
import supportsColor from 'supports-color'
import hasha from 'hasha'
import la from 'lazy-ass'
import util from '../../lib/util'
import logger from '../../lib/logger'
describe('util', () => {
beforeEach(() => {
sinon.stub(process, 'exit')
sinon.stub(logger, 'error')
})
context('.isBrokenGtkDisplay', () => {
it('detects only GTK message', () => {
(os.platform as any).returns('linux')
const text = '[some noise here] Gtk: cannot open display: 99'
expect(util.isBrokenGtkDisplay(text)).to.be.true
// and not for the other messages
expect(util.isBrokenGtkDisplay('display was set incorrectly')).to.be.false
})
})
context('.getGitHubIssueUrl', () => {
it('returns url for issue number', () => {
const url = util.getGitHubIssueUrl(4034)
expect(url).to.equal('https://github.com/cypress-io/cypress/issues/4034')
})
it('throws for anything but a positive integer', () => {
expect(() => {
return util.getGitHubIssueUrl('4034')
}).to.throw
expect(() => {
return util.getGitHubIssueUrl(-5)
}).to.throw
expect(() => {
return util.getGitHubIssueUrl(5.19)
}).to.throw
})
})
context('.stdoutLineMatches', () => {
it('is a function', () => {
expect(util.stdoutLineMatches).to.be.a('function')
})
it('matches entire output', () => {
const line = '444'
expect(util.stdoutLineMatches(line, line)).to.be.true
})
it('matches a line in output', () => {
const line = '444'
const stdout = ['start', line, 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).to.be.true
})
it('matches a trimmed line in output', () => {
const line = '444'
const stdout = ['start', ` ${line} `, 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).to.be.true
})
it('does not find match', () => {
const line = '445'
const stdout = ['start', '444', 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).to.be.false
})
})
context('.normalizeModuleOptions', () => {
it('does not change other properties', () => {
const options = {
foo: 'bar',
}
snapshot('others_unchanged 1', util.normalizeModuleOptions(options))
})
it('passes string env unchanged', () => {
const options = {
env: 'foo=bar',
}
snapshot('env_as_string 1', util.normalizeModuleOptions(options))
})
it('converts environment object', () => {
const options = {
env: {
foo: 'bar',
magicNumber: 1234,
host: 'kevin.dev.local',
},
}
snapshot('env_as_object 1', util.normalizeModuleOptions(options))
})
it('converts config object', () => {
const options = {
config: {
baseUrl: 'http://localhost:2000',
watchForFileChanges: false,
},
}
snapshot('config_as_object 1', util.normalizeModuleOptions(options))
})
it('converts reporterOptions object', () => {
const options = {
reporterOptions: {
mochaFile: 'results/my-test-output.xml',
toConsole: true,
},
}
snapshot('reporter_options_as_object 1', util.normalizeModuleOptions(options))
})
it('converts specs array', () => {
const options = {
spec: [
'a', 'b', 'c',
],
}
snapshot('spec_as_array 1', util.normalizeModuleOptions(options))
})
it('does not convert spec when string', () => {
const options = {
spec: 'x,y,z',
}
snapshot('spec_as_string 1', util.normalizeModuleOptions(options))
})
})
context('.supportsColor', () => {
it('is true on obj return for stdout and stderr', () => {
sinon.stub(supportsColor, 'stdout').value({})
sinon.stub(supportsColor, 'stderr').value({})
expect(util.supportsColor()).to.be.true
})
it('is false on false return for stdout', () => {
delete process.env.CI
sinon.stub(supportsColor, 'stdout').value(false)
sinon.stub(supportsColor, 'stderr').value({})
expect(util.supportsColor()).to.be.false
})
it('is false on false return for stderr', () => {
delete process.env.CI
sinon.stub(supportsColor, 'stdout').value({})
sinon.stub(supportsColor, 'stderr').value(false)
expect(util.supportsColor()).to.be.false
})
it('is true when running in CI', () => {
process.env.CI = '1'
sinon.stub(supportsColor, 'stdout').value(false)
expect(util.supportsColor()).to.be.true
})
it('is false when NO_COLOR has been set', () => {
process.env.CI = '1'
process.env.NO_COLOR = '1'
sinon.stub(supportsColor, 'stdout').value({})
sinon.stub(supportsColor, 'stderr').value({})
expect(util.supportsColor()).to.be.false
})
})
context('.getEnvOverrides', () => {
it('returns object with colors + process overrides', () => {
// shouldn't be stubbing 'what we own' but its easiest in this case
sinon.stub(util, 'supportsColor').returns(true)
sinon.stub(tty, 'isatty').returns(true)
expect(util.getEnvOverrides()).to.deep.eq({
FORCE_STDIN_TTY: '1',
FORCE_STDOUT_TTY: '1',
FORCE_STDERR_TTY: '1',
FORCE_COLOR: '1',
DEBUG_COLORS: '1',
MOCHA_COLORS: '1',
})
;(util.supportsColor as any).returns(false)
;(tty.isatty as any).returns(false)
expect(util.getEnvOverrides()).to.deep.eq({
FORCE_STDIN_TTY: '0',
FORCE_STDOUT_TTY: '0',
FORCE_STDERR_TTY: '0',
FORCE_COLOR: '0',
DEBUG_COLORS: '0',
})
})
})
context('.getForceTty', () => {
it('forces when each stream is a tty', () => {
sinon.stub(tty, 'isatty')
.withArgs(0).returns(true)
.withArgs(1).returns(true)
.withArgs(2).returns(true)
expect(util.getForceTty()).to.deep.eq({
FORCE_STDIN_TTY: true,
FORCE_STDOUT_TTY: true,
FORCE_STDERR_TTY: true,
})
;(tty.isatty as any)
.withArgs(0).returns(false)
.withArgs(1).returns(false)
.withArgs(2).returns(false)
expect(util.getForceTty()).to.deep.eq({
FORCE_STDIN_TTY: false,
FORCE_STDOUT_TTY: false,
FORCE_STDERR_TTY: false,
})
})
})
context('.getOriginalNodeOptions', () => {
let restoreEnv: any
const sandbox = sinon.createSandbox()
afterEach(() => {
if (restoreEnv) {
restoreEnv()
restoreEnv = null
}
})
it('copy NODE_OPTIONS to ORIGINAL_NODE_OPTIONS', () => {
sandbox.stub(process.versions, 'node').value('v16.14.2')
sandbox.stub(process.versions, 'openssl').value('1.0.0')
restoreEnv = mockedEnv({
NODE_OPTIONS: '--require foo.js',
})
expect(util.getOriginalNodeOptions({})).to.deep.eq({
ORIGINAL_NODE_OPTIONS: '--require foo.js',
})
})
})
context('.exit', () => {
it('calls process.exit', () => {
(process.exit as any).withArgs(2).withArgs(0)
util.exit(2)
util.exit(0)
})
})
context('.logErrorExit1', () => {
it('calls logger.error and process.exit', () => {
const err = new Error('foo')
;(logger.error as any).withArgs('foo')
;(process.exit as any).withArgs(1)
util.logErrorExit1(err)
})
})
describe('.isSemver', () => {
it('is true with 3-digit version', () => {
expect(util.isSemver('1.2.3')).to.equal(true)
})
it('is true with 2-digit version', () => {
expect(util.isSemver('1.2')).to.equal(true)
})
it('is true with 1-digit version', () => {
expect(util.isSemver('1')).to.equal(true)
})
it('is false with URL', () => {
expect(util.isSemver('www.cypress.io/download/1.2.3')).to.equal(false)
})
it('is false with file path', () => {
expect(util.isSemver('0/path/1.2.3/mypath/2.3')).to.equal(false)
})
})
describe('.calculateEta', () => {
it('Remaining eta is same as elapsed when 50%', () => {
expect(util.calculateEta('50', 1000)).to.equal(1000)
})
it('Remaining eta is 0 when 100%', () => {
expect(util.calculateEta('100', 500)).to.equal(0)
})
})
describe('.convertPercentToPercentage', () => {
it('converts to 100 when 1', () => {
expect(util.convertPercentToPercentage(1)).to.equal(100)
})
it('strips out extra decimals', () => {
expect(util.convertPercentToPercentage(0.37892)).to.equal(38)
})
it('returns 0 if null num', () => {
expect(util.convertPercentToPercentage(null)).to.equal(0)
})
})
context('.printNodeOptions', () => {
describe('NODE_OPTIONS is not set', () => {
it('does nothing if debug is not enabled', () => {
const log = sinon.spy()
;(log as any).enabled = false
util.printNodeOptions(log)
expect(log).not.have.been.called
})
it('prints message when debug is enabled', () => {
const log = sinon.spy()
;(log as any).enabled = true
util.printNodeOptions(log)
expect(log).to.be.calledWith('NODE_OPTIONS is not set')
})
})
describe('NODE_OPTIONS is set', () => {
beforeEach(() => {
process.env.NODE_OPTIONS = 'foo'
})
it('does nothing if debug is not enabled', () => {
const log = sinon.spy()
;(log as any).enabled = false
util.printNodeOptions(log)
expect(log).not.have.been.called
})
it('prints value when debug is enabled', () => {
const log = sinon.spy()
;(log as any).enabled = true
util.printNodeOptions(log)
expect(log).to.be.calledWith('NODE_OPTIONS=%s', 'foo')
})
})
})
describe('.getOsVersionAsync', () => {
let util
let systeminformation = {
osInfo: sinon.stub(),
}
beforeEach(async () => {
const proxyquire = await import('proxyquire')
util = proxyquire.default(`../../lib/util`, { systeminformation }).default
})
it('calls os.release when systeminformation fails', () => {
(os.platform as any).returns('darwin')
;(os.release as any).returns('some-release')
systeminformation.osInfo.rejects(new Error('systeminformation failed'))
return util.getOsVersionAsync()
.then(() => {
expect(os.release).to.be.called
expect(systeminformation.osInfo).to.be.called
})
})
it('uses systeminformation when it succeeds', () => {
(os.platform as any).returns('linux')
systeminformation.osInfo.resolves({
distro: 'Ubuntu',
release: '22.04',
})
return util.getOsVersionAsync()
.then((result) => {
expect(result).to.equal('Ubuntu - 22.04')
expect(systeminformation.osInfo).to.be.called
// os.release should not be called when systeminformation succeeds
expect(os.release).to.not.be.called
})
})
it('falls back to os.release when systeminformation returns incomplete data', () => {
(os.platform as any).returns('linux')
;(os.release as any).returns('5.15.0')
systeminformation.osInfo.resolves({
distro: 'Ubuntu',
// missing release property
})
return util.getOsVersionAsync()
.then(() => {
expect(systeminformation.osInfo).to.be.called
expect(os.release).to.be.called
})
})
})
describe('dequote', () => {
it('removes double quotes', () => {
expect(util.dequote('"foo"')).to.equal('foo')
})
it('keeps single quotes', () => {
expect(util.dequote('\'foo\'')).to.equal('\'foo\'')
})
it('keeps unbalanced double quotes', () => {
expect(util.dequote('"foo')).to.equal('"foo')
})
it('keeps inner double quotes', () => {
expect(util.dequote('a"b"c')).to.equal('a"b"c')
})
it('passes empty strings', () => {
expect(util.dequote('')).to.equal('')
})
it('keeps single double quote character', () => {
expect(util.dequote('"')).to.equal('"')
})
})
describe('.getEnv', () => {
it('reads from package.json config', () => {
process.env.npm_package_config_CYPRESS_FOO = 'bar'
expect(util.getEnv('CYPRESS_FOO')).to.eql('bar')
})
it('reads from .npmrc config', () => {
process.env.npm_config_CYPRESS_FOO = 'bar'
expect(util.getEnv('CYPRESS_FOO')).to.eql('bar')
})
it('reads from env var', () => {
process.env.CYPRESS_FOO = 'bar'
expect(util.getEnv('CYPRESS_FOO')).to.eql('bar')
})
it('prefers env var over .npmrc config', () => {
process.env.CYPRESS_FOO = 'bar'
process.env.npm_config_CYPRESS_FOO = 'baz'
expect(util.getEnv('CYPRESS_FOO')).to.eql('bar')
})
it('prefers env var over .npmrc config even if it\'s an empty string', () => {
process.env.CYPRESS_FOO = ''
process.env.npm_config_CYPRESS_FOO = 'baz'
expect(util.getEnv('CYPRESS_FOO')).to.eql('')
})
it('prefers .npmrc config over package config', () => {
process.env.npm_package_config_CYPRESS_FOO = 'baz'
process.env.npm_config_CYPRESS_FOO = 'bloop'
expect(util.getEnv('CYPRESS_FOO')).to.eql('bloop')
})
it('prefers .npmrc config over package config even if it\'s an empty string', () => {
process.env.npm_package_config_CYPRESS_FOO = 'baz'
process.env.npm_config_CYPRESS_FOO = ''
expect(util.getEnv('CYPRESS_FOO')).to.eql('')
})
it('npm config set should work', () => {
process.env.npm_config_cypress_foo_foo = 'bazz'
expect(util.getEnv('CYPRESS_FOO_FOO')).to.eql('bazz')
})
it('throws on non-string name', () => {
expect(() => {
util.getEnv()
}).to.throw()
expect(() => {
util.getEnv(42)
}).to.throw()
})
context('with trim = true', () => {
it('trims returned string', () => {
process.env.FOO = ' bar '
expect(util.getEnv('FOO', true)).to.equal('bar')
})
it('removes quotes from the returned string', () => {
process.env.FOO = ' "bar" '
expect(util.getEnv('FOO', true)).to.equal('bar')
})
it('removes only single level of double quotes', () => {
process.env.FOO = ' ""bar"" '
expect(util.getEnv('FOO', true)).to.equal('"bar"')
})
it('keeps unbalanced double quote', () => {
process.env.FOO = ' "bar '
expect(util.getEnv('FOO', true)).to.equal('"bar')
})
it('trims but does not remove single quotes', () => {
process.env.FOO = ' \'bar\' '
expect(util.getEnv('FOO', true)).to.equal('\'bar\'')
})
it('keeps whitespace inside removed quotes', () => {
process.env.FOO = '"foo.txt "'
expect(util.getEnv('FOO', true)).to.equal('foo.txt ')
})
})
})
context('.getFileChecksum', () => {
it('computes same hash as Hasha SHA512', () => {
return Promise.all([
util.getFileChecksum(__filename),
hasha.fromFile(__filename, { algorithm: 'sha512' }),
]).then(([checksum, expectedChecksum]) => {
la(checksum === expectedChecksum, 'our computed checksum', checksum,
'is different from expected', expectedChecksum)
})
})
})
context('parseOpts', () => {
it('passes normal options and strips unknown ones', () => {
const result = util.parseOpts({
unknownOptions: true,
group: 'my group name',
ciBuildId: 'my ci build id',
})
expect(result).to.deep.equal({
group: 'my group name',
ciBuildId: 'my ci build id',
})
})
it('removes leftover double quotes', () => {
const result = util.parseOpts({
group: '"my group name"',
ciBuildId: '"my ci build id"',
})
expect(result).to.deep.equal({
group: 'my group name',
ciBuildId: 'my ci build id',
})
})
it('leaves unbalanced double quotes', () => {
const result = util.parseOpts({
group: 'my group name"',
ciBuildId: '"my ci build id',
})
expect(result).to.deep.equal({
group: 'my group name"',
ciBuildId: '"my ci build id',
})
})
it('works with unspecified options', () => {
const result = util.parseOpts({
// notice that "group" option is missing
ciBuildId: '"my ci build id"',
})
expect(result).to.deep.equal({
ciBuildId: 'my ci build id',
})
})
})
})

View File

@@ -1,124 +0,0 @@
import _ from 'lodash'
import os from 'os'
import sinon from 'sinon'
import mockfs from 'mock-fs'
import Bluebird from 'bluebird'
import util from '../lib/util'
import nock from 'nock'
import { MockChildProcess } from 'spawn-mock'
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import chaiString from 'chai-string'
import sinonChai from '@cypress/sinon-chai'
const _kill = MockChildProcess.prototype.kill
const patchMockSpawn = (): void => {
MockChildProcess.prototype.kill = function (...args: any[]): any {
this.emit('exit')
return _kill.apply(this, args)
}
}
patchMockSpawn()
// Set up global variables for test environment
declare global {
const sinon: typeof import('sinon')
const expect: typeof import('chai').expect
const lib: string
}
(global as any).sinon = sinon
;(global as any).expect = chai.expect
chai
.use(sinonChai)
.use(chaiString)
.use(chaiAsPromised)
sinon.usingPromise(Bluebird as any)
delete process.env.CYPRESS_RUN_BINARY
delete process.env.CYPRESS_INSTALL_BINARY
delete process.env.CYPRESS_CACHE_FOLDER
delete process.env.CYPRESS_DOWNLOAD_MIRROR
delete process.env.DISPLAY
// enable running specs with --silent w/out affecting logging in tests
process.env.npm_config_loglevel = 'notice'
const env = _.clone(process.env)
function throwIfFnNotStubbed (stub: any, method: string): void {
const sig = `.${method}(...)`
stub.callsFake(function (...args: any[]): void {
const err = new Error(`${sig} was called without being stubbed.
${sig} was called with arguments:
${args.map(JSON.stringify).join(', ')}
`)
err.stack = _
.chain(err.stack)
.split('\n')
.reject((str: string) => {
return _.includes(str, 'sinon')
})
.join('\n')
.value()
throw err
})
}
const $stub = sinon.stub
sinon.stub = function (obj?: any, method?: string): any {
/* eslint-disable prefer-rest-params */
const stub = $stub.apply(this, arguments as any)
let fns = [method]
if (arguments.length === 1) {
fns = _.functions(obj)
}
if (arguments.length === 0) {
throwIfFnNotStubbed(stub, '[anonymous function]')
return stub
}
fns.forEach((name: string) => {
const fn = obj[name]
if (_.isFunction(fn)) {
throwIfFnNotStubbed(fn, name)
}
})
return stub
}
beforeEach(function (): void {
sinon.stub(os, 'platform')
sinon.stub(os, 'arch')
sinon.stub(os, 'release')
sinon.stub(util, 'getOsVersionAsync').resolves('Foo-OsVersion')
;(os.arch as any).returns('x64')
})
afterEach(function (): void {
mockfs.restore()
process.env = _.clone(env)
sinon.restore()
nock.cleanAll()
;(util as any)._cachedArch = undefined
})

View File

@@ -1,12 +0,0 @@
import _snapshot from 'snap-shot-it'
import mockfs from 'mock-fs'
// Type as any to avoid strict typing issues with rest parameters
const snapshotAny: any = _snapshot
const snapshot = (...args: any[]): void => {
mockfs.restore()
snapshotAny(...args)
}
export default snapshot

View File

@@ -1,15 +0,0 @@
import spawnMock from 'spawn-mock'
// sinon is assumed to be available globally in test environment
declare const sinon: any
export default {
mockSpawn (cb: (cp: any) => any): any {
return spawnMock.mockSpawn((cp: any) => {
// execa expects .cancel to exist
cp.cancel = sinon.stub()
return cb(cp)
})
},
}

View File

@@ -1,30 +0,0 @@
const _write = process.stdout.write
const stdoutModule = {
capture (): { data: string[], toString: () => string } {
const logs: string[] = []
const write = process.stdout.write
process.stdout.write = function (str: any): boolean {
logs.push(str)
/* eslint-disable prefer-rest-params */
return write.apply(this, arguments as any)
}
return {
data: logs,
toString: (): string => {
return logs.join('')
},
}
},
restore (): void {
process.stdout.write = _write
},
}
export default stdoutModule

18
cli/tsconfig.esm.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"rootDir": "./lib",
"outDir": "./dist",
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"noImplicitAny": false,
},
"include": [
"lib/**/*.mts",
]
}

View File

@@ -1,22 +1,17 @@
{
"compilerOptions": {
"rootDir": "./lib",
"outDir": "./dist",
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"resolveJsonModule": true,
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"noImplicitAny": false,
"types": [
"mocha"
]
},
"include": [
"index.ts",
"lib/**/*.ts"
],
"exclude": [
"types",
"scripts"
"lib/**/*.ts",
]
}

View File

@@ -21,7 +21,7 @@ const {
getIndexJscHash,
DUMMY_INDEX_JSC_HASH,
} = require('./binary/binary-sources')
const verify = require('../cli/lib/tasks/verify').default
const { needsSandbox } = require('../cli/lib/tasks/verify')
const execa = require('execa')
const meta = require('./binary/meta')
@@ -30,7 +30,7 @@ const CY_ROOT_DIR = path.join(__dirname, '..')
const createJscFromCypress = async () => {
const args = []
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}

View File

@@ -25,7 +25,7 @@ const { moveBinaries } = require('./move-binaries')
const { exec } = require('child_process')
const xvfb = require('../../cli/lib/exec/xvfb').default
const smoke = require('./smoke')
const verify = require('../../cli/lib/tasks/verify').default
const { needsSandbox } = require('../../cli/lib/tasks/verify')
const execa = require('execa')
const log = function (msg) {
@@ -69,7 +69,7 @@ async function testExecutableVersion (buildAppExecutable, version) {
const args = ['--version']
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}

View File

@@ -5,7 +5,7 @@ const execa = require('execa')
const path = require('path')
const Promise = require('bluebird')
const os = require('os')
const verify = require('../../cli/lib/tasks/verify').default
const { needsSandbox } = require('../../cli/lib/tasks/verify')
const Fixtures = require('@tooling/system-tests')
const { scaffoldCommonNodeModules } = require('@tooling/system-tests/lib/dep-installer')
@@ -37,7 +37,7 @@ const runSmokeTest = function (buildAppExecutable, timeoutSeconds = 30) {
const args = []
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}
@@ -86,7 +86,7 @@ const runProjectTest = function (buildAppExecutable, e2e) {
`--spec=${e2e}/cypress/e2e/simple_passing.cy.js`,
]
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}
@@ -136,7 +136,7 @@ const runFailingProjectTest = function (buildAppExecutable, e2e) {
`--spec=${e2e}/cypress/e2e/simple_failing.cy.js`,
]
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}
@@ -178,7 +178,7 @@ const runV8SnapshotProjectTest = function (buildAppExecutable, e2e) {
`--spec=${e2e}/cypress/e2e/simple_v8_snapshot.cy.js`,
]
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}
@@ -215,7 +215,7 @@ const runErroringProjectTest = function (buildAppExecutable, e2e, testName, erro
`--spec=${e2e}/cypress/e2e/simple_passing.cy.js`,
]
if (verify.needsSandbox()) {
if (needsSandbox()) {
args.push('--no-sandbox')
}

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