mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-14 19:20:42 -06:00
Merge branch 'develop' into 9.0-release
This commit is contained in:
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"chrome:beta": "94.0.4606.54",
|
||||
"chrome:stable": "94.0.4606.54"
|
||||
"chrome:beta": "95.0.4638.32",
|
||||
"chrome:stable": "94.0.4606.71"
|
||||
}
|
||||
|
||||
15
circle.yml
15
circle.yml
@@ -991,8 +991,6 @@ jobs:
|
||||
- run: yarn lerna run build-prod --stream
|
||||
# run unit tests from each individual package
|
||||
- run: yarn test
|
||||
# check for compile errors with the releaserc scripts
|
||||
- run: yarn test-npm-package-release-script
|
||||
- verify-mocha-results:
|
||||
expectedResultCount: 9
|
||||
- store_test_results:
|
||||
@@ -1002,6 +1000,14 @@ jobs:
|
||||
path: cli/test/html
|
||||
- store-npm-logs
|
||||
|
||||
unit-tests-release:
|
||||
<<: *defaults
|
||||
resource_class: medium
|
||||
parallelism: 1
|
||||
steps:
|
||||
- restore_cached_workspace
|
||||
- run: yarn test-npm-package-release-script
|
||||
|
||||
lint-types:
|
||||
<<: *defaults
|
||||
parallelism: 1
|
||||
@@ -1975,6 +1981,10 @@ linux-workflow: &linux-workflow
|
||||
- unit-tests:
|
||||
requires:
|
||||
- build
|
||||
- unit-tests-release:
|
||||
context: test-runner:npm-release
|
||||
requires:
|
||||
- build
|
||||
- server-unit-tests:
|
||||
requires:
|
||||
- build
|
||||
@@ -2108,6 +2118,7 @@ linux-workflow: &linux-workflow
|
||||
- server-integration-tests
|
||||
- server-unit-tests
|
||||
- unit-tests
|
||||
- unit-tests-release
|
||||
- cli-visual-tests
|
||||
|
||||
# various testing scenarios, like building full binary
|
||||
|
||||
@@ -68,6 +68,23 @@ const cypressModuleApi = {
|
||||
return cli.parseRunCommand(args)
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides automatic code completion for configuration in many popular code editors.
|
||||
* While it's not strictly necessary for Cypress to parse your configuration, we
|
||||
* recommend wrapping your config object with `defineConfig()`
|
||||
* @example
|
||||
* module.exports = defineConfig({
|
||||
* viewportWith: 400
|
||||
* })
|
||||
*
|
||||
* @see ../types/cypress-npm-api.d.ts
|
||||
* @param {Cypress.ConfigOptions} config
|
||||
* @returns {Cypress.ConfigOptions} the configuration passed in parameter
|
||||
*/
|
||||
defineConfig (config) {
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = cypressModuleApi
|
||||
|
||||
15
cli/types/cypress-npm-api.d.ts
vendored
15
cli/types/cypress-npm-api.d.ts
vendored
@@ -377,6 +377,21 @@ declare module 'cypress' {
|
||||
* Cypress does
|
||||
*/
|
||||
cli: CypressCommandLine.CypressCliParser
|
||||
|
||||
/**
|
||||
* Provides automatic code completion for configuration in many popular code editors.
|
||||
* While it's not strictly necessary for Cypress to parse your configuration, we
|
||||
* recommend wrapping your config object with `defineConfig()`
|
||||
* @example
|
||||
* module.exports = defineConfig({
|
||||
* viewportWith: 400
|
||||
* })
|
||||
*
|
||||
* @see ../types/cypress-npm-api.d.ts
|
||||
* @param {Cypress.ConfigOptions} config
|
||||
* @returns {Cypress.ConfigOptions} the configuration passed in parameter
|
||||
*/
|
||||
defineConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions
|
||||
}
|
||||
|
||||
// export Cypress NPM module interface
|
||||
|
||||
175
cli/types/cypress.d.ts
vendored
175
cli/types/cypress.d.ts
vendored
@@ -168,7 +168,7 @@ declare namespace Cypress {
|
||||
/**
|
||||
* The interface for user-defined properties in Window object under test.
|
||||
*/
|
||||
interface ApplicationWindow {} // tslint:disable-line
|
||||
interface ApplicationWindow { } // tslint:disable-line
|
||||
|
||||
/**
|
||||
* Several libraries are bundled with Cypress by default.
|
||||
@@ -521,7 +521,7 @@ declare namespace Cypress {
|
||||
/**
|
||||
* @see https://on.cypress.io/keyboard-api
|
||||
*/
|
||||
Keyboard: {
|
||||
Keyboard: {
|
||||
defaults(options: Partial<KeyboardDefaultsOptions>): void
|
||||
}
|
||||
|
||||
@@ -579,7 +579,7 @@ declare namespace Cypress {
|
||||
}
|
||||
|
||||
interface SessionOptions {
|
||||
validate?: () => false|void
|
||||
validate?: () => false | void
|
||||
}
|
||||
|
||||
type CanReturnChainable = void | Chainable | Promise<unknown>
|
||||
@@ -717,36 +717,36 @@ declare namespace Cypress {
|
||||
```
|
||||
*/
|
||||
clearLocalStorage(re: RegExp): Chainable<Storage>
|
||||
/**
|
||||
* Clear data in local storage.
|
||||
* Cypress automatically runs this command before each test to prevent state from being
|
||||
* shared across tests. You shouldn’t need to use this command unless you’re using it
|
||||
* to clear localStorage inside a single test. Yields `localStorage` object.
|
||||
*
|
||||
* @see https://on.cypress.io/clearlocalstorage
|
||||
* @param {options} [object] - options object
|
||||
* @example
|
||||
```
|
||||
// Removes all local storage items, without logging
|
||||
cy.clearLocalStorage({ log: false })
|
||||
```
|
||||
*/
|
||||
/**
|
||||
* Clear data in local storage.
|
||||
* Cypress automatically runs this command before each test to prevent state from being
|
||||
* shared across tests. You shouldn’t need to use this command unless you’re using it
|
||||
* to clear localStorage inside a single test. Yields `localStorage` object.
|
||||
*
|
||||
* @see https://on.cypress.io/clearlocalstorage
|
||||
* @param {options} [object] - options object
|
||||
* @example
|
||||
```
|
||||
// Removes all local storage items, without logging
|
||||
cy.clearLocalStorage({ log: false })
|
||||
```
|
||||
*/
|
||||
clearLocalStorage(options: Partial<Loggable>): Chainable<Storage>
|
||||
/**
|
||||
* Clear data in local storage.
|
||||
* Cypress automatically runs this command before each test to prevent state from being
|
||||
* shared across tests. You shouldn’t need to use this command unless you’re using it
|
||||
* to clear localStorage inside a single test. Yields `localStorage` object.
|
||||
*
|
||||
* @see https://on.cypress.io/clearlocalstorage
|
||||
* @param {string} [key] - name of a particular item to remove (optional).
|
||||
* @param {options} [object] - options object
|
||||
* @example
|
||||
```
|
||||
// Removes item "todos" without logging
|
||||
cy.clearLocalStorage("todos", { log: false })
|
||||
```
|
||||
*/
|
||||
/**
|
||||
* Clear data in local storage.
|
||||
* Cypress automatically runs this command before each test to prevent state from being
|
||||
* shared across tests. You shouldn’t need to use this command unless you’re using it
|
||||
* to clear localStorage inside a single test. Yields `localStorage` object.
|
||||
*
|
||||
* @see https://on.cypress.io/clearlocalstorage
|
||||
* @param {string} [key] - name of a particular item to remove (optional).
|
||||
* @param {options} [object] - options object
|
||||
* @example
|
||||
```
|
||||
// Removes item "todos" without logging
|
||||
cy.clearLocalStorage("todos", { log: false })
|
||||
```
|
||||
*/
|
||||
clearLocalStorage(key: string, options: Partial<Loggable>): Chainable<Storage>
|
||||
|
||||
/**
|
||||
@@ -834,7 +834,7 @@ declare namespace Cypress {
|
||||
* // or use this shortcut
|
||||
* cy.clock().invoke('restore')
|
||||
*/
|
||||
clock(now: number|Date, options?: Loggable): Chainable<Clock>
|
||||
clock(now: number | Date, options?: Loggable): Chainable<Clock>
|
||||
/**
|
||||
* Mocks global clock but only overrides specific functions.
|
||||
*
|
||||
@@ -843,7 +843,7 @@ declare namespace Cypress {
|
||||
* // keep current date but override "setTimeout" and "clearTimeout"
|
||||
* cy.clock(null, ['setTimeout', 'clearTimeout'])
|
||||
*/
|
||||
clock(now: number|Date, functions?: Array<'setTimeout' | 'clearTimeout' | 'setInterval' | 'clearInterval' | 'Date'>, options?: Loggable): Chainable<Clock>
|
||||
clock(now: number | Date, functions?: Array<'setTimeout' | 'clearTimeout' | 'setInterval' | 'clearInterval' | 'Date'>, options?: Loggable): Chainable<Clock>
|
||||
/**
|
||||
* Mocks global clock and all functions.
|
||||
*
|
||||
@@ -977,14 +977,14 @@ declare namespace Cypress {
|
||||
*/
|
||||
debug(options?: Partial<Loggable>): Chainable<Subject>
|
||||
|
||||
/**
|
||||
* Save/Restore browser Cookies, LocalStorage, and SessionStorage data resulting from the supplied `setup` function.
|
||||
*
|
||||
* Only available if the `experimentalSessionSupport` config option is enabled.
|
||||
*
|
||||
* @see https://on.cypress.io/session
|
||||
*/
|
||||
session(id: string|object, setup?: SessionOptions['validate'], options?: SessionOptions): Chainable<null>
|
||||
/**
|
||||
* Save/Restore browser Cookies, LocalStorage, and SessionStorage data resulting from the supplied `setup` function.
|
||||
*
|
||||
* Only available if the `experimentalSessionSupport` config option is enabled.
|
||||
*
|
||||
* @see https://on.cypress.io/session
|
||||
*/
|
||||
session(id: string | object, setup?: SessionOptions['validate'], options?: SessionOptions): Chainable<null>
|
||||
|
||||
/**
|
||||
* Get the window.document of the page that is currently active.
|
||||
@@ -1648,17 +1648,11 @@ declare namespace Cypress {
|
||||
scrollTo(x: number | string, y: number | string, options?: Partial<ScrollToOptions>): Chainable<Subject>
|
||||
|
||||
/**
|
||||
* Select an `<option>` with specific text within a `<select>`.
|
||||
* Select an `<option>` with specific text, value, or index within a `<select>`.
|
||||
*
|
||||
* @see https://on.cypress.io/select
|
||||
*/
|
||||
select(text: string | string[], options?: Partial<SelectOptions>): Chainable<Subject>
|
||||
/**
|
||||
* Select an `<option>` with specific value(s) within a `<select>`.
|
||||
*
|
||||
* @see https://on.cypress.io/select
|
||||
*/
|
||||
select(value: string | string[], options?: Partial<SelectOptions>): Chainable<Subject>
|
||||
select(valueOrTextOrIndex: string | number | Array<string | number>, options?: Partial<SelectOptions>): Chainable<Subject>
|
||||
|
||||
/**
|
||||
* @deprecated Use `cy.intercept()` instead.
|
||||
@@ -1909,13 +1903,13 @@ declare namespace Cypress {
|
||||
*
|
||||
* @see https://on.cypress.io/then
|
||||
*/
|
||||
then<S extends HTMLElement>(options: Partial<Timeoutable>, fn: (this: ObjectLike, currentSubject: Subject) => S): Chainable<JQuery<S>>
|
||||
/**
|
||||
* Enables you to work with the subject yielded from the previous command / promise.
|
||||
*
|
||||
* @see https://on.cypress.io/then
|
||||
*/
|
||||
then<S extends ArrayLike<HTMLElement>>(options: Partial<Timeoutable>, fn: (this: ObjectLike, currentSubject: Subject) => S): Chainable<JQuery<S extends ArrayLike<infer T> ? T : never>>
|
||||
then<S extends HTMLElement>(options: Partial<Timeoutable>, fn: (this: ObjectLike, currentSubject: Subject) => S): Chainable<JQuery<S>>
|
||||
/**
|
||||
* Enables you to work with the subject yielded from the previous command / promise.
|
||||
*
|
||||
* @see https://on.cypress.io/then
|
||||
*/
|
||||
then<S extends ArrayLike<HTMLElement>>(options: Partial<Timeoutable>, fn: (this: ObjectLike, currentSubject: Subject) => S): Chainable<JQuery<S extends ArrayLike<infer T> ? T : never>>
|
||||
/**
|
||||
* Enables you to work with the subject yielded from the previous command / promise.
|
||||
*
|
||||
@@ -2754,7 +2748,7 @@ declare namespace Cypress {
|
||||
* To enable test retries only in runMode, set e.g. `{ openMode: null, runMode: 2 }`
|
||||
* @default null
|
||||
*/
|
||||
retries: Nullable<number | {runMode?: Nullable<number>, openMode?: Nullable<number>}>
|
||||
retries: Nullable<number | { runMode?: Nullable<number>, openMode?: Nullable<number> }>
|
||||
/**
|
||||
* Enables including elements within the shadow DOM when using querying
|
||||
* commands (e.g. cy.get(), cy.find()). Can be set globally in cypress.json,
|
||||
@@ -2891,7 +2885,7 @@ declare namespace Cypress {
|
||||
* All configuration items are optional.
|
||||
*/
|
||||
type CoreConfigOptions = Partial<Omit<ResolvedConfigOptions, TestingType>>
|
||||
type ConfigOptions = CoreConfigOptions & {e2e?: CoreConfigOptions, component?: CoreConfigOptions }
|
||||
type ConfigOptions = CoreConfigOptions & { e2e?: CoreConfigOptions, component?: CoreConfigOptions }
|
||||
|
||||
interface PluginConfigOptions extends ResolvedConfigOptions {
|
||||
/**
|
||||
@@ -2998,6 +2992,7 @@ declare namespace Cypress {
|
||||
disableTimersAndAnimations: boolean
|
||||
padding: Padding
|
||||
scale: boolean
|
||||
overwrite: boolean
|
||||
onBeforeScreenshot: ($el: JQuery) => void
|
||||
onAfterScreenshot: ($el: JQuery, props: {
|
||||
path: string
|
||||
@@ -5703,48 +5698,48 @@ declare namespace Cypress {
|
||||
}
|
||||
```
|
||||
*/
|
||||
interface cy extends Chainable<undefined> {}
|
||||
interface cy extends Chainable<undefined> { }
|
||||
}
|
||||
|
||||
declare namespace Mocha {
|
||||
interface TestFunction {
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test
|
||||
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: AsyncFunc): Test
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: AsyncFunc): Test
|
||||
}
|
||||
interface ExclusiveTestFunction {
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test
|
||||
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: AsyncFunc): Test
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: AsyncFunc): Test
|
||||
}
|
||||
interface PendingTestFunction {
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test
|
||||
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: AsyncFunc): Test
|
||||
/**
|
||||
* Describe a specification or test-case with the given `title`, TestOptions, and callback `fn` acting
|
||||
* as a thunk.
|
||||
*/
|
||||
(title: string, config: Cypress.TestConfigOverrides, fn?: AsyncFunc): Test
|
||||
}
|
||||
|
||||
interface SuiteFunction {
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
"private": false,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "yarn prepare-example && tsc -p ./tsconfig.json && node scripts/example copy-to ./dist/initial-template && yarn copy \"./src/**/*.template.js\" \"./dist/src\"",
|
||||
"build": "yarn prepare-example && tsc -p ./tsconfig.json && node scripts/example copy-to ./dist/initial-template && yarn prepare-copy-templates",
|
||||
"build-prod": "yarn build",
|
||||
"prepare-example": "node scripts/example copy-to ./initial-template",
|
||||
"prepare-copy-templates": "node scripts/copy-templates copy-to ./dist/src",
|
||||
"test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json './src/**/*.test.ts'",
|
||||
"test:watch": "yarn test -w"
|
||||
},
|
||||
@@ -20,6 +21,7 @@
|
||||
"chalk": "4.1.0",
|
||||
"cli-highlight": "2.1.10",
|
||||
"commander": "6.1.0",
|
||||
"fast-glob": "3.2.7",
|
||||
"find-up": "5.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob": "^7.1.6",
|
||||
|
||||
40
npm/create-cypress-tests/scripts/copy-templates.js
Normal file
40
npm/create-cypress-tests/scripts/copy-templates.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const fg = require('fast-glob')
|
||||
const fs = require('fs-extra')
|
||||
const chalk = require('chalk')
|
||||
const path = require('path')
|
||||
const program = require('commander')
|
||||
|
||||
program
|
||||
.command('copy-to [destination]')
|
||||
.description('copy ./src/**/*.template.js into destination')
|
||||
.action(async (destination) => {
|
||||
const srcPath = path.resolve(__dirname, '..', 'src')
|
||||
const destinationPath = path.resolve(process.cwd(), destination)
|
||||
|
||||
const templates = await fg('**/*.template.js', {
|
||||
cwd: srcPath,
|
||||
onlyFiles: true,
|
||||
unique: true,
|
||||
})
|
||||
|
||||
const srcOuput = './src/'
|
||||
let destinationOuput = destination.replace('/\\/g', '/')
|
||||
|
||||
if (!destinationOuput.endsWith('/')) {
|
||||
destinationOuput += '/'
|
||||
}
|
||||
|
||||
const relOutput = (template, forSource) => {
|
||||
return `${forSource ? srcOuput : destinationOuput}${template}`
|
||||
}
|
||||
|
||||
const result = await Promise.all(templates.map(async (template) => {
|
||||
await fs.copy(path.join(srcPath, template), path.join(destinationPath, template))
|
||||
|
||||
return () => console.log(`✅ ${relOutput(template, true)} successfully copied to ${chalk.cyan(relOutput(template, false))}`)
|
||||
}))
|
||||
|
||||
result.forEach((r) => r())
|
||||
})
|
||||
|
||||
program.parse(process.argv)
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Styles, standards, and components used throughout Cypress",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && yarn rollup -c rollup.config.js",
|
||||
"build": "rimraf dist && rollup -c rollup.config.js",
|
||||
"build-prod": "yarn build",
|
||||
"build-storybook": "build-storybook",
|
||||
"build-style-types": "tsm \"src/css/derived/*.scss\" --nameFormat none --exportType default",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Test React components using Cypress",
|
||||
"main": "dist/cypress-react.cjs.js",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && yarn transpile-plugins && yarn rollup -c rollup.config.js",
|
||||
"build": "rimraf dist && yarn transpile-plugins && rollup -c rollup.config.js",
|
||||
"build-prod": "yarn build",
|
||||
"cy:open": "node ../../scripts/cypress.js open-ct",
|
||||
"cy:open:debug": "node --inspect-brk ../../scripts/start.js --component-testing --run-project ${PWD}",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const debug = require('debug')('@cypress/react')
|
||||
const getNextJsBaseWebpackConfig = require('next/dist/build/webpack-config').default
|
||||
const { findPagesDir } = require('../../dist/next/findPagesDir')
|
||||
const { getRunWebpackSpan } = require('../../dist/next/getRunWebpackSpan')
|
||||
|
||||
async function getNextWebpackConfig (config) {
|
||||
let loadConfig
|
||||
@@ -20,6 +21,7 @@ async function getNextWebpackConfig (config) {
|
||||
}
|
||||
}
|
||||
const nextConfig = await loadConfig('development', config.projectRoot)
|
||||
const runWebpackSpan = await getRunWebpackSpan()
|
||||
const nextWebpackConfig = await getNextJsBaseWebpackConfig(
|
||||
config.projectRoot,
|
||||
{
|
||||
@@ -30,6 +32,7 @@ async function getNextWebpackConfig (config) {
|
||||
pagesDir: findPagesDir(config.projectRoot),
|
||||
entrypoints: {},
|
||||
rewrites: { fallback: [], afterFiles: [], beforeFiles: [] },
|
||||
...runWebpackSpan,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
16
npm/react/plugins/next/getRunWebpackSpan.ts
Normal file
16
npm/react/plugins/next/getRunWebpackSpan.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Span } from 'next/dist/telemetry/trace/trace'
|
||||
|
||||
// Starting with v11.1.1, a trace is required.
|
||||
// 'next/dist/telemetry/trace/trace' only exists since v10.0.9
|
||||
// and our peerDeps support back to v8 so try-catch this import
|
||||
export async function getRunWebpackSpan (): Promise<{ runWebpackSpan?: Span }> {
|
||||
let trace: (name: string) => Span
|
||||
|
||||
try {
|
||||
trace = await import('next/dist/telemetry/trace/trace').then((m) => m.trace)
|
||||
|
||||
return { runWebpackSpan: trace('cypress') }
|
||||
} catch (_) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Debug from 'debug'
|
||||
import { createServer, ViteDevServer, InlineConfig, UserConfig } from 'vite'
|
||||
import { createServer, ViteDevServer, InlineConfig } from 'vite'
|
||||
import { dirname, resolve } from 'path'
|
||||
import getPort from 'get-port'
|
||||
import { makeCypressPlugin } from './makeCypressPlugin'
|
||||
@@ -17,7 +17,7 @@ export interface StartDevServerOptions {
|
||||
* to override some options, you can do so using this.
|
||||
* @optional
|
||||
*/
|
||||
viteConfig?: Omit<UserConfig, 'base' | 'root'>
|
||||
viteConfig?: Omit<InlineConfig, 'base' | 'root'>
|
||||
}
|
||||
|
||||
const resolveServerConfig = async ({ viteConfig, options }: StartDevServerOptions): Promise<InlineConfig> => {
|
||||
@@ -52,12 +52,21 @@ const resolveServerConfig = async ({ viteConfig, options }: StartDevServerOption
|
||||
// Ask vite to pre-optimize all dependencies of the specs
|
||||
finalConfig.optimizeDeps = finalConfig.optimizeDeps || {}
|
||||
|
||||
// pre-optimizea all the specs
|
||||
// pre-optimize all the specs
|
||||
if ((options.specs && options.specs.length)) {
|
||||
finalConfig.optimizeDeps.entries = [...options.specs.map((spec) => spec.relative)]
|
||||
// fix: we must preserve entries configured on target project
|
||||
const existingOptimizeDepsEntries = finalConfig.optimizeDeps.entries
|
||||
|
||||
if (existingOptimizeDepsEntries) {
|
||||
finalConfig.optimizeDeps.entries = [...existingOptimizeDepsEntries, ...options.specs.map((spec) => spec.relative)]
|
||||
} else {
|
||||
finalConfig.optimizeDeps.entries = [...options.specs.map((spec) => spec.relative)]
|
||||
}
|
||||
|
||||
// only optimize a supportFile is it is not false or undefined
|
||||
if (supportFile) {
|
||||
finalConfig.optimizeDeps.entries.push(supportFile)
|
||||
// fix: on windows we need to replace backslashes with slashes
|
||||
finalConfig.optimizeDeps.entries.push(supportFile.replace(/\\/g, '/'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Browser-based Component Testing for Vue.js with Cypress.io ✌️🌲",
|
||||
"main": "dist/cypress-vue.cjs.js",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && yarn rollup -c rollup.config.js",
|
||||
"build": "rimraf dist && rollup -c rollup.config.js",
|
||||
"build-prod": "yarn build",
|
||||
"cy:open": "node ../../scripts/cypress.js open-ct --project ${PWD}",
|
||||
"cy:run": "node ../../scripts/cypress.js run-ct --project ${PWD}",
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cypress",
|
||||
"version": "8.4.1",
|
||||
"version": "8.5.0",
|
||||
"description": "Cypress.io end to end testing tool",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -64,12 +64,8 @@
|
||||
"type-check": "node scripts/type_check",
|
||||
"verify:mocha:results": "node ./scripts/verify_mocha_results",
|
||||
"prewatch": "yarn ensure-deps",
|
||||
"watch": "lerna exec yarn watch --parallel --stream"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
"watch": "lerna exec yarn watch --parallel --stream",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/bumpercar": "2.0.12",
|
||||
@@ -147,7 +143,7 @@
|
||||
"hasha": "5.0.0",
|
||||
"http-server": "0.12.3",
|
||||
"human-interval": "1.0.0",
|
||||
"husky": "2.4.1",
|
||||
"husky": "7.0.2",
|
||||
"inquirer": "3.3.0",
|
||||
"inquirer-confirm": "2.0.3",
|
||||
"jest": "24.9.0",
|
||||
@@ -157,7 +153,7 @@
|
||||
"konfig": "0.2.1",
|
||||
"lazy-ass": "1.6.0",
|
||||
"lerna": "3.20.2",
|
||||
"lint-staged": "11.0.0",
|
||||
"lint-staged": "11.1.2",
|
||||
"listr2": "3.8.3",
|
||||
"lodash": "4.17.21",
|
||||
"make-empty-github-commit": "cypress-io/make-empty-github-commit#4a592aedb776ba2f4cc88979055315a53eec42ee",
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
"requestTimeout": 5000,
|
||||
"resolvedNodePath": null,
|
||||
"resolvedNodeVersion": "1.2.3",
|
||||
"configFile": "cypress.json",
|
||||
"hosts": {
|
||||
"*.foobar.com": "127.0.0.1"
|
||||
},
|
||||
|
||||
@@ -326,9 +326,9 @@ describe('Settings', () => {
|
||||
|
||||
context('when configFile is false', () => {
|
||||
beforeEach(function () {
|
||||
this.openProject.resolve(Cypress._.assign({
|
||||
this.openProject.resolve(Cypress._.assign(this.config, {
|
||||
configFile: false,
|
||||
}, this.config))
|
||||
}))
|
||||
|
||||
this.goToSettings()
|
||||
|
||||
@@ -342,9 +342,9 @@ describe('Settings', () => {
|
||||
|
||||
context('when configFile is set', function () {
|
||||
beforeEach(function () {
|
||||
this.openProject.resolve(Cypress._.assign({
|
||||
this.openProject.resolve(Cypress._.assign(this.config, {
|
||||
configFile: 'special-cypress.json',
|
||||
}, this.config))
|
||||
}))
|
||||
|
||||
this.goToSettings()
|
||||
|
||||
@@ -358,34 +358,52 @@ describe('Settings', () => {
|
||||
})
|
||||
|
||||
describe('project id panel', () => {
|
||||
beforeEach(function () {
|
||||
this.openProject.resolve(this.config)
|
||||
this.projectStatuses[0].id = this.config.projectId
|
||||
this.getProjectStatus.resolve(this.projectStatuses[0])
|
||||
context('with json file', () => {
|
||||
beforeEach(function () {
|
||||
this.openProject.resolve(this.config)
|
||||
this.projectStatuses[0].id = this.config.projectId
|
||||
this.getProjectStatus.resolve(this.projectStatuses[0])
|
||||
|
||||
this.goToSettings()
|
||||
cy.contains('Project ID').click()
|
||||
this.goToSettings()
|
||||
cy.contains('Project ID').click()
|
||||
})
|
||||
|
||||
it('displays project id section', function () {
|
||||
cy.contains(this.config.projectId)
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('shows tooltip on hover of copy to clipboard', () => {
|
||||
cy.get('.action-copy').trigger('mouseover')
|
||||
cy.get('.cy-tooltip').should('contain', 'Copy to clipboard')
|
||||
})
|
||||
|
||||
it('copies project id config to clipboard', function () {
|
||||
cy.get('.action-copy').click()
|
||||
.then(() => {
|
||||
const expectedJsonConfig = {
|
||||
projectId: this.config.projectId,
|
||||
}
|
||||
const expectedCopyCommand = JSON.stringify(expectedJsonConfig, null, 2)
|
||||
|
||||
expect(this.ipc.setClipboardText).to.be.calledWith(expectedCopyCommand)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('displays project id section', function () {
|
||||
cy.contains(this.config.projectId)
|
||||
cy.percySnapshot()
|
||||
})
|
||||
context('with js file', () => {
|
||||
beforeEach(function () {
|
||||
this.openProject.resolve({ ...this.config, configFile: 'custom.cypress.js' })
|
||||
this.projectStatuses[0].id = this.config.projectId
|
||||
this.getProjectStatus.resolve(this.projectStatuses[0])
|
||||
|
||||
it('shows tooltip on hover of copy to clipboard', () => {
|
||||
cy.get('.action-copy').trigger('mouseover')
|
||||
cy.get('.cy-tooltip').should('contain', 'Copy to clipboard')
|
||||
})
|
||||
this.goToSettings()
|
||||
cy.contains('Project ID').click()
|
||||
})
|
||||
|
||||
it('copies project id config to clipboard', function () {
|
||||
cy.get('.action-copy').click()
|
||||
.then(() => {
|
||||
const expectedJsonConfig = {
|
||||
projectId: this.config.projectId,
|
||||
}
|
||||
const expectedCopyCommand = JSON.stringify(expectedJsonConfig, null, 2)
|
||||
|
||||
expect(this.ipc.setClipboardText).to.be.calledWith(expectedCopyCommand)
|
||||
it('displays project id section', function () {
|
||||
cy.get('[data-cy="project-id"] pre').contains('module.exports = {')
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -586,6 +604,17 @@ describe('Settings', () => {
|
||||
.should('contain', systemNodeVersion)
|
||||
.should('not.contain', bundledNodeVersion)
|
||||
})
|
||||
|
||||
it('should display an additional line when configFile is not JSON', function () {
|
||||
const configFile = 'notjson.js'
|
||||
|
||||
this.navigateWithConfig({
|
||||
configFile,
|
||||
})
|
||||
|
||||
cy.contains(`Node.js Version (${bundledNodeVersion})`).click()
|
||||
cy.get('.node-version li').should('contain', configFile)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxy settings panel', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ const onSubmitNewProject = function (orgId) {
|
||||
projectName: this.config.projectName,
|
||||
orgId,
|
||||
public: false,
|
||||
configFile: 'cypress.json',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -25,6 +26,7 @@ const onSubmitNewProject = function (orgId) {
|
||||
projectRoot: '/foo/bar',
|
||||
orgId,
|
||||
public: true,
|
||||
configFile: 'cypress.json',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -487,7 +489,10 @@ describe('Connect to Dashboard', function () {
|
||||
cy.get('.setup-project')
|
||||
.contains('.btn', 'Set up project').click()
|
||||
.then(() => {
|
||||
expect(this.ipc.setProjectId).to.be.calledWith({ id: this.dashboardProjects[1].id, projectRoot: '/foo/bar' })
|
||||
expect(this.ipc.setProjectId).to.be.calledWith({
|
||||
id: this.dashboardProjects[1].id,
|
||||
projectRoot: '/foo/bar',
|
||||
configFile: 'cypress.json' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ const setupDashboardProject = (projectDetails) => {
|
||||
.catch(ipc.isUnauthed, ipc.handleUnauthed)
|
||||
}
|
||||
|
||||
const setProjectId = (id, projectRoot) => {
|
||||
return ipc.setProjectId({ id, projectRoot })
|
||||
const setProjectId = (id, projectRoot, configFile) => {
|
||||
return ipc.setProjectId({ id, projectRoot, configFile })
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -6,10 +6,14 @@ const configFileFormatted = (configFile) => {
|
||||
return <><code>cypress.json</code> file (currently disabled by <code>--config-file false</code>)</>
|
||||
}
|
||||
|
||||
if (isUndefined(configFile) || configFile === 'cypress.json') {
|
||||
if (isUndefined(configFile)) {
|
||||
return <><code>cypress.json</code> file</>
|
||||
}
|
||||
|
||||
if (['cypress.json', 'cypress.config.js'].includes(configFile)) {
|
||||
return <><code>{configFile}</code> file</>
|
||||
}
|
||||
|
||||
return <>custom config file <code>{configFile}</code></>
|
||||
}
|
||||
|
||||
|
||||
@@ -103,3 +103,5 @@ export function stripSharedDirsFromDir2 (dir1, dir2, osName) {
|
||||
.join(sep)
|
||||
.value()
|
||||
}
|
||||
|
||||
export const isFileJSON = (file) => file && /\.json$/.test(file)
|
||||
|
||||
@@ -378,10 +378,13 @@ class SetupProject extends Component {
|
||||
projectRoot: this.props.project.path,
|
||||
orgId: this.state.selectedOrgId,
|
||||
public: this.state.public,
|
||||
configFile: this.props.project.configFile,
|
||||
})
|
||||
}
|
||||
|
||||
return dashboardProjectsApi.setProjectId(this.state.selectedProjectId, this.props.project.path)
|
||||
return dashboardProjectsApi.setProjectId(this.state.selectedProjectId,
|
||||
this.props.project.path,
|
||||
this.props.project.configFile)
|
||||
.then((id) => {
|
||||
const project = dashboardProjectsStore.getProjectById(id)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { observer } from 'mobx-react'
|
||||
import React from 'react'
|
||||
|
||||
import ipc from '../lib/ipc'
|
||||
import { isFileJSON } from '../lib/utils'
|
||||
import { configFileFormatted } from '../lib/config-file-formatted'
|
||||
|
||||
const openHelp = (e) => {
|
||||
e.preventDefault()
|
||||
@@ -89,6 +91,9 @@ const NodeVersion = observer(({ project }) => {
|
||||
<div className='well text-muted'>
|
||||
This Node.js version is used to:
|
||||
<ul>
|
||||
{isFileJSON(project.configFile)
|
||||
? undefined
|
||||
: <li>Execute code in the {configFileFormatted(project.configFile)}.</li>}
|
||||
<li>Build files in the {formatIntegrationFolder()} folder.</li>
|
||||
<li>Build files in the <code>cypress/support</code> folder.</li>
|
||||
<li>Execute code in the {formatPluginsFile() ? formatPluginsFile() : 'plugins'} file.</li>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import ipc from '../lib/ipc'
|
||||
import { isFileJSON } from '../lib/utils'
|
||||
import { configFileFormatted } from '../lib/config-file-formatted'
|
||||
|
||||
const openProjectIdHelp = (e) => {
|
||||
@@ -19,10 +20,6 @@ const openProjectIdHelp = (e) => {
|
||||
const ProjectId = observer(({ project }) => {
|
||||
if (!project.id) return null
|
||||
|
||||
const projectIdJsonConfig = {
|
||||
projectId: project.id,
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-cy="project-id">
|
||||
<a href='#' className='learn-more' onClick={openProjectIdHelp}>
|
||||
@@ -33,7 +30,7 @@ const ProjectId = observer(({ project }) => {
|
||||
It identifies your project and should not be changed.
|
||||
</p>
|
||||
<pre className='line-nums copy-to-clipboard'>
|
||||
<a className="action-copy" onClick={() => ipc.setClipboardText(JSON.stringify(projectIdJsonConfig, null, 2))}>
|
||||
<a className="action-copy" onClick={() => ipc.setClipboardText(document.querySelector('[data-cy="project-id"] pre').innerText)}>
|
||||
<Tooltip
|
||||
title='Copy to clipboard'
|
||||
placement='top'
|
||||
@@ -42,9 +39,20 @@ const ProjectId = observer(({ project }) => {
|
||||
<i className='fas fa-clipboard' />
|
||||
</Tooltip>
|
||||
</a>
|
||||
<span>{'{'}</span>
|
||||
<span>{` "projectId": "${project.id}"`}</span>
|
||||
<span>{'}'}</span>
|
||||
{
|
||||
isFileJSON(project.configFile) ?
|
||||
<>
|
||||
<span>{'{'}</span>
|
||||
<span>{` "projectId": "${project.id}"`}</span>
|
||||
<span>{'}'}</span>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<span>{'module.exports = {'}</span>
|
||||
<span>{` projectId: "${project.id}"`}</span>
|
||||
<span>{'}'}</span>
|
||||
</>
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -44,4 +44,14 @@ describe('ProjectId', () => {
|
||||
|
||||
cy.get('@externalOpen').should('have.been.called')
|
||||
})
|
||||
|
||||
it('shows a different output when configFile is js', () => {
|
||||
mount(<ProjectId project={{ ...project, configFile: 'cypress.config.js' }} />, {
|
||||
stylesheets: '/__root/dist/app.css',
|
||||
})
|
||||
|
||||
cy.get('[data-cy=project-id] pre').then(($pre) => {
|
||||
expect($pre.text()).to.contain('module.exports = ')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,6 +36,12 @@ describe('src/cy/commands/actions/select', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('selects by index', () => {
|
||||
cy.get('select[name=maps]').select(2).then(($select) => {
|
||||
expect($select).to.have.value('de_nuke')
|
||||
})
|
||||
})
|
||||
|
||||
it('selects by trimmed text with newlines stripped', () => {
|
||||
cy.get('select[name=maps]').select('italy').then(($select) => {
|
||||
expect($select).to.have.value('cs_italy')
|
||||
@@ -48,9 +54,15 @@ describe('src/cy/commands/actions/select', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('can handle valid index 0', () => {
|
||||
cy.get('select[name=maps]').select(0).then(($select) => {
|
||||
expect($select).to.have.value('de_dust2')
|
||||
})
|
||||
})
|
||||
|
||||
it('can select an array of values', () => {
|
||||
cy.get('select[name=movies]').select(['apoc', 'br']).then(($select) => {
|
||||
expect($select.val()).to.deep.eq(['apoc', 'br'])
|
||||
cy.get('select[name=movies]').select(['apoc', 'br', 'co']).then(($select) => {
|
||||
expect($select.val()).to.deep.eq(['apoc', 'br', 'co'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,6 +100,26 @@ describe('src/cy/commands/actions/select', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('can select an array of indexes', () => {
|
||||
cy.get('select[name=movies]').select([1, 5]).then(($select) => {
|
||||
expect($select.val()).to.deep.eq(['thc', 'twbb'])
|
||||
})
|
||||
})
|
||||
|
||||
it('can select an array of same value and index', () => {
|
||||
cy.get('select[name=movies]').select(['thc', 1]).then(($select) => {
|
||||
expect($select.val()).to.deep.eq(['thc'])
|
||||
})
|
||||
})
|
||||
|
||||
it('unselects all options if called with empty array', () => {
|
||||
cy.get('select[name=movies]').select(['apoc', 'br'])
|
||||
|
||||
cy.get('select[name=movies]').select([]).then(($select) => {
|
||||
expect($select.val()).to.deep.eq([])
|
||||
})
|
||||
})
|
||||
|
||||
// readonly should only be limited to inputs, not checkboxes
|
||||
it('can select a readonly select', () => {
|
||||
cy.get('select[name=hunter]').select('gon').then(($select) => {
|
||||
@@ -348,6 +380,39 @@ describe('src/cy/commands/actions/select', () => {
|
||||
cy.get('select').select('foo')
|
||||
})
|
||||
|
||||
it('throws when called with no arguments', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` must be passed a string, number, or array as its 1st argument. You passed: `undefined`.')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select[name=maps]').select()
|
||||
})
|
||||
|
||||
it('throws when called with null', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` must be passed a string, number, or array as its 1st argument. You passed: `null`.')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select[name=maps]').select(null)
|
||||
})
|
||||
|
||||
it('throws when called with invalid type', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` must be passed a string, number, or array as its 1st argument. You passed: `true`.')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select[name=foods]').select(true)
|
||||
})
|
||||
|
||||
it('throws on anything other than a select', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` can only be called on a `<select>`. Your subject is a: `<input id="input">`')
|
||||
@@ -359,6 +424,39 @@ describe('src/cy/commands/actions/select', () => {
|
||||
cy.get('input:first').select('foo')
|
||||
})
|
||||
|
||||
it('throws on negative index', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` was called with an invalid index: `-1`. Index must be a non-negative integer.')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select:first').select(-1)
|
||||
})
|
||||
|
||||
it('throws on non-integer index', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` was called with an invalid index: `1.5`. Index must be a non-negative integer.')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select:first').select(1.5)
|
||||
})
|
||||
|
||||
it('throws on out-of-range index', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` failed because it could not find a single `<option>` with value, index, or text matching: `3`')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select[name=foods]').select(3)
|
||||
})
|
||||
|
||||
it('throws when finding duplicate values', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` matched more than one `option` by value or text: `bm`')
|
||||
@@ -395,7 +493,7 @@ describe('src/cy/commands/actions/select', () => {
|
||||
|
||||
it('throws when value or text does not exist', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` failed because it could not find a single `<option>` with value or text matching: `foo`')
|
||||
expect(err.message).to.include('`cy.select()` failed because it could not find a single `<option>` with value, index, or text matching: `foo`')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
@@ -404,6 +502,28 @@ describe('src/cy/commands/actions/select', () => {
|
||||
cy.get('select[name=foods]').select('foo')
|
||||
})
|
||||
|
||||
it('throws invalid argument error when called with empty string', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` failed because it could not find a single `<option>` with value, index, or text matching: ``')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select[name=foods]').select('')
|
||||
})
|
||||
|
||||
it('throws invalid array argument error when called with invalid array', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` must be passed an array containing only strings and/or numbers. You passed: `[true,false]`')
|
||||
expect(err.docsUrl).to.eq('https://on.cypress.io/select')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('select[name=foods]').select([true, false])
|
||||
})
|
||||
|
||||
it('throws when the <select> itself is disabled', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('`cy.select()` failed because this element is currently disabled:')
|
||||
@@ -526,7 +646,7 @@ describe('src/cy/commands/actions/select', () => {
|
||||
done()
|
||||
})
|
||||
|
||||
cy.get('#select-maps').select('de_dust2').then(($select) => {})
|
||||
cy.get('#select-maps').select('de_dust2').then(($select) => { })
|
||||
})
|
||||
|
||||
it('snapshots after clicking', () => {
|
||||
|
||||
@@ -11,8 +11,7 @@ const testFail = (cb, expectedDocsUrl = 'https://on.cypress.io/intercept') => {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Network retries leak between tests, causing flake.
|
||||
describe('network stubbing', { retries: 2 }, function () {
|
||||
describe('network stubbing', function () {
|
||||
const { $, _, sinon, state, Promise } = Cypress
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
capture: 'viewport',
|
||||
screenshotOnRunFailure: true,
|
||||
disableTimersAndAnimations: true,
|
||||
overwrite: false,
|
||||
scale: true,
|
||||
blackout: ['.foo'],
|
||||
}
|
||||
@@ -135,6 +136,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
isOpen: true,
|
||||
appOnly: false,
|
||||
scale: true,
|
||||
overwrite: false,
|
||||
waitForCommandSynchronization: true,
|
||||
disableTimersAndAnimations: true,
|
||||
blackout: [],
|
||||
@@ -146,6 +148,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
isOpen: false,
|
||||
appOnly: false,
|
||||
scale: true,
|
||||
overwrite: false,
|
||||
waitForCommandSynchronization: true,
|
||||
disableTimersAndAnimations: true,
|
||||
blackout: [],
|
||||
@@ -276,7 +279,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('takes screenshot of hook title with test', () => {})
|
||||
it('takes screenshot of hook title with test', () => { })
|
||||
})
|
||||
|
||||
context('#screenshot', () => {
|
||||
@@ -411,6 +414,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
isOpen: true,
|
||||
appOnly: true,
|
||||
scale: true,
|
||||
overwrite: false,
|
||||
waitForCommandSynchronization: false,
|
||||
disableTimersAndAnimations: true,
|
||||
blackout: ['.foo'],
|
||||
@@ -431,6 +435,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
isOpen: false,
|
||||
appOnly: true,
|
||||
scale: true,
|
||||
overwrite: false,
|
||||
waitForCommandSynchronization: false,
|
||||
disableTimersAndAnimations: true,
|
||||
blackout: ['.foo'],
|
||||
@@ -453,6 +458,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
isOpen: true,
|
||||
appOnly: false,
|
||||
scale: true,
|
||||
overwrite: false,
|
||||
waitForCommandSynchronization: true,
|
||||
disableTimersAndAnimations: true,
|
||||
blackout: [],
|
||||
@@ -474,6 +480,7 @@ describe('src/cy/commands/screenshot', () => {
|
||||
isOpen: true,
|
||||
appOnly: true,
|
||||
scale: true,
|
||||
overwrite: false,
|
||||
waitForCommandSynchronization: false,
|
||||
disableTimersAndAnimations: true,
|
||||
blackout: ['.foo'],
|
||||
|
||||
@@ -2407,12 +2407,12 @@ describe('src/cy/commands/xhr', () => {
|
||||
it('logs response', () => {
|
||||
cy.then(function () {
|
||||
cy.wrap(this).its('lastLog').invoke('invoke', 'consoleProps').should((consoleProps) => {
|
||||
expect(consoleProps['Response Body']).to.deep.eq({
|
||||
expect(consoleProps['Response Body'].trim()).to.deep.eq(JSON.stringify({
|
||||
some: 'json',
|
||||
foo: {
|
||||
bar: 'baz',
|
||||
},
|
||||
})
|
||||
}, null, 2))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Proxy Logging', () => {
|
||||
const { _ } = Cypress
|
||||
|
||||
@@ -43,12 +45,6 @@ describe('Proxy Logging', () => {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// block race conditions caused by log update debouncing
|
||||
// @ts-ignore
|
||||
Cypress.config('logAttrsDelay', 0)
|
||||
})
|
||||
|
||||
context('request logging', () => {
|
||||
it('fetch log shows resource type, url, method, and status code and has expected snapshots and consoleProps', (done) => {
|
||||
fetch('/some-url')
|
||||
@@ -106,6 +102,43 @@ describe('Proxy Logging', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// @see https://github.com/cypress-io/cypress/issues/17656
|
||||
it('xhr log has response body/status code', (done) => {
|
||||
cy.window()
|
||||
.then((win) => {
|
||||
cy.on('log:changed', (log) => {
|
||||
try {
|
||||
expect(log.snapshots.map((v) => v.name)).to.deep.eq(['request', 'response'])
|
||||
expect(log.consoleProps['Response Headers']).to.include({
|
||||
'x-powered-by': 'Express',
|
||||
})
|
||||
|
||||
expect(log.consoleProps['Response Body']).to.include('Cannot GET /some-url')
|
||||
expect(log.consoleProps['Response Status Code']).to.eq(404)
|
||||
|
||||
expect(log.renderProps).to.include({
|
||||
indicator: 'bad',
|
||||
message: 'GET 404 /some-url',
|
||||
})
|
||||
|
||||
expect(Object.keys(log.consoleProps)).to.deep.eq(
|
||||
['Event', 'Resource Type', 'Method', 'URL', 'Request went to origin?', 'XHR', 'groups', 'Request Headers', 'Response Status Code', 'Response Headers', 'Response Body'],
|
||||
)
|
||||
|
||||
done()
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('assertion error, retrying', err)
|
||||
}
|
||||
})
|
||||
|
||||
const xhr = new win.XMLHttpRequest()
|
||||
|
||||
xhr.open('GET', '/some-url')
|
||||
xhr.send()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not log an unintercepted non-xhr/fetch request', (done) => {
|
||||
const img = new Image()
|
||||
const logs: any[] = []
|
||||
|
||||
@@ -175,6 +175,23 @@ describe('uncaught errors', () => {
|
||||
}).get('button:first').click()
|
||||
})
|
||||
|
||||
it('does not fail if thrown custom error with readonly name', (done) => {
|
||||
cy.once('fail', (err) => {
|
||||
expect(err.name).to.include('CustomError')
|
||||
expect(err.message).to.include('custom error')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
cy.then(() => {
|
||||
throw new class CustomError extends Error {
|
||||
get name () {
|
||||
return 'CustomError'
|
||||
}
|
||||
}('custom error')
|
||||
})
|
||||
})
|
||||
|
||||
it('fails test based on an uncaught error after last command and before completing', (done) => {
|
||||
cy.on('fail', () => {
|
||||
done()
|
||||
|
||||
@@ -10,7 +10,23 @@ const newLineRe = /\n/g
|
||||
|
||||
export default (Commands, Cypress, cy) => {
|
||||
Commands.addAll({ prevSubject: 'element' }, {
|
||||
select (subject, valueOrText, options = {}) {
|
||||
select (subject, valueOrTextOrIndex, options = {}) {
|
||||
if (
|
||||
!_.isNumber(valueOrTextOrIndex)
|
||||
&& !_.isString(valueOrTextOrIndex)
|
||||
&& !_.isArray(valueOrTextOrIndex)
|
||||
) {
|
||||
$errUtils.throwErrByPath('select.invalid_argument', { args: { value: JSON.stringify(valueOrTextOrIndex) } })
|
||||
}
|
||||
|
||||
if (
|
||||
_.isArray(valueOrTextOrIndex)
|
||||
&& valueOrTextOrIndex.length > 0
|
||||
&& !_.some(valueOrTextOrIndex, (val) => _.isNumber(val) || _.isString(val))
|
||||
) {
|
||||
$errUtils.throwErrByPath('select.invalid_array_argument', { args: { value: JSON.stringify(valueOrTextOrIndex) } })
|
||||
}
|
||||
|
||||
const userOptions = options
|
||||
|
||||
options = _.defaults({}, userOptions, {
|
||||
@@ -63,19 +79,23 @@ export default (Commands, Cypress, cy) => {
|
||||
$errUtils.throwErrByPath('select.multiple_elements', { args: { num: options.$el.length } })
|
||||
}
|
||||
|
||||
// normalize valueOrText if its not an array
|
||||
valueOrText = [].concat(valueOrText).map((v) => {
|
||||
// normalize valueOrTextOrIndex if its not an array
|
||||
valueOrTextOrIndex = [].concat(valueOrTextOrIndex).map((v) => {
|
||||
if (_.isNumber(v) && (!_.isInteger(v) || v < 0)) {
|
||||
$errUtils.throwErrByPath('select.invalid_number', { args: { index: v } })
|
||||
}
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/16045
|
||||
// replace ` ` in the text to `\us00a0` to find match.
|
||||
// @see https://stackoverflow.com/a/53306311/1038927
|
||||
return v.replace(/ /g, '\u00a0')
|
||||
return _.isNumber(v) ? v : v.replace(/ /g, '\u00a0')
|
||||
})
|
||||
|
||||
const multiple = options.$el.prop('multiple')
|
||||
|
||||
// throw if we're not a multiple select and we've
|
||||
// passed an array of values
|
||||
if (!multiple && valueOrText.length > 1) {
|
||||
if (!multiple && valueOrTextOrIndex.length > 1) {
|
||||
$errUtils.throwErrByPath('select.invalid_multiple')
|
||||
}
|
||||
|
||||
@@ -96,7 +116,7 @@ export default (Commands, Cypress, cy) => {
|
||||
const value = $elements.getNativeProp(el, 'value')
|
||||
const optEl = $dom.wrap(el)
|
||||
|
||||
if (valueOrText.includes(value)) {
|
||||
if (valueOrTextOrIndex.includes(value) || valueOrTextOrIndex.includes(index)) {
|
||||
optionEls.push(optEl)
|
||||
values.push(value)
|
||||
}
|
||||
@@ -124,7 +144,7 @@ export default (Commands, Cypress, cy) => {
|
||||
notAllUniqueValues = uniqueValues.length !== optionsObjects.length
|
||||
|
||||
_.each(optionsObjects, (obj) => {
|
||||
if (valueOrText.includes(obj.text)) {
|
||||
if (valueOrTextOrIndex.includes(obj.text)) {
|
||||
optionEls.push(obj.$el)
|
||||
const objValue = obj.value
|
||||
|
||||
@@ -137,13 +157,13 @@ export default (Commands, Cypress, cy) => {
|
||||
// we have more than 1 option to set then blow up
|
||||
if (!multiple && (values.length > 1)) {
|
||||
$errUtils.throwErrByPath('select.multiple_matches', {
|
||||
args: { value: valueOrText.join(', ') },
|
||||
args: { value: valueOrTextOrIndex.join(', ') },
|
||||
})
|
||||
}
|
||||
|
||||
if (!values.length) {
|
||||
if (!values.length && !(_.isArray(valueOrTextOrIndex) && valueOrTextOrIndex.length === 0)) {
|
||||
$errUtils.throwErrByPath('select.no_matches', {
|
||||
args: { value: valueOrText.join(', ') },
|
||||
args: { value: valueOrTextOrIndex.join(', ') },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options = {}) => {
|
||||
capture,
|
||||
padding,
|
||||
clip,
|
||||
overwrite,
|
||||
disableTimersAndAnimations,
|
||||
onBeforeScreenshot,
|
||||
onAfterScreenshot,
|
||||
@@ -313,6 +314,7 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options = {}) => {
|
||||
waitForCommandSynchronization: !isAppOnly(screenshotConfig),
|
||||
disableTimersAndAnimations,
|
||||
blackout: getBlackout(screenshotConfig),
|
||||
overwrite,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +355,7 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options = {}) => {
|
||||
},
|
||||
scaled: getShouldScale(screenshotConfig),
|
||||
blackout: getBlackout(screenshotConfig),
|
||||
overwrite,
|
||||
startTime: startTime.toISOString(),
|
||||
})
|
||||
|
||||
@@ -450,7 +453,7 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
|
||||
const isWin = $dom.isWindow(subject)
|
||||
|
||||
let screenshotConfig = _.pick(options, 'capture', 'scale', 'disableTimersAndAnimations', 'blackout', 'waitForCommandSynchronization', 'padding', 'clip', 'onBeforeScreenshot', 'onAfterScreenshot')
|
||||
let screenshotConfig = _.pick(options, 'capture', 'scale', 'disableTimersAndAnimations', 'overwrite', 'blackout', 'waitForCommandSynchronization', 'padding', 'clip', 'onBeforeScreenshot', 'onAfterScreenshot')
|
||||
|
||||
screenshotConfig = $Screenshot.validate(screenshotConfig, 'screenshot', options._log)
|
||||
screenshotConfig = _.extend($Screenshot.getConfig(), screenshotConfig)
|
||||
|
||||
@@ -343,9 +343,10 @@ export default {
|
||||
|
||||
// since this failed this means that a specific command failed
|
||||
// and we should highlight it in red or insert a new command
|
||||
if (_.isObject(err)) {
|
||||
// @ts-ignore
|
||||
if (_.isObject(err) && !err.name) {
|
||||
// @ts-ignore
|
||||
err.name = err.name || 'CypressError'
|
||||
err.name = 'CypressError'
|
||||
}
|
||||
|
||||
commandRunningFailed(Cypress, state, err)
|
||||
|
||||
@@ -1095,11 +1095,7 @@ export default {
|
||||
message: stripIndent`\
|
||||
${cmd('request')} was invoked with \`{ failOnStatusCode: false, retryOnStatusCodeFailure: true }\`.
|
||||
|
||||
These options are incompatible with each other.
|
||||
|
||||
- To retry on non-2xx status codes, pass \`{ failOnStatusCode: true, retryOnStatusCodeFailure: true }\`.
|
||||
- To not retry on non-2xx status codes, pass \`{ failOnStatusCode: true, retryOnStatusCodeFailure: true }\`.
|
||||
- To fail on non-2xx status codes without retrying (the default behavior), pass \`{ failOnStatusCode: true, retryOnStatusCodeFailure: false }\``,
|
||||
\`failOnStatusCode\` must be \`true\` if \`retryOnStatusCodeFailure\` is \`true\`.`,
|
||||
docsUrl: 'https://on.cypress.io/request',
|
||||
},
|
||||
auth_invalid: {
|
||||
@@ -1148,9 +1144,9 @@ export default {
|
||||
The request we sent was:
|
||||
|
||||
${getHttpProps([
|
||||
{ key: 'method', value: obj.method },
|
||||
{ key: 'URL', value: obj.url },
|
||||
])}
|
||||
{ key: 'method', value: obj.method },
|
||||
{ key: 'URL', value: obj.url },
|
||||
])}
|
||||
|
||||
${divider(60, '-')}
|
||||
|
||||
@@ -1182,22 +1178,22 @@ export default {
|
||||
The request we sent was:
|
||||
|
||||
${getHttpProps([
|
||||
{ key: 'method', value: obj.method },
|
||||
{ key: 'URL', value: obj.url },
|
||||
{ key: 'headers', value: obj.requestHeaders },
|
||||
{ key: 'body', value: obj.requestBody },
|
||||
{ key: 'redirects', value: obj.redirects },
|
||||
])}
|
||||
{ key: 'method', value: obj.method },
|
||||
{ key: 'URL', value: obj.url },
|
||||
{ key: 'headers', value: obj.requestHeaders },
|
||||
{ key: 'body', value: obj.requestBody },
|
||||
{ key: 'redirects', value: obj.redirects },
|
||||
])}
|
||||
|
||||
${divider(60, '-')}
|
||||
|
||||
The response we got was:
|
||||
|
||||
${getHttpProps([
|
||||
{ key: 'status', value: `${obj.status} - ${obj.statusText}` },
|
||||
{ key: 'headers', value: obj.responseHeaders },
|
||||
{ key: 'body', value: obj.responseBody },
|
||||
])}
|
||||
{ key: 'status', value: `${obj.status} - ${obj.statusText}` },
|
||||
{ key: 'headers', value: obj.responseHeaders },
|
||||
{ key: 'body', value: obj.responseBody },
|
||||
])}
|
||||
`, 10),
|
||||
docsUrl: 'https://on.cypress.io/request',
|
||||
}
|
||||
@@ -1210,9 +1206,9 @@ export default {
|
||||
The request we sent was:
|
||||
|
||||
${getHttpProps([
|
||||
{ key: 'method', value: obj.method },
|
||||
{ key: 'URL', value: obj.url },
|
||||
])}
|
||||
{ key: 'method', value: obj.method },
|
||||
{ key: 'URL', value: obj.url },
|
||||
])}
|
||||
|
||||
No response was received within the timeout.`, 10),
|
||||
docsUrl: 'https://on.cypress.io/request',
|
||||
@@ -1381,6 +1377,14 @@ export default {
|
||||
},
|
||||
|
||||
select: {
|
||||
invalid_argument: {
|
||||
message: `${cmd('select')} must be passed a string, number, or array as its 1st argument. You passed: \`{{value}}\`.`,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
},
|
||||
invalid_array_argument: {
|
||||
message: `${cmd('select')} must be passed an array containing only strings and/or numbers. You passed: \`{{value}}\`.`,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
},
|
||||
disabled: {
|
||||
message: `${cmd('select')} failed because this element is currently disabled:\n\n\`{{node}}\``,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
@@ -1393,6 +1397,10 @@ export default {
|
||||
message: `${cmd('select')} was called with an array of arguments but does not have a \`multiple\` attribute set.`,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
},
|
||||
invalid_number: {
|
||||
message: `${cmd('select')} was called with an invalid index: \`{{index}}\`. Index must be a non-negative integer.`,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
},
|
||||
multiple_elements: {
|
||||
message: `${cmd('select')} can only be called on a single \`<select>\`. Your subject contained {{num}} elements.`,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
@@ -1402,7 +1410,7 @@ export default {
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
},
|
||||
no_matches: {
|
||||
message: `${cmd('select')} failed because it could not find a single \`<option>\` with value or text matching: \`{{value}}\``,
|
||||
message: `${cmd('select')} failed because it could not find a single \`<option>\` with value, index, or text matching: \`{{value}}\``,
|
||||
docsUrl: 'https://on.cypress.io/select',
|
||||
},
|
||||
option_disabled: {
|
||||
|
||||
@@ -18,7 +18,6 @@ const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewp
|
||||
const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ')
|
||||
const BLACKLIST_PROPS = 'snapshots'.split(' ')
|
||||
|
||||
let delay = null
|
||||
let counter = 0
|
||||
|
||||
const { HIGHLIGHT_ATTR } = $Snapshots
|
||||
@@ -113,10 +112,6 @@ const setCounter = (num) => {
|
||||
return counter = num
|
||||
}
|
||||
|
||||
const setDelay = (val) => {
|
||||
return delay = val != null ? val : 4
|
||||
}
|
||||
|
||||
const defaults = function (state, config, obj) {
|
||||
const instrument = obj.instrument != null ? obj.instrument : 'command'
|
||||
|
||||
@@ -523,12 +518,6 @@ export default {
|
||||
counter = 0
|
||||
const logs = {}
|
||||
|
||||
// give us the ability to change the delay for firing
|
||||
// the change event, or default it to 4
|
||||
if (delay == null) {
|
||||
delay = setDelay(config('logAttrsDelay'))
|
||||
}
|
||||
|
||||
const trigger = function (log, event) {
|
||||
// bail if we never fired our initial log event
|
||||
if (!log._hasInitiallyLogged) {
|
||||
|
||||
@@ -87,66 +87,7 @@ function getRequestLogConfig (req: Omit<ProxyRequest, 'log'>): Partial<Cypress.L
|
||||
url: req.preRequest.url,
|
||||
method: req.preRequest.method,
|
||||
timeout: 0,
|
||||
consoleProps: () => {
|
||||
// high-level request information
|
||||
const consoleProps = {
|
||||
'Resource Type': req.preRequest.resourceType,
|
||||
Method: req.preRequest.method,
|
||||
URL: req.preRequest.url,
|
||||
'Request went to origin?': req.flags.stubbed ? 'no (response was stubbed, see below)' : 'yes',
|
||||
}
|
||||
|
||||
if (req.flags.reqModified) consoleProps['Request modified?'] = 'yes'
|
||||
|
||||
if (req.flags.resModified) consoleProps['Response modified?'] = 'yes'
|
||||
|
||||
// details on matched XHR/intercept
|
||||
if (req.xhr) consoleProps['XHR'] = req.xhr.xhr
|
||||
|
||||
if (req.interceptions.length) {
|
||||
if (req.interceptions.length > 1) {
|
||||
consoleProps['Matched `cy.intercept()`s'] = req.interceptions.map(formatInterception)
|
||||
} else {
|
||||
consoleProps['Matched `cy.intercept()`'] = formatInterception(req.interceptions[0])
|
||||
}
|
||||
}
|
||||
|
||||
if (req.stack) {
|
||||
consoleProps['groups'] = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Initiator',
|
||||
items: [req.stack],
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// details on request/response/errors
|
||||
consoleProps['Request Headers'] = req.preRequest.headers
|
||||
|
||||
if (req.responseReceived) {
|
||||
_.assign(consoleProps, {
|
||||
'Response Status Code': req.responseReceived.status,
|
||||
'Response Headers': req.responseReceived.headers,
|
||||
})
|
||||
}
|
||||
|
||||
let resBody
|
||||
|
||||
if (req.xhr) {
|
||||
consoleProps['Response Body'] = req.xhr.responseBody
|
||||
} else if ((resBody = _.chain(req.interceptions).last().get('interception.response.body').value())) {
|
||||
consoleProps['Response Body'] = resBody
|
||||
}
|
||||
|
||||
if (req.error) {
|
||||
consoleProps['Error'] = req.error
|
||||
}
|
||||
|
||||
return consoleProps
|
||||
},
|
||||
consoleProps: () => req.consoleProps,
|
||||
renderProps: () => {
|
||||
function getIndicator (): 'aborted' | 'pending' | 'successful' | 'bad' {
|
||||
if (!req.responseReceived) {
|
||||
@@ -211,9 +152,89 @@ class ProxyRequest {
|
||||
resModified?: boolean
|
||||
} = {}
|
||||
|
||||
// constant reference to consoleProps so changes reach the console
|
||||
// @see https://github.com/cypress-io/cypress/issues/17656
|
||||
readonly consoleProps: any
|
||||
|
||||
constructor (preRequest: BrowserPreRequest, opts?: Partial<ProxyRequest>) {
|
||||
this.preRequest = preRequest
|
||||
opts && _.assign(this, opts)
|
||||
|
||||
// high-level request information
|
||||
this.consoleProps = {
|
||||
'Resource Type': preRequest.resourceType,
|
||||
Method: preRequest.method,
|
||||
URL: preRequest.url,
|
||||
}
|
||||
|
||||
this.updateConsoleProps()
|
||||
}
|
||||
|
||||
updateConsoleProps () {
|
||||
const { consoleProps } = this
|
||||
|
||||
consoleProps['Request went to origin?'] = this.flags.stubbed ? 'no (response was stubbed, see below)' : 'yes'
|
||||
|
||||
if (this.flags.reqModified) consoleProps['Request modified?'] = 'yes'
|
||||
|
||||
if (this.flags.resModified) consoleProps['Response modified?'] = 'yes'
|
||||
|
||||
// details on matched XHR/intercept
|
||||
if (this.xhr) consoleProps['XHR'] = this.xhr.xhr
|
||||
|
||||
if (this.interceptions.length) {
|
||||
if (this.interceptions.length > 1) {
|
||||
consoleProps['Matched `cy.intercept()`s'] = this.interceptions.map(formatInterception)
|
||||
} else {
|
||||
consoleProps['Matched `cy.intercept()`'] = formatInterception(this.interceptions[0])
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stack) {
|
||||
consoleProps['groups'] = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Initiator',
|
||||
items: [this.stack],
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ensure these fields are always ordered correctly regardless of when they are added
|
||||
['Response Status Code', 'Response Headers', 'Response Body', 'Request Headers', 'Request Body'].forEach((k) => delete consoleProps[k])
|
||||
|
||||
// details on request
|
||||
consoleProps['Request Headers'] = this.preRequest.headers
|
||||
|
||||
const reqBody = _.chain(this.interceptions).last().get('interception.request.body').value()
|
||||
|
||||
if (reqBody) consoleProps['Request Body'] = reqBody
|
||||
|
||||
if (this.responseReceived) {
|
||||
_.assign(consoleProps, {
|
||||
'Response Status Code': this.responseReceived.status,
|
||||
'Response Headers': this.responseReceived.headers,
|
||||
})
|
||||
}
|
||||
|
||||
// details on response
|
||||
let resBody
|
||||
|
||||
if (this.xhr) {
|
||||
if (!consoleProps['Response Headers']) consoleProps['Response Headers'] = this.xhr.responseHeaders
|
||||
|
||||
if (!consoleProps['Response Status Code']) consoleProps['Response Status Code'] = this.xhr.xhr.status
|
||||
|
||||
consoleProps['Response Body'] = this.xhr.xhr.response
|
||||
} else if ((resBody = _.chain(this.interceptions).last().get('interception.response.body').value())) {
|
||||
consoleProps['Response Body'] = resBody
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
consoleProps['Error'] = this.error
|
||||
}
|
||||
}
|
||||
|
||||
setFlag = (flag: keyof ProxyRequest['flags']) => {
|
||||
@@ -310,7 +331,15 @@ export default class ProxyLogging {
|
||||
}
|
||||
|
||||
proxyRequest.responseReceived = responseReceived
|
||||
proxyRequest.log?.snapshot('response').end()
|
||||
|
||||
proxyRequest.updateConsoleProps()
|
||||
|
||||
// @ts-ignore
|
||||
const hasResponseSnapshot = proxyRequest.log?.get('snapshots')?.find((v) => v.name === 'response')
|
||||
|
||||
if (!hasResponseSnapshot) proxyRequest.log?.snapshot('response')
|
||||
|
||||
proxyRequest.log?.end()
|
||||
}
|
||||
|
||||
private updateRequestWithError (error: RequestError): void {
|
||||
@@ -321,6 +350,7 @@ export default class ProxyLogging {
|
||||
}
|
||||
|
||||
proxyRequest.error = $errUtils.makeErrFromObj(error.error)
|
||||
proxyRequest.updateConsoleProps()
|
||||
proxyRequest.log?.snapshot('error').error(proxyRequest.error)
|
||||
}
|
||||
|
||||
|
||||
@@ -133,9 +133,9 @@ const validate = (props, cmd, log) => {
|
||||
values.capture = capture
|
||||
}
|
||||
|
||||
validateAndSetBoolean(props, values, cmd, log, 'scale')
|
||||
validateAndSetBoolean(props, values, cmd, log, 'disableTimersAndAnimations')
|
||||
validateAndSetBoolean(props, values, cmd, log, 'screenshotOnRunFailure')
|
||||
['scale', 'disableTimersAndAnimations', 'screenshotOnRunFailure', 'overwrite'].forEach((key) => {
|
||||
validateAndSetBoolean(props, values, cmd, log, key)
|
||||
})
|
||||
|
||||
if (blackout) {
|
||||
const existsNonString = _.some(blackout, (selector) => {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"minimist": "1.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "13.2.0",
|
||||
"electron": "14.1.0",
|
||||
"execa": "4.1.0",
|
||||
"mocha": "3.5.3"
|
||||
},
|
||||
|
||||
@@ -151,4 +151,13 @@ We found an invalid value in the file: \`cypress.json\`
|
||||
|
||||
Found an error while validating the \`browsers\` list. Expected \`family\` to be either chromium or firefox. Instead the value was: \`{"name":"bad browser","family":"unknown family","displayName":"Bad browser","version":"no version","path":"/path/to","majorVersion":123}\`
|
||||
|
||||
`
|
||||
|
||||
exports['e2e config throws error when multiple default config file are found in project 1'] = `
|
||||
There is both a \`cypress.config.js\` and a \`cypress.config.ts\` at the location below:
|
||||
/foo/bar/.projects/pristine
|
||||
|
||||
Cypress does not know which one to read for config. Please remove one of the two and try again.
|
||||
|
||||
|
||||
`
|
||||
|
||||
@@ -21,3 +21,11 @@ You passed: xyz
|
||||
|
||||
The error was: Cannot read property 'split' of undefined
|
||||
`
|
||||
|
||||
exports['invalid spec error'] = `
|
||||
Cypress encountered an error while parsing the argument spec
|
||||
|
||||
You passed: [object Object]
|
||||
|
||||
The error was: spec must be a string or comma-separated list
|
||||
`
|
||||
|
||||
@@ -9,7 +9,7 @@ import scaffold from './scaffold'
|
||||
import { fs } from './util/fs'
|
||||
import keys from './util/keys'
|
||||
import origin from './util/origin'
|
||||
import settings from './util/settings'
|
||||
import * as settings from './util/settings'
|
||||
import Debug from 'debug'
|
||||
import pathHelpers from './util/path_helpers'
|
||||
import findSystemNode from './util/find_system_node'
|
||||
|
||||
1
packages/server/lib/configFiles.ts
Normal file
1
packages/server/lib/configFiles.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const CYPRESS_CONFIG_FILES = ['cypress.json', 'cypress.config.js', 'cypress.config.ts']
|
||||
@@ -694,6 +694,20 @@ const getMsgByType = function (type, arg1 = {}, arg2, arg3) {
|
||||
${chalk.yellow(arg1.error)}
|
||||
|
||||
Learn more at https://on.cypress.io/reporters`
|
||||
// TODO: update with vetted cypress language
|
||||
case 'NO_DEFAULT_CONFIG_FILE_FOUND':
|
||||
return stripIndent`\
|
||||
Could not find a Cypress configuration file, exiting.
|
||||
|
||||
We looked but did not find a default config file in this folder: ${chalk.blue(arg1)}`
|
||||
// TODO: update with vetted cypress language
|
||||
case 'CONFIG_FILES_LANGUAGE_CONFLICT':
|
||||
return stripIndent`
|
||||
There is both a \`${arg2}\` and a \`${arg3}\` at the location below:
|
||||
${arg1}
|
||||
|
||||
Cypress does not know which one to read for config. Please remove one of the two and try again.
|
||||
`
|
||||
case 'CONFIG_FILE_NOT_FOUND':
|
||||
return stripIndent`\
|
||||
Could not find a Cypress configuration file, exiting.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const ipc = require('electron').ipcMain
|
||||
const { clipboard } = require('electron')
|
||||
const debug = require('debug')('cypress:server:events')
|
||||
@@ -323,6 +324,20 @@ const handleEvent = function (options, bus, event, id, type, arg) {
|
||||
onWarning,
|
||||
})
|
||||
}).call('getConfig')
|
||||
.then((config) => {
|
||||
if (config.configFile && path.isAbsolute(config.configFile)) {
|
||||
config.configFile = path.relative(arg, config.configFile)
|
||||
}
|
||||
|
||||
// those two values make no sense to display in
|
||||
// the GUI
|
||||
if (config.resolved) {
|
||||
config.resolved.configFile = undefined
|
||||
config.resolved.testingType = undefined
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
.then(send)
|
||||
.catch(sendErr)
|
||||
|
||||
@@ -337,7 +352,7 @@ const handleEvent = function (options, bus, event, id, type, arg) {
|
||||
.catch(sendErr)
|
||||
|
||||
case 'set:project:id':
|
||||
return ProjectStatic.writeProjectId(arg.id, arg.projectRoot)
|
||||
return ProjectStatic.writeProjectId(arg)
|
||||
.then(send)
|
||||
.catch(sendErr)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ const logSymbols = require('log-symbols')
|
||||
|
||||
const recordMode = require('./record')
|
||||
const errors = require('../errors')
|
||||
const ProjectStatic = require('../project_static')
|
||||
const Reporter = require('../reporter')
|
||||
const browserUtils = require('../browsers')
|
||||
const { openProject } = require('../open_project')
|
||||
@@ -609,22 +608,21 @@ const openProjectCreate = (projectRoot, socketId, args) => {
|
||||
return openProject.create(projectRoot, args, options)
|
||||
}
|
||||
|
||||
const createAndOpenProject = function (socketId, options) {
|
||||
const createAndOpenProject = async function (socketId, options) {
|
||||
const { projectRoot, projectId } = options
|
||||
|
||||
return ProjectStatic.ensureExists(projectRoot, options)
|
||||
.then(() => {
|
||||
// open this project without
|
||||
// adding it to the global cache
|
||||
return openProjectCreate(projectRoot, socketId, options)
|
||||
})
|
||||
.call('getProject')
|
||||
return openProjectCreate(projectRoot, socketId, options)
|
||||
.then((open_project) => open_project.getProject())
|
||||
.then((project) => {
|
||||
return Promise.props({
|
||||
return Promise.all([
|
||||
project,
|
||||
config: project.getConfig(),
|
||||
projectId: getProjectId(project, projectId),
|
||||
})
|
||||
project.getConfig(),
|
||||
getProjectId(project, projectId),
|
||||
]).then(([project, config, projectId]) => ({
|
||||
project,
|
||||
config,
|
||||
projectId,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface LaunchArgs {
|
||||
_: [string] // Cypress App binary location
|
||||
config: Record<string, unknown>
|
||||
cwd: string
|
||||
browser: Browser
|
||||
browser?: Browser['name']
|
||||
configFile?: string
|
||||
project: string // projectRoot
|
||||
projectRoot: string // same as above
|
||||
@@ -46,6 +46,12 @@ export interface LaunchArgs {
|
||||
os: PlatformName
|
||||
|
||||
onFocusTests?: () => any
|
||||
/**
|
||||
* in run mode, the path of the project run
|
||||
* path is relative if specified with --project,
|
||||
* absolute if implied by current working directory
|
||||
*/
|
||||
runProject?: string
|
||||
}
|
||||
|
||||
// @see https://github.com/cypress-io/cypress/issues/18094
|
||||
@@ -456,7 +462,7 @@ export class OpenProject {
|
||||
try {
|
||||
await this.openProject.initializeConfig()
|
||||
await this.openProject.open()
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.isCypressErr && err.portInUse) {
|
||||
errors.throw(err.type, err.port)
|
||||
} else {
|
||||
|
||||
@@ -7,11 +7,11 @@ const Promise = require('bluebird')
|
||||
const preprocessor = require('./preprocessor')
|
||||
const devServer = require('./dev-server')
|
||||
const resolve = require('../../util/resolve')
|
||||
const tsNodeUtil = require('../../util/ts_node')
|
||||
const browserLaunch = require('./browser_launch')
|
||||
const task = require('./task')
|
||||
const util = require('../util')
|
||||
const validateEvent = require('./validate_event')
|
||||
const tsNodeUtil = require('./ts_node')
|
||||
|
||||
let registeredEventsById = {}
|
||||
let registeredEventsByName = {}
|
||||
|
||||
@@ -23,14 +23,14 @@ import system from './util/system'
|
||||
import user from './user'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
import { fs } from './util/fs'
|
||||
import settings from './util/settings'
|
||||
import * as settings from './util/settings'
|
||||
import plugins from './plugins'
|
||||
import specsUtil from './util/specs'
|
||||
import Watchers from './watchers'
|
||||
import devServer from './plugins/dev-server'
|
||||
import preprocessor from './plugins/preprocessor'
|
||||
import { SpecsStore } from './specs-store'
|
||||
import { checkSupportFile } from './project_utils'
|
||||
import { checkSupportFile, getDefaultConfigFilePath } from './project_utils'
|
||||
import type { LaunchArgs } from './open_project'
|
||||
|
||||
// Cannot just use RuntimeConfigOptions as is because some types are not complete.
|
||||
@@ -54,7 +54,7 @@ type WebSocketOptionsCallback = (...args: any[]) => any
|
||||
export interface OpenProjectLaunchOptions {
|
||||
args?: LaunchArgs
|
||||
|
||||
configFile?: string | boolean
|
||||
configFile?: string | false
|
||||
browsers?: Cypress.Browser[]
|
||||
|
||||
// Callback to reload the Desktop GUI when cypress.json is changed.
|
||||
@@ -516,7 +516,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
projectRoot,
|
||||
}: {
|
||||
projectRoot: string
|
||||
configFile?: string | boolean
|
||||
configFile?: string | false
|
||||
onSettingsChanged?: false | (() => void)
|
||||
}) {
|
||||
// bail if we havent been told to
|
||||
@@ -690,6 +690,12 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
}
|
||||
|
||||
async initializeConfig (): Promise<Cfg> {
|
||||
// set default for "configFile" if undefined
|
||||
if (this.options.configFile === undefined
|
||||
|| this.options.configFile === null) {
|
||||
this.options.configFile = await getDefaultConfigFilePath(this.projectRoot, !this.options.args?.runProject)
|
||||
}
|
||||
|
||||
let theCfg: Cfg = await config.get(this.projectRoot, this.options)
|
||||
|
||||
if (theCfg.browsers) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import api from './api'
|
||||
import cache from './cache'
|
||||
import user from './user'
|
||||
import keys from './util/keys'
|
||||
import settings from './util/settings'
|
||||
import * as settings from './util/settings'
|
||||
import { ProjectBase } from './project-base'
|
||||
import { getDefaultConfigFilePath } from './project_utils'
|
||||
|
||||
const debug = Debug('cypress:server:project_static')
|
||||
|
||||
@@ -60,7 +61,7 @@ export async function _getProject (clientProject, authToken) {
|
||||
debug('got project from api')
|
||||
|
||||
return _mergeDetails(clientProject, project)
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
debug('failed to get project from api', err.statusCode)
|
||||
switch (err.statusCode) {
|
||||
case 404:
|
||||
@@ -153,16 +154,19 @@ export async function add (path, options) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getId (path) {
|
||||
return new ProjectBase({ projectRoot: path, testingType: 'e2e', options: {} }).getProjectId()
|
||||
export async function getId (path) {
|
||||
const configFile = await getDefaultConfigFilePath(path)
|
||||
|
||||
return new ProjectBase({ projectRoot: path, testingType: 'e2e', options: { configFile } }).getProjectId()
|
||||
}
|
||||
|
||||
export function ensureExists (path, options) {
|
||||
// is there a configFile? is the root writable?
|
||||
return settings.exists(path, options)
|
||||
interface ProjectIdOptions{
|
||||
id: string
|
||||
projectRoot: string
|
||||
configFile: string
|
||||
}
|
||||
|
||||
export async function writeProjectId (id: string, projectRoot: string) {
|
||||
export async function writeProjectId ({ id, projectRoot, configFile }: ProjectIdOptions) {
|
||||
const attrs = { projectId: id }
|
||||
|
||||
logger.info('Writing Project ID', _.clone(attrs))
|
||||
@@ -170,7 +174,7 @@ export async function writeProjectId (id: string, projectRoot: string) {
|
||||
// TODO: We need to set this
|
||||
// this.generatedProjectIdTimestamp = new Date()
|
||||
|
||||
await settings.write(projectRoot, attrs)
|
||||
await settings.write(projectRoot, attrs, { configFile })
|
||||
|
||||
return id
|
||||
}
|
||||
@@ -180,10 +184,11 @@ interface ProjectDetails {
|
||||
projectRoot: string
|
||||
orgId: string | null
|
||||
public: boolean
|
||||
configFile: string
|
||||
}
|
||||
|
||||
export async function createCiProject (projectDetails: ProjectDetails, projectRoot: string) {
|
||||
debug('create CI project with projectDetails %o projectRoot %s', projectDetails, projectRoot)
|
||||
export async function createCiProject ({ projectRoot, configFile, ...projectDetails }: ProjectDetails) {
|
||||
debug('create CI project with projectDetails %o projectRoot %s', projectDetails)
|
||||
|
||||
const authToken = await user.ensureAuthToken()
|
||||
const remoteOrigin = await commitInfo.getRemoteOrigin(projectRoot)
|
||||
@@ -195,7 +200,11 @@ export async function createCiProject (projectDetails: ProjectDetails, projectRo
|
||||
|
||||
const newProject = await api.createProject(projectDetails, remoteOrigin, authToken)
|
||||
|
||||
await writeProjectId(newProject.id, projectRoot)
|
||||
await writeProjectId({
|
||||
configFile,
|
||||
projectRoot,
|
||||
id: newProject.id,
|
||||
})
|
||||
|
||||
return newProject
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Debug from 'debug'
|
||||
import path from 'path'
|
||||
|
||||
import settings from './util/settings'
|
||||
import * as settings from './util/settings'
|
||||
import errors from './errors'
|
||||
import { fs } from './util/fs'
|
||||
import { escapeFilenameInUrl } from './util/escape_filename'
|
||||
import { CYPRESS_CONFIG_FILES } from './configFiles'
|
||||
|
||||
const debug = Debug('cypress:server:project_utils')
|
||||
|
||||
@@ -119,7 +120,7 @@ export const checkSupportFile = async ({
|
||||
configFile,
|
||||
}: {
|
||||
supportFile?: string | boolean
|
||||
configFile?: string | boolean
|
||||
configFile?: string | false
|
||||
}) => {
|
||||
if (supportFile && typeof supportFile === 'string') {
|
||||
const found = await fs.pathExists(supportFile)
|
||||
@@ -131,3 +132,26 @@ export const checkSupportFile = async ({
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export async function getDefaultConfigFilePath (projectRoot: string, returnDefaultValueIfNotFound: boolean = true): Promise<string | undefined> {
|
||||
const filesInProjectDir = await fs.readdir(projectRoot)
|
||||
|
||||
const foundConfigFiles = CYPRESS_CONFIG_FILES.filter((file) => filesInProjectDir.includes(file))
|
||||
|
||||
// if we only found one default file, it is the one
|
||||
if (foundConfigFiles.length === 1) {
|
||||
return foundConfigFiles[0]
|
||||
}
|
||||
|
||||
// if we found more than one, throw a language conflict
|
||||
if (foundConfigFiles.length > 1) {
|
||||
throw errors.throw('CONFIG_FILES_LANGUAGE_CONFLICT', projectRoot, ...foundConfigFiles)
|
||||
}
|
||||
|
||||
if (returnDefaultValueIfNotFound) {
|
||||
// Default is to create a new `cypress.json` file if one does not exist.
|
||||
return CYPRESS_CONFIG_FILES[0]
|
||||
}
|
||||
|
||||
throw errors.get('NO_DEFAULT_CONFIG_FILE_FOUND', projectRoot)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import Debug from 'debug'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import send from 'send'
|
||||
import { getPathToDist } from '@packages/resolve-dist'
|
||||
import { runner } from './controllers/runner'
|
||||
import type { InitializeRoutes } from './routes'
|
||||
|
||||
const debug = Debug('cypress:server:routes-ct')
|
||||
@@ -15,12 +14,7 @@ const serveChunk = (req: Request, res: Response, clientRoute: string) => {
|
||||
|
||||
export const createRoutesCT = ({
|
||||
config,
|
||||
specsStore,
|
||||
nodeProxy,
|
||||
getCurrentBrowser,
|
||||
testingType,
|
||||
getSpec,
|
||||
getRemoteState,
|
||||
}: InitializeRoutes) => {
|
||||
const routesCt = Router()
|
||||
|
||||
@@ -44,19 +38,6 @@ export const createRoutesCT = ({
|
||||
throw Error(`clientRoute is required. Received ${clientRoute}`)
|
||||
}
|
||||
|
||||
routesCt.get(clientRoute, (req, res) => {
|
||||
debug('Serving Cypress front-end by requested URL:', req.url)
|
||||
|
||||
runner.serve(req, res, 'runner-ct', {
|
||||
config,
|
||||
testingType,
|
||||
getSpec,
|
||||
getCurrentBrowser,
|
||||
getRemoteState,
|
||||
specsStore,
|
||||
})
|
||||
})
|
||||
|
||||
// enables runner-ct to make a dynamic import
|
||||
routesCt.get([
|
||||
`${clientRoute}ctChunk-*`,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import path from 'path'
|
||||
import la from 'lazy-ass'
|
||||
import check from 'check-more-types'
|
||||
import Debug from 'debug'
|
||||
import { Router } from 'express'
|
||||
|
||||
@@ -8,7 +6,6 @@ import AppData from './util/app_data'
|
||||
import CacheBuster from './util/cache_buster'
|
||||
import specController from './controllers/spec'
|
||||
import reporter from './controllers/reporter'
|
||||
import { runner } from './controllers/runner'
|
||||
import client from './controllers/client'
|
||||
import files from './controllers/files'
|
||||
import type { InitializeRoutes } from './routes'
|
||||
@@ -17,13 +14,8 @@ const debug = Debug('cypress:server:routes-e2e')
|
||||
|
||||
export const createRoutesE2E = ({
|
||||
config,
|
||||
specsStore,
|
||||
getRemoteState,
|
||||
networkProxy,
|
||||
getSpec,
|
||||
getCurrentBrowser,
|
||||
onError,
|
||||
testingType,
|
||||
}: InitializeRoutes) => {
|
||||
const routesE2E = Router()
|
||||
|
||||
@@ -137,20 +129,5 @@ export const createRoutesE2E = ({
|
||||
res.sendFile(file, { etag: false })
|
||||
})
|
||||
|
||||
la(check.unemptyString(config.clientRoute), 'missing client route in config', config)
|
||||
|
||||
routesE2E.get(`${config.clientRoute}`, (req, res) => {
|
||||
debug('Serving Cypress front-end by requested URL:', req.url)
|
||||
|
||||
runner.serve(req, res, 'runner', {
|
||||
config,
|
||||
testingType,
|
||||
getSpec,
|
||||
getCurrentBrowser,
|
||||
getRemoteState,
|
||||
specsStore,
|
||||
})
|
||||
})
|
||||
|
||||
return routesE2E
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type httpProxy from 'http-proxy'
|
||||
import Debug from 'debug'
|
||||
import { ErrorRequestHandler, Router } from 'express'
|
||||
|
||||
import type { SpecsStore } from './specs-store'
|
||||
@@ -9,6 +10,8 @@ import xhrs from './controllers/xhrs'
|
||||
import { runner } from './controllers/runner'
|
||||
import { iframesController } from './controllers/iframes'
|
||||
|
||||
const debug = Debug('cypress:server:routes')
|
||||
|
||||
export interface InitializeRoutes {
|
||||
specsStore: SpecsStore
|
||||
config: Cfg
|
||||
@@ -26,6 +29,8 @@ export const createCommonRoutes = ({
|
||||
networkProxy,
|
||||
testingType,
|
||||
getSpec,
|
||||
getCurrentBrowser,
|
||||
specsStore,
|
||||
getRemoteState,
|
||||
nodeProxy,
|
||||
}: InitializeRoutes) => {
|
||||
@@ -49,6 +54,25 @@ export const createCommonRoutes = ({
|
||||
}
|
||||
})
|
||||
|
||||
const clientRoute = config.clientRoute
|
||||
|
||||
if (!clientRoute) {
|
||||
throw Error(`clientRoute is required. Received ${clientRoute}`)
|
||||
}
|
||||
|
||||
router.get(clientRoute, (req, res) => {
|
||||
debug('Serving Cypress front-end by requested URL:', req.url)
|
||||
|
||||
runner.serve(req, res, testingType === 'e2e' ? 'runner' : 'runner-ct', {
|
||||
config,
|
||||
testingType,
|
||||
getSpec,
|
||||
getCurrentBrowser,
|
||||
getRemoteState,
|
||||
specsStore,
|
||||
})
|
||||
})
|
||||
|
||||
router.all('*', (req, res) => {
|
||||
networkProxy.handleHttpRequest(req, res)
|
||||
})
|
||||
|
||||
@@ -298,8 +298,9 @@ const getDimensions = function (details) {
|
||||
return pick(details.image.bitmap)
|
||||
}
|
||||
|
||||
const ensureSafePath = function (withoutExt, extension, num = 0) {
|
||||
const suffix = `${num ? ` (${num})` : ''}.${extension}`
|
||||
const ensureSafePath = function (withoutExt, extension, overwrite, num = 0) {
|
||||
const suffix = `${(num && !overwrite) ? ` (${num})` : ''}.${extension}`
|
||||
|
||||
const maxSafePrefixBytes = maxSafeBytes - suffix.length
|
||||
const filenameBuf = Buffer.from(path.basename(withoutExt))
|
||||
|
||||
@@ -315,8 +316,8 @@ const ensureSafePath = function (withoutExt, extension, num = 0) {
|
||||
|
||||
return fs.pathExists(fullPath)
|
||||
.then((found) => {
|
||||
if (found) {
|
||||
return ensureSafePath(withoutExt, extension, num + 1)
|
||||
if (found && !overwrite) {
|
||||
return ensureSafePath(withoutExt, extension, overwrite, num + 1)
|
||||
}
|
||||
|
||||
// path does not exist, attempt to create it to check for an ENAMETOOLONG error
|
||||
@@ -328,7 +329,7 @@ const ensureSafePath = function (withoutExt, extension, num = 0) {
|
||||
if (err.code === 'ENAMETOOLONG' && maxSafePrefixBytes >= MIN_PREFIX_BYTES) {
|
||||
maxSafeBytes -= 1
|
||||
|
||||
return ensureSafePath(withoutExt, extension, num)
|
||||
return ensureSafePath(withoutExt, extension, overwrite, num)
|
||||
}
|
||||
|
||||
throw err
|
||||
@@ -342,7 +343,7 @@ const sanitizeToString = (title) => {
|
||||
return sanitize(_.toString(title))
|
||||
}
|
||||
|
||||
const getPath = function (data, ext, screenshotsFolder) {
|
||||
const getPath = function (data, ext, screenshotsFolder, overwrite) {
|
||||
let names
|
||||
const specNames = (data.specName || '')
|
||||
.split(pathSeparatorRe)
|
||||
@@ -371,13 +372,13 @@ const getPath = function (data, ext, screenshotsFolder) {
|
||||
|
||||
const withoutExt = path.join(screenshotsFolder, ...specNames, ...names)
|
||||
|
||||
return ensureSafePath(withoutExt, ext)
|
||||
return ensureSafePath(withoutExt, ext, overwrite)
|
||||
}
|
||||
|
||||
const getPathToScreenshot = function (data, details, screenshotsFolder) {
|
||||
const ext = mime.getExtension(getType(details))
|
||||
|
||||
return getPath(data, ext, screenshotsFolder)
|
||||
return getPath(data, ext, screenshotsFolder, data.overwrite)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -392,9 +393,8 @@ module.exports = {
|
||||
copy (src, dest) {
|
||||
return fs
|
||||
.copyAsync(src, dest, { overwrite: true })
|
||||
.catch({ code: 'ENOENT' }, () => {})
|
||||
.catch({ code: 'ENOENT' }, () => { })
|
||||
},
|
||||
// dont yell about ENOENT errors
|
||||
|
||||
get (screenshotsFolder) {
|
||||
// find all files in all nested dirs
|
||||
|
||||
@@ -247,22 +247,29 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (spec) {
|
||||
const resolvePath = (p) => {
|
||||
return path.resolve(options.cwd, p)
|
||||
}
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/8818
|
||||
// Sometimes spec is parsed to array. Because of that, we need check.
|
||||
if (typeof spec === 'string') {
|
||||
// clean up single quotes wrapping the spec for Windows users
|
||||
// https://github.com/cypress-io/cypress/issues/2298
|
||||
if (spec[0] === '\'' && spec[spec.length - 1] === '\'') {
|
||||
spec = spec.substring(1, spec.length - 1)
|
||||
try {
|
||||
const resolvePath = (p) => {
|
||||
return path.resolve(options.cwd, p)
|
||||
}
|
||||
|
||||
options.spec = strToArray(spec).map(resolvePath)
|
||||
} else {
|
||||
options.spec = spec.map(resolvePath)
|
||||
// https://github.com/cypress-io/cypress/issues/8818
|
||||
// Sometimes spec is parsed to array. Because of that, we need check.
|
||||
if (typeof spec === 'string') {
|
||||
// clean up single quotes wrapping the spec for Windows users
|
||||
// https://github.com/cypress-io/cypress/issues/2298
|
||||
if (spec[0] === '\'' && spec[spec.length - 1] === '\'') {
|
||||
spec = spec.substring(1, spec.length - 1)
|
||||
}
|
||||
|
||||
options.spec = strToArray(spec).map(resolvePath)
|
||||
} else {
|
||||
options.spec = spec.map(resolvePath)
|
||||
}
|
||||
} catch (err) {
|
||||
debug('could not pass config spec value %s', spec)
|
||||
debug('error %o', err)
|
||||
|
||||
return errors.throw('COULD_NOT_PARSE_ARGUMENTS', 'spec', spec, 'spec must be a string or comma-separated list')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -505,7 +505,7 @@ const _providerCommitParams = () => {
|
||||
message: env.DRONE_COMMIT_MESSAGE,
|
||||
authorName: env.DRONE_COMMIT_AUTHOR,
|
||||
authorEmail: env.DRONE_COMMIT_AUTHOR_EMAIL,
|
||||
// remoteOrigin: ???
|
||||
remoteOrigin: env.DRONE_GIT_HTTP_URL,
|
||||
defaultBranch: env.DRONE_REPO_BRANCH,
|
||||
},
|
||||
githubActions: {
|
||||
|
||||
@@ -14,10 +14,6 @@ let requireProcess: cp.ChildProcess | null
|
||||
interface RequireAsyncOptions{
|
||||
projectRoot: string
|
||||
loadErrorCode: string
|
||||
/**
|
||||
* members of the object returned that are functions and will need to be wrapped
|
||||
*/
|
||||
functionNames: string[]
|
||||
}
|
||||
|
||||
interface ChildOptions{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require('graceful-fs').gracefulify(require('fs'))
|
||||
const stripAnsi = require('strip-ansi')
|
||||
const debug = require('debug')('cypress:server:require_async:child')
|
||||
const tsNodeUtil = require('./ts_node')
|
||||
const util = require('../plugins/util')
|
||||
const ipc = util.wrapIpc(process)
|
||||
|
||||
@@ -8,6 +9,8 @@ require('./suppress_warnings').suppress()
|
||||
|
||||
const { file, projectRoot } = require('minimist')(process.argv.slice(2))
|
||||
|
||||
let tsRegistered = false
|
||||
|
||||
run(ipc, file, projectRoot)
|
||||
|
||||
/**
|
||||
@@ -24,6 +27,14 @@ function run (ipc, requiredFile, projectRoot) {
|
||||
throw new Error('Unexpected: projectRoot should be a string')
|
||||
}
|
||||
|
||||
if (!tsRegistered && requiredFile.endsWith('.ts')) {
|
||||
debug('register typescript for required file')
|
||||
tsNodeUtil.register(projectRoot, requiredFile)
|
||||
|
||||
// ensure typescript is only registered once
|
||||
tsRegistered = true
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
debug('uncaught exception:', util.serializeError(err))
|
||||
ipc.send('error', util.serializeError(err))
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
const Promise = require('bluebird')
|
||||
const path = require('path')
|
||||
const errors = require('../errors')
|
||||
const log = require('../log')
|
||||
const { fs } = require('../util/fs')
|
||||
const { requireAsync } = require('./require_async')
|
||||
const debug = require('debug')('cypress:server:settings')
|
||||
|
||||
function jsCode (obj) {
|
||||
const objJSON = obj && !_.isEmpty(obj)
|
||||
? JSON.stringify(_.omit(obj, 'configFile'), null, 2)
|
||||
: `{
|
||||
|
||||
}`
|
||||
|
||||
return `module.exports = ${objJSON}
|
||||
`
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// think about adding another PSemaphore
|
||||
// here since we can read + write the
|
||||
// settings at the same time something else
|
||||
// is potentially reading it
|
||||
|
||||
const flattenCypress = (obj) => {
|
||||
return obj.cypress ? obj.cypress : undefined
|
||||
}
|
||||
|
||||
const maybeVerifyConfigFile = Promise.method((configFile) => {
|
||||
if (configFile === false) {
|
||||
return
|
||||
}
|
||||
|
||||
return fs.statAsync(configFile)
|
||||
})
|
||||
|
||||
const renameVisitToPageLoad = (obj) => {
|
||||
const v = obj.visitTimeout
|
||||
|
||||
if (v) {
|
||||
obj = _.omit(obj, 'visitTimeout')
|
||||
obj.pageLoadTimeout = v
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
const renameCommandTimeout = (obj) => {
|
||||
const c = obj.commandTimeout
|
||||
|
||||
if (c) {
|
||||
obj = _.omit(obj, 'commandTimeout')
|
||||
obj.defaultCommandTimeout = c
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
const renameSupportFolder = (obj) => {
|
||||
const sf = obj.supportFolder
|
||||
|
||||
if (sf) {
|
||||
obj = _.omit(obj, 'supportFolder')
|
||||
obj.supportFile = sf
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_pathToFile (projectRoot, file) {
|
||||
return path.isAbsolute(file) ? file : path.join(projectRoot, file)
|
||||
},
|
||||
|
||||
_err (type, file, err) {
|
||||
const e = errors.get(type, file, err)
|
||||
|
||||
e.code = err.code
|
||||
e.errno = err.errno
|
||||
throw e
|
||||
},
|
||||
|
||||
_logReadErr (file, err) {
|
||||
errors.throw('ERROR_READING_FILE', file, err)
|
||||
},
|
||||
|
||||
_logWriteErr (file, err) {
|
||||
return this._err('ERROR_WRITING_FILE', file, err)
|
||||
},
|
||||
|
||||
_write (file, obj = {}) {
|
||||
if (/\.json$/.test(file)) {
|
||||
debug('writing json file')
|
||||
|
||||
return fs.outputJsonAsync(file, obj, { spaces: 2 })
|
||||
.return(obj)
|
||||
.catch((err) => {
|
||||
return this._logWriteErr(file, err)
|
||||
})
|
||||
}
|
||||
|
||||
debug('writing javascript file')
|
||||
|
||||
return fs.writeFileAsync(file, jsCode(obj))
|
||||
.return(obj)
|
||||
.catch((err) => {
|
||||
return this._logWriteErr(file, err)
|
||||
})
|
||||
},
|
||||
|
||||
_applyRewriteRules (obj = {}) {
|
||||
return _.reduce([flattenCypress, renameVisitToPageLoad, renameCommandTimeout, renameSupportFolder], (memo, fn) => {
|
||||
const ret = fn(memo)
|
||||
|
||||
return ret ? ret : memo
|
||||
}, _.cloneDeep(obj))
|
||||
},
|
||||
|
||||
isComponentTesting (options = {}) {
|
||||
return options.testingType === 'component'
|
||||
},
|
||||
|
||||
configFile (options = {}) {
|
||||
return options.configFile === false ? false : (options.configFile || 'cypress.json')
|
||||
},
|
||||
|
||||
id (projectRoot, options = {}) {
|
||||
const file = this.pathToConfigFile(projectRoot, options)
|
||||
|
||||
return fs.readJsonAsync(file)
|
||||
.get('projectId')
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Ensures the project at this root has a config file
|
||||
* that is readable and writable by the node process
|
||||
* @param {string} projectRoot root of the project
|
||||
* @param {object} options
|
||||
* @returns
|
||||
*/
|
||||
exists (projectRoot, options = {}) {
|
||||
const file = this.pathToConfigFile(projectRoot, options)
|
||||
|
||||
// first check if cypress.json exists
|
||||
return maybeVerifyConfigFile(file)
|
||||
.then(() => {
|
||||
// if it does also check that the projectRoot
|
||||
// directory is writable
|
||||
return fs.accessAsync(projectRoot, fs.W_OK)
|
||||
}).catch({ code: 'ENOENT' }, () => {
|
||||
// cypress.json does not exist, completely new project
|
||||
log('cannot find file %s', file)
|
||||
|
||||
return this._err('CONFIG_FILE_NOT_FOUND', this.configFile(options), projectRoot)
|
||||
}).catch({ code: 'EACCES' }, { code: 'EPERM' }, () => {
|
||||
// we cannot write due to folder permissions
|
||||
return errors.warning('FOLDER_NOT_WRITABLE', projectRoot)
|
||||
}).catch((err) => {
|
||||
if (errors.isCypressErr(err)) {
|
||||
throw err
|
||||
}
|
||||
|
||||
return this._logReadErr(file, err)
|
||||
})
|
||||
},
|
||||
|
||||
read (projectRoot, options = {}) {
|
||||
if (options.configFile === false) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
const file = this.pathToConfigFile(projectRoot, options)
|
||||
|
||||
return requireAsync(file,
|
||||
{
|
||||
projectRoot,
|
||||
loadErrorCode: 'CONFIG_FILE_ERROR',
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.type === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
|
||||
debug('file not found', file)
|
||||
|
||||
return this._write(file, {})
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
})
|
||||
.then((configObject = {}) => {
|
||||
if (this.isComponentTesting(options) && 'component' in configObject) {
|
||||
configObject = { ...configObject, ...configObject.component }
|
||||
}
|
||||
|
||||
if (!this.isComponentTesting(options) && 'e2e' in configObject) {
|
||||
configObject = { ...configObject, ...configObject.e2e }
|
||||
}
|
||||
|
||||
debug('resolved configObject', configObject)
|
||||
const changed = this._applyRewriteRules(configObject)
|
||||
|
||||
// if our object is unchanged
|
||||
// then just return it
|
||||
if (_.isEqual(configObject, changed)) {
|
||||
return configObject
|
||||
}
|
||||
|
||||
// else write the new reduced obj
|
||||
return this._write(file, changed)
|
||||
.then((config) => {
|
||||
return config
|
||||
})
|
||||
}).catch((err) => {
|
||||
debug('an error occured when reading config', err)
|
||||
if (errors.isCypressErr(err)) {
|
||||
throw err
|
||||
}
|
||||
|
||||
return this._logReadErr(file, err)
|
||||
})
|
||||
},
|
||||
|
||||
readEnv (projectRoot) {
|
||||
const file = this.pathToCypressEnvJson(projectRoot)
|
||||
|
||||
return fs.readJsonAsync(file)
|
||||
.catch({ code: 'ENOENT' }, () => {
|
||||
return {}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (errors.isCypressErr(err)) {
|
||||
throw err
|
||||
}
|
||||
|
||||
return this._logReadErr(file, err)
|
||||
})
|
||||
},
|
||||
|
||||
write (projectRoot, obj = {}, options = {}) {
|
||||
if (options.configFile === false) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
return this.read(projectRoot, options)
|
||||
.then((settings) => {
|
||||
_.extend(settings, obj)
|
||||
|
||||
const file = this.pathToConfigFile(projectRoot, options)
|
||||
|
||||
return this._write(file, settings)
|
||||
})
|
||||
},
|
||||
|
||||
pathToConfigFile (projectRoot, options = {}) {
|
||||
const configFile = this.configFile(options)
|
||||
|
||||
return configFile && this._pathToFile(projectRoot, configFile)
|
||||
},
|
||||
|
||||
pathToCypressEnvJson (projectRoot) {
|
||||
return this._pathToFile(projectRoot, 'cypress.env.json')
|
||||
},
|
||||
}
|
||||
238
packages/server/lib/util/settings.ts
Normal file
238
packages/server/lib/util/settings.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import _ from 'lodash'
|
||||
import Promise from 'bluebird'
|
||||
import path from 'path'
|
||||
import errors from '../errors'
|
||||
import { fs } from '../util/fs'
|
||||
import { requireAsync } from './require_async'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('cypress:server:settings')
|
||||
|
||||
interface SettingsOptions {
|
||||
testingType?: 'component' |'e2e'
|
||||
configFile?: string | false
|
||||
args?: {
|
||||
runProject?: string
|
||||
}
|
||||
}
|
||||
|
||||
function jsCode (obj) {
|
||||
const objJSON = obj && !_.isEmpty(obj)
|
||||
? JSON.stringify(_.omit(obj, 'configFile'), null, 2)
|
||||
: `{
|
||||
|
||||
}`
|
||||
|
||||
return `module.exports = ${objJSON}
|
||||
`
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// think about adding another PSemaphore
|
||||
// here since we can read + write the
|
||||
// settings at the same time something else
|
||||
// is potentially reading it
|
||||
|
||||
const flattenCypress = (obj) => {
|
||||
return obj.cypress ? obj.cypress : undefined
|
||||
}
|
||||
|
||||
const renameVisitToPageLoad = (obj) => {
|
||||
const v = obj.visitTimeout
|
||||
|
||||
if (v) {
|
||||
obj = _.omit(obj, 'visitTimeout')
|
||||
obj.pageLoadTimeout = v
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
const renameCommandTimeout = (obj) => {
|
||||
const c = obj.commandTimeout
|
||||
|
||||
if (c) {
|
||||
obj = _.omit(obj, 'commandTimeout')
|
||||
obj.defaultCommandTimeout = c
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
const renameSupportFolder = (obj) => {
|
||||
const sf = obj.supportFolder
|
||||
|
||||
if (sf) {
|
||||
obj = _.omit(obj, 'supportFolder')
|
||||
obj.supportFile = sf
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
function _pathToFile (projectRoot, file) {
|
||||
return path.isAbsolute(file) ? file : path.join(projectRoot, file)
|
||||
}
|
||||
|
||||
function _err (type, file, err) {
|
||||
const e = errors.get(type, file, err)
|
||||
|
||||
e.code = err.code
|
||||
e.errno = err.errno
|
||||
throw e
|
||||
}
|
||||
|
||||
function _logReadErr (file, err) {
|
||||
errors.throw('ERROR_READING_FILE', file, err)
|
||||
}
|
||||
|
||||
function _logWriteErr (file, err) {
|
||||
return _err('ERROR_WRITING_FILE', file, err)
|
||||
}
|
||||
|
||||
function _write (file, obj = {}) {
|
||||
if (/\.json$/.test(file)) {
|
||||
debug('writing json file')
|
||||
|
||||
return fs.outputJson(file, obj, { spaces: 2 })
|
||||
.then(() => obj)
|
||||
.catch((err) => {
|
||||
return _logWriteErr(file, err)
|
||||
})
|
||||
}
|
||||
|
||||
debug('writing javascript file')
|
||||
|
||||
return fs.writeFileAsync(file, jsCode(obj))
|
||||
.return(obj)
|
||||
.catch((err) => {
|
||||
return _logWriteErr(file, err)
|
||||
})
|
||||
}
|
||||
|
||||
function _applyRewriteRules (obj = {}) {
|
||||
return _.reduce([flattenCypress, renameVisitToPageLoad, renameCommandTimeout, renameSupportFolder], (memo, fn) => {
|
||||
const ret = fn(memo)
|
||||
|
||||
return ret ? ret : memo
|
||||
}, _.cloneDeep(obj))
|
||||
}
|
||||
|
||||
export function isComponentTesting (options: SettingsOptions = {}) {
|
||||
return options.testingType === 'component'
|
||||
}
|
||||
|
||||
export function configFile (options: SettingsOptions = {}) {
|
||||
// default is only used in tests.
|
||||
// This prevents a the change from becoming bigger than it should
|
||||
// FIXME: remove the default
|
||||
return options.configFile === false ? false : (options.configFile || 'cypress.json')
|
||||
}
|
||||
|
||||
export function id (projectRoot, options = {}) {
|
||||
const file = pathToConfigFile(projectRoot, options)
|
||||
|
||||
return fs.readJson(file)
|
||||
.then((config) => config.projectId)
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
export function read (projectRoot, options: SettingsOptions = {}) {
|
||||
if (options.configFile === false) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
const file = pathToConfigFile(projectRoot, options)
|
||||
|
||||
return requireAsync(file,
|
||||
{
|
||||
projectRoot,
|
||||
loadErrorCode: 'CONFIG_FILE_ERROR',
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.type === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
|
||||
if (options.args?.runProject) {
|
||||
return Promise.reject(errors.get('CONFIG_FILE_NOT_FOUND', options.configFile, projectRoot))
|
||||
}
|
||||
|
||||
return _write(file, {})
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
})
|
||||
.then((configObject = {}) => {
|
||||
if (isComponentTesting(options) && 'component' in configObject) {
|
||||
configObject = { ...configObject, ...configObject.component }
|
||||
}
|
||||
|
||||
if (!isComponentTesting(options) && 'e2e' in configObject) {
|
||||
configObject = { ...configObject, ...configObject.e2e }
|
||||
}
|
||||
|
||||
debug('resolved configObject', configObject)
|
||||
const changed = _applyRewriteRules(configObject)
|
||||
|
||||
// if our object is unchanged
|
||||
// then just return it
|
||||
if (_.isEqual(configObject, changed)) {
|
||||
return configObject
|
||||
}
|
||||
|
||||
// else write the new reduced obj
|
||||
return _write(file, changed)
|
||||
.then((config) => {
|
||||
return config
|
||||
})
|
||||
}).catch((err) => {
|
||||
debug('an error occured when reading config', err)
|
||||
if (errors.isCypressErr(err)) {
|
||||
throw err
|
||||
}
|
||||
|
||||
return _logReadErr(file, err)
|
||||
})
|
||||
}
|
||||
|
||||
export function readEnv (projectRoot) {
|
||||
const file = pathToCypressEnvJson(projectRoot)
|
||||
|
||||
return fs.readJson(file)
|
||||
.catch((err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (errors.isCypressErr(err)) {
|
||||
throw err
|
||||
}
|
||||
|
||||
return _logReadErr(file, err)
|
||||
})
|
||||
}
|
||||
|
||||
export function write (projectRoot, obj = {}, options: SettingsOptions = {}) {
|
||||
if (options.configFile === false) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
return read(projectRoot, options)
|
||||
.then((settings) => {
|
||||
_.extend(settings, obj)
|
||||
|
||||
const file = pathToConfigFile(projectRoot, options)
|
||||
|
||||
return _write(file, settings)
|
||||
})
|
||||
}
|
||||
|
||||
export function pathToConfigFile (projectRoot, options: SettingsOptions = {}) {
|
||||
const file = configFile(options)
|
||||
|
||||
return file && _pathToFile(projectRoot, file)
|
||||
}
|
||||
|
||||
export function pathToCypressEnvJson (projectRoot) {
|
||||
return _pathToFile(projectRoot, 'cypress.env.json')
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
const debug = require('debug')('cypress:server:ts-node')
|
||||
const path = require('path')
|
||||
const tsnode = require('ts-node')
|
||||
const resolve = require('../../util/resolve')
|
||||
const resolve = require('./resolve')
|
||||
|
||||
const getTsNodeOptions = (tsPath, pluginsFile) => {
|
||||
const getTsNodeOptions = (tsPath, registeredFile) => {
|
||||
return {
|
||||
compiler: tsPath, // use the user's installed typescript
|
||||
compilerOptions: {
|
||||
@@ -11,20 +11,23 @@ const getTsNodeOptions = (tsPath, pluginsFile) => {
|
||||
},
|
||||
// resolves tsconfig.json starting from the plugins directory
|
||||
// instead of the cwd (the project root)
|
||||
dir: path.dirname(pluginsFile),
|
||||
dir: path.dirname(registeredFile),
|
||||
transpileOnly: true, // transpile only (no type-check) for speed
|
||||
}
|
||||
}
|
||||
|
||||
const register = (projectRoot, pluginsFile) => {
|
||||
const register = (projectRoot, registeredFile) => {
|
||||
try {
|
||||
debug('projectRoot path: %s', projectRoot)
|
||||
debug('registeredFile: %s', registeredFile)
|
||||
const tsPath = resolve.typescript(projectRoot)
|
||||
|
||||
if (!tsPath) return
|
||||
|
||||
const tsOptions = getTsNodeOptions(tsPath, pluginsFile)
|
||||
|
||||
debug('typescript path: %s', tsPath)
|
||||
|
||||
const tsOptions = getTsNodeOptions(tsPath, registeredFile)
|
||||
|
||||
debug('registering project TS with options %o', tsOptions)
|
||||
|
||||
require('tsconfig-paths/register')
|
||||
@@ -1,4 +1,6 @@
|
||||
const e2e = require('../support/helpers/e2e').default
|
||||
const { fs } = require('../../lib/util/fs')
|
||||
const path = require('path')
|
||||
const Fixtures = require('../support/helpers/fixtures')
|
||||
|
||||
describe('e2e config', () => {
|
||||
@@ -54,4 +56,38 @@ describe('e2e config', () => {
|
||||
configFile: 'cypress.config.custom.js',
|
||||
})
|
||||
})
|
||||
|
||||
it('supports custom configFile in TypeScript', function () {
|
||||
return e2e.exec(this, {
|
||||
project: Fixtures.projectPath('config-with-custom-file-ts'),
|
||||
configFile: 'cypress.config.custom.ts',
|
||||
})
|
||||
})
|
||||
|
||||
it('supports custom configFile in a default JavaScript file', function () {
|
||||
return e2e.exec(this, {
|
||||
project: Fixtures.projectPath('config-with-js'),
|
||||
})
|
||||
})
|
||||
|
||||
it('supports custom configFile in a default TypeScript file', function () {
|
||||
return e2e.exec(this, {
|
||||
project: Fixtures.projectPath('config-with-ts'),
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error when multiple default config file are found in project', function () {
|
||||
const projectRoot = Fixtures.projectPath('pristine')
|
||||
|
||||
return Promise.all([
|
||||
fs.writeFile(path.join(projectRoot, 'cypress.config.js'), 'module.exports = {}'),
|
||||
fs.writeFile(path.join(projectRoot, 'cypress.config.ts'), 'export default {}'),
|
||||
]).then(() => {
|
||||
return e2e.exec(this, {
|
||||
project: projectRoot,
|
||||
expectedExitCode: 1,
|
||||
snapshot: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -499,7 +499,7 @@ describe('lib/cypress', () => {
|
||||
])
|
||||
}).each(ensureDoesNotExist)
|
||||
.then(() => {
|
||||
this.expectExitWithErr('CONFIG_FILE_NOT_FOUND', this.pristinePath)
|
||||
this.expectExitWithErr('NO_DEFAULT_CONFIG_FILE_FOUND', this.pristinePath)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
module.exports = {
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
pageLoadTimeout: 10000,
|
||||
e2e: {
|
||||
defaultCommandTimeout: 500,
|
||||
videoCompression: 20,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const config: Record<string, any> = {
|
||||
pageLoadTimeout: 10000,
|
||||
e2e: {
|
||||
defaultCommandTimeout: 500,
|
||||
videoCompression: 20,
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,8 @@
|
||||
it('overrides config', () => {
|
||||
// overrides come from plugins
|
||||
expect(Cypress.config('defaultCommandTimeout')).to.eq(500)
|
||||
expect(Cypress.config('videoCompression')).to.eq(20)
|
||||
|
||||
// overrides come from CLI
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(10000)
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
pageLoadTimeout: 10000,
|
||||
e2e: {
|
||||
defaultCommandTimeout: 500,
|
||||
videoCompression: 20,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
it('overrides config', () => {
|
||||
// overrides come from plugins
|
||||
expect(Cypress.config('defaultCommandTimeout')).to.eq(500)
|
||||
expect(Cypress.config('videoCompression')).to.eq(20)
|
||||
|
||||
// overrides come from CLI
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(10000)
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
pageLoadTimeout: 10000,
|
||||
e2e: {
|
||||
defaultCommandTimeout: 500,
|
||||
videoCompression: 20,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
it('overrides config', () => {
|
||||
// overrides come from plugins
|
||||
expect(Cypress.config('defaultCommandTimeout')).to.eq(500)
|
||||
expect(Cypress.config('videoCompression')).to.eq(20)
|
||||
|
||||
// overrides come from CLI
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(10000)
|
||||
})
|
||||
@@ -302,13 +302,44 @@ describe('taking screenshots', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// @see https://github.com/cypress-io/cypress/issues/7955
|
||||
it('can pass overwrite option to replace existing filename', () => {
|
||||
cy.viewport(600, 200)
|
||||
cy.visit('http://localhost:3322/color/yellow')
|
||||
cy.screenshot('overwrite-test', {
|
||||
overwrite: false,
|
||||
clip: { x: 10, y: 10, width: 160, height: 80 },
|
||||
})
|
||||
|
||||
cy.task('check:screenshot:size', {
|
||||
name: `${path.basename(__filename)}/overwrite-test.png`,
|
||||
width: 160,
|
||||
height: 80,
|
||||
devicePixelRatio,
|
||||
})
|
||||
|
||||
cy.screenshot('overwrite-test', {
|
||||
overwrite: true,
|
||||
clip: { x: 10, y: 10, width: 100, height: 50 },
|
||||
})
|
||||
|
||||
cy.readFile(`cypress/screenshots/${path.basename(__filename)}/overwrite-test (1).png`).should('not.exist')
|
||||
|
||||
cy.task('check:screenshot:size', {
|
||||
name: `${path.basename(__filename)}/overwrite-test.png`,
|
||||
width: 100,
|
||||
height: 50,
|
||||
devicePixelRatio,
|
||||
})
|
||||
})
|
||||
|
||||
context('before hooks', () => {
|
||||
before(() => {
|
||||
// failure 2
|
||||
throw new Error('before hook failing')
|
||||
})
|
||||
|
||||
it('empty test 1', () => {})
|
||||
it('empty test 1', () => { })
|
||||
})
|
||||
|
||||
context('each hooks', () => {
|
||||
@@ -322,7 +353,7 @@ describe('taking screenshots', () => {
|
||||
throw new Error('after each hook failed')
|
||||
})
|
||||
|
||||
it('empty test 2', () => {})
|
||||
it('empty test 2', () => { })
|
||||
})
|
||||
|
||||
context(`really long test title ${Cypress._.repeat('a', 255)}`, () => {
|
||||
|
||||
@@ -152,6 +152,18 @@ describe('lib/util/args', () => {
|
||||
|
||||
expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/foo_spec.js`)
|
||||
})
|
||||
|
||||
it('throws if argument cannot be parsed', function () {
|
||||
expect(() => {
|
||||
return this.setup('--run-project', 'foo', '--spec', {})
|
||||
}).to.throw
|
||||
|
||||
try {
|
||||
return this.setup('--run-project', 'foo', '--spec', {})
|
||||
} catch (err) {
|
||||
return snapshot('invalid spec error', stripAnsi(err.message))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
context('--tag', () => {
|
||||
|
||||
@@ -519,6 +519,7 @@ describe('lib/util/ci_provider', () => {
|
||||
DRONE_COMMIT_AUTHOR: 'droneCommitAuthor',
|
||||
DRONE_COMMIT_AUTHOR_EMAIL: 'droneCommitAuthorEmail',
|
||||
DRONE_REPO_BRANCH: 'droneRepoBranch',
|
||||
DRONE_GIT_HTTP_URL: 'droneRemoteOrigin',
|
||||
}, { clear: true })
|
||||
|
||||
expectsName('drone')
|
||||
@@ -536,6 +537,7 @@ describe('lib/util/ci_provider', () => {
|
||||
authorName: 'droneCommitAuthor',
|
||||
authorEmail: 'droneCommitAuthorEmail',
|
||||
defaultBranch: 'droneRepoBranch',
|
||||
remoteOrigin: 'droneRemoteOrigin',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -926,12 +926,12 @@ describe('lib/gui/events', () => {
|
||||
|
||||
describe('set:project:id', () => {
|
||||
it('calls writeProjectId with projectRoot', function () {
|
||||
const arg = { id: '1', projectRoot: '/project/root/' }
|
||||
const stub = sinon.stub(ProjectStatic, 'writeProjectId').resolves()
|
||||
const arg = { id: '1', projectRoot: '/project/root/', configFile: 'cypress.json' }
|
||||
const stubWriteProjectId = sinon.stub(ProjectStatic, 'writeProjectId').resolves()
|
||||
|
||||
return this.handleEvent('set:project:id', arg)
|
||||
.then(() => {
|
||||
expect(stub).to.be.calledWith(arg.id, arg.projectRoot)
|
||||
expect(stubWriteProjectId).to.be.calledWith(arg)
|
||||
expect(this.send.firstCall.args[0]).to.eq('response')
|
||||
expect(this.send.firstCall.args[1].id).to.match(/set:project:id-/)
|
||||
})
|
||||
@@ -940,12 +940,12 @@ describe('lib/gui/events', () => {
|
||||
|
||||
describe('setup:dashboard:project', () => {
|
||||
it('returns result of ProjectStatic.createCiProject', function () {
|
||||
const arg = { projectRoot: '/project/root/' }
|
||||
const stub = sinon.stub(ProjectStatic, 'createCiProject').resolves()
|
||||
const arg = { projectRoot: '/project/root/', configFile: 'cypress.json' }
|
||||
const stubCreateCiProject = sinon.stub(ProjectStatic, 'createCiProject').resolves()
|
||||
|
||||
return this.handleEvent('setup:dashboard:project', arg)
|
||||
.then(() => {
|
||||
expect(stub).to.be.calledWith(arg, arg.projectRoot)
|
||||
expect(stubCreateCiProject).to.be.calledWith(arg)
|
||||
expect(this.send.firstCall.args[0]).to.eq('response')
|
||||
expect(this.send.firstCall.args[1].id).to.match(/setup:dashboard:project-/)
|
||||
})
|
||||
|
||||
@@ -32,11 +32,12 @@ describe('gui/files', () => {
|
||||
|
||||
this.err = new Error('foo')
|
||||
|
||||
sinon.stub(ProjectBase.prototype, 'initializeConfig').resolves()
|
||||
sinon.stub(ProjectBase.prototype, 'open').resolves()
|
||||
sinon.stub(ProjectBase.prototype, 'getConfig').returns(this.config)
|
||||
|
||||
this.showSaveDialog = sinon.stub(dialog, 'showSaveDialog').resolves(this.selectedPath)
|
||||
this.createFile = sinon.stub(specWriter, 'createFile').resolves({})
|
||||
this.createFile = sinon.stub(specWriter, 'createFile').resolves()
|
||||
this.getSpecs = sinon.stub(openProject, 'getSpecs').resolves(this.specs)
|
||||
|
||||
return openProject.create('/_test-output/path/to/project-e2e', {
|
||||
|
||||
@@ -20,7 +20,6 @@ const random = require(`${root}../lib/util/random`)
|
||||
const system = require(`${root}../lib/util/system`)
|
||||
const specsUtil = require(`${root}../lib/util/specs`)
|
||||
const { experimental } = require(`${root}../lib/experiments`)
|
||||
const ProjectStatic = require(`${root}../lib/project_static`)
|
||||
|
||||
describe('lib/modes/run', () => {
|
||||
beforeEach(function () {
|
||||
@@ -660,7 +659,6 @@ describe('lib/modes/run', () => {
|
||||
|
||||
sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync()
|
||||
sinon.stub(user, 'ensureAuthToken')
|
||||
sinon.stub(ProjectStatic, 'ensureExists').resolves()
|
||||
sinon.stub(random, 'id').returns(1234)
|
||||
sinon.stub(openProject, 'create').resolves(openProject)
|
||||
sinon.stub(runMode, 'waitForSocketConnection').resolves()
|
||||
@@ -738,7 +736,6 @@ describe('lib/modes/run', () => {
|
||||
|
||||
sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync()
|
||||
sinon.stub(user, 'ensureAuthToken')
|
||||
sinon.stub(ProjectStatic, 'ensureExists').resolves()
|
||||
sinon.stub(random, 'id').returns(1234)
|
||||
sinon.stub(openProject, 'create').resolves(openProject)
|
||||
sinon.stub(system, 'info').resolves({ osName: 'osFoo', osVersion: 'fooVersion' })
|
||||
|
||||
@@ -10,7 +10,7 @@ const util = require(`${root}../../lib/plugins/util`)
|
||||
const resolve = require(`${root}../../lib/util/resolve`)
|
||||
const browserUtils = require(`${root}../../lib/browsers/utils`)
|
||||
const Fixtures = require(`${root}../../test/support/helpers/fixtures`)
|
||||
const tsNodeUtil = require(`${root}../../lib/plugins/child/ts_node`)
|
||||
const tsNodeUtil = require(`${root}../../lib/util/ts_node`)
|
||||
|
||||
const runPlugins = require(`${root}../../lib/plugins/child/run_plugins`)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ const tsnode = require('ts-node')
|
||||
|
||||
const resolve = require(`${root}../../lib/util/resolve`)
|
||||
|
||||
const tsNodeUtil = require(`${root}../../lib/plugins/child/ts_node`)
|
||||
const tsNodeUtil = require(`${root}../../lib/util/ts_node`)
|
||||
|
||||
describe('lib/plugins/child/ts_node', () => {
|
||||
describe('lib/util/ts_node', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(tsnode, 'register')
|
||||
sinon.stub(resolve, 'typescript').returns('/path/to/typescript.js')
|
||||
|
||||
@@ -87,15 +87,15 @@ describe('lib/project-base', () => {
|
||||
expect(p.projectRoot).to.eq(path.resolve('../foo/bar'))
|
||||
})
|
||||
|
||||
it('handles CT specific behaviors', async function () {
|
||||
it('sets CT specific defaults and calls CT function', async function () {
|
||||
sinon.stub(ServerE2E.prototype, 'open').resolves([])
|
||||
sinon.stub(ProjectBase.prototype, 'startCtDevServer').resolves({ port: 9999 })
|
||||
|
||||
const projectCt = new ProjectBase({ projectRoot: '../foo/bar', testingType: 'component' })
|
||||
const projectCt = new ProjectBase({ projectRoot: this.pristinePath, testingType: 'component' })
|
||||
|
||||
await projectCt.initializeConfig()
|
||||
|
||||
return projectCt.open({}).then((project) => {
|
||||
return projectCt.open({}).then(() => {
|
||||
expect(projectCt._cfg.viewportHeight).to.eq(500)
|
||||
expect(projectCt._cfg.viewportWidth).to.eq(500)
|
||||
expect(projectCt._cfg.baseUrl).to.eq('http://localhost:9999')
|
||||
@@ -149,7 +149,8 @@ describe('lib/project-base', () => {
|
||||
const integrationFolder = 'foo/bar/baz'
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(config, 'get').withArgs(this.todosPath, { foo: 'bar' }).resolves({ baz: 'quux', integrationFolder, browsers: [] })
|
||||
sinon.stub(config, 'get').withArgs(this.todosPath, { foo: 'bar', configFile: 'cypress.json' })
|
||||
.resolves({ baz: 'quux', integrationFolder, browsers: [] })
|
||||
})
|
||||
|
||||
it('calls config.get with projectRoot + options + saved state', function () {
|
||||
@@ -948,14 +949,14 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
})
|
||||
|
||||
it('calls Settings.write with projectRoot and attrs', function () {
|
||||
return writeProjectId('id-123').then((id) => {
|
||||
return writeProjectId({ id: 'id-123' }).then((id) => {
|
||||
expect(id).to.eq('id-123')
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: This
|
||||
xit('sets generatedProjectIdTimestamp', function () {
|
||||
return writeProjectId('id-123').then(() => {
|
||||
return writeProjectId({ id: 'id-123' }).then(() => {
|
||||
expect(this.project.generatedProjectIdTimestamp).to.be.a('date')
|
||||
})
|
||||
})
|
||||
@@ -1016,13 +1017,14 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
|
||||
context('#createCiProject', () => {
|
||||
const projectRoot = '/_test-output/path/to/project-e2e'
|
||||
const configFile = 'cypress.config.js'
|
||||
|
||||
beforeEach(function () {
|
||||
this.project = new ProjectBase({ projectRoot, testingType: 'e2e' })
|
||||
this.newProject = { id: 'project-id-123' }
|
||||
|
||||
sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')
|
||||
sinon.stub(settings, 'write').resolves('project-id-123')
|
||||
sinon.stub(settings, 'write').resolves()
|
||||
sinon.stub(commitInfo, 'getRemoteOrigin').resolves('remoteOrigin')
|
||||
sinon.stub(api, 'createProject')
|
||||
.withArgs({ foo: 'bar' }, 'remoteOrigin', 'auth-token-123')
|
||||
@@ -1030,19 +1032,19 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
})
|
||||
|
||||
it('calls api.createProject with user session', function () {
|
||||
return createCiProject({ foo: 'bar' }, projectRoot).then(() => {
|
||||
return createCiProject({ foo: 'bar', projectRoot }).then(() => {
|
||||
expect(api.createProject).to.be.calledWith({ foo: 'bar' }, 'remoteOrigin', 'auth-token-123')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls writeProjectId with id', function () {
|
||||
return createCiProject({ foo: 'bar' }, projectRoot).then(() => {
|
||||
expect(settings.write).to.be.calledWith(projectRoot, { projectId: 'project-id-123' })
|
||||
return createCiProject({ foo: 'bar', projectRoot, configFile }).then(() => {
|
||||
expect(settings.write).to.be.calledWith(projectRoot, { projectId: 'project-id-123' }, { configFile })
|
||||
})
|
||||
})
|
||||
|
||||
it('returns project id', function () {
|
||||
return createCiProject({ foo: 'bar' }, projectRoot).then((projectId) => {
|
||||
return createCiProject({ foo: 'bar', projectRoot }).then((projectId) => {
|
||||
expect(projectId).to.eql(this.newProject)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Chai from 'chai'
|
||||
import { getSpecUrl, checkSupportFile } from '../../lib/project_utils'
|
||||
import Fixtures from '../support/helpers/fixtures'
|
||||
import path from 'path'
|
||||
import sinon from 'sinon'
|
||||
import { fs } from '../../lib/util/fs'
|
||||
import { getSpecUrl, checkSupportFile, getDefaultConfigFilePath } from '../../lib/project_utils'
|
||||
import Fixtures from '../support/helpers/fixtures'
|
||||
|
||||
const todosPath = Fixtures.projectPath('todos')
|
||||
|
||||
@@ -117,4 +119,58 @@ describe('lib/project_utils', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDefaultConfigFilePath', () => {
|
||||
let readdirStub
|
||||
const projectRoot = '/a/project/root'
|
||||
|
||||
beforeEach(() => {
|
||||
readdirStub = sinon.stub(fs, 'readdir')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
readdirStub.restore()
|
||||
})
|
||||
|
||||
it('finds cypress.json when present', async () => {
|
||||
readdirStub.withArgs(projectRoot).resolves(['cypress.json'])
|
||||
const ret = await getDefaultConfigFilePath(projectRoot)
|
||||
|
||||
expect(ret).to.equal('cypress.json')
|
||||
})
|
||||
|
||||
it('defaults to cypress.config.js when present', async () => {
|
||||
readdirStub.withArgs(projectRoot).resolves(['cypress.config.js'])
|
||||
const ret = await getDefaultConfigFilePath(projectRoot)
|
||||
|
||||
expect(ret).to.equal('cypress.config.js')
|
||||
})
|
||||
|
||||
it('defaults to cypress.json when no file is returned', async () => {
|
||||
readdirStub.withArgs(projectRoot).resolves([])
|
||||
const ret = await getDefaultConfigFilePath(projectRoot)
|
||||
|
||||
expect(ret).to.equal('cypress.json')
|
||||
})
|
||||
|
||||
it('errors if two default files are present', async () => {
|
||||
readdirStub.withArgs(projectRoot).resolves(['cypress.config.js', 'cypress.json'])
|
||||
try {
|
||||
await getDefaultConfigFilePath(projectRoot)
|
||||
throw Error('should have failed')
|
||||
} catch (err) {
|
||||
expect(err).to.have.property('type', 'CONFIG_FILES_LANGUAGE_CONFLICT')
|
||||
}
|
||||
})
|
||||
|
||||
it('errors if no file is present and we asked not to create any', async () => {
|
||||
readdirStub.withArgs(projectRoot).resolves([])
|
||||
try {
|
||||
await getDefaultConfigFilePath(projectRoot, false)
|
||||
throw Error('should have failed')
|
||||
} catch (err) {
|
||||
expect(err).to.have.property('type', 'NO_DEFAULT_CONFIG_FILE_FOUND')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
require('../spec_helper')
|
||||
|
||||
const path = require('path')
|
||||
const { fs } = require(`${root}lib/util/fs`)
|
||||
const settings = require(`${root}lib/util/settings`)
|
||||
|
||||
const projectRoot = process.cwd()
|
||||
|
||||
describe('lib/settings', () => {
|
||||
context('with no configFile option', () => {
|
||||
beforeEach(function () {
|
||||
this.setup = (obj = {}) => {
|
||||
return fs.writeJsonAsync('cypress.json', obj)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
return fs.removeAsync('cypress.json')
|
||||
})
|
||||
|
||||
context('nested cypress object', () => {
|
||||
it('flattens object on read', function () {
|
||||
return this.setup({ cypress: { foo: 'bar' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
|
||||
return fs.readJsonAsync('cypress.json')
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.readEnv', () => {
|
||||
afterEach(() => {
|
||||
return fs.removeAsync('cypress.env.json')
|
||||
})
|
||||
|
||||
it('parses json', () => {
|
||||
const json = { foo: 'bar', baz: 'quux' }
|
||||
|
||||
fs.writeJsonSync('cypress.env.json', json)
|
||||
|
||||
return settings.readEnv(projectRoot)
|
||||
.then((obj) => {
|
||||
expect(obj).to.deep.eq(json)
|
||||
})
|
||||
})
|
||||
|
||||
it('throws when invalid json', () => {
|
||||
fs.writeFileSync('cypress.env.json', '{\'foo;: \'bar}')
|
||||
|
||||
return settings.readEnv(projectRoot)
|
||||
.catch((err) => {
|
||||
expect(err.type).to.eq('ERROR_READING_FILE')
|
||||
expect(err.message).to.include('SyntaxError')
|
||||
|
||||
expect(err.message).to.include(projectRoot)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not write initial file', () => {
|
||||
return settings.readEnv(projectRoot)
|
||||
.then((obj) => {
|
||||
expect(obj).to.deep.eq({})
|
||||
}).then(() => {
|
||||
return fs.pathExists('cypress.env.json')
|
||||
}).then((found) => {
|
||||
expect(found).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.id', () => {
|
||||
beforeEach(function () {
|
||||
this.projectRoot = path.join(projectRoot, '_test-output/path/to/project/')
|
||||
|
||||
return fs.ensureDirAsync(this.projectRoot)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return fs.removeAsync(`${this.projectRoot}cypress.json`)
|
||||
})
|
||||
|
||||
it('returns project id for project', function () {
|
||||
return fs.writeJsonAsync(`${this.projectRoot}cypress.json`, {
|
||||
projectId: 'id-123',
|
||||
})
|
||||
.then(() => {
|
||||
return settings.id(this.projectRoot)
|
||||
}).then((id) => {
|
||||
expect(id).to.equal('id-123')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.read', () => {
|
||||
it('promises cypress.json', function () {
|
||||
return this.setup({ foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('promises cypress.json and merges CT specific properties for via testingType: component', function () {
|
||||
return this.setup({ a: 'b', component: { a: 'c' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, { testingType: 'component' })
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ a: 'c', component: { a: 'c' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('promises cypress.json and merges e2e specific properties', function () {
|
||||
return this.setup({ a: 'b', e2e: { a: 'c' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ a: 'c', e2e: { a: 'c' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames commandTimeout -> defaultCommandTimeout', function () {
|
||||
return this.setup({ commandTimeout: 30000, foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ defaultCommandTimeout: 30000, foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames supportFolder -> supportFile', function () {
|
||||
return this.setup({ supportFolder: 'foo', foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ supportFile: 'foo', foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames visitTimeout -> pageLoadTimeout', function () {
|
||||
return this.setup({ visitTimeout: 30000, foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ pageLoadTimeout: 30000, foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames visitTimeout -> pageLoadTimeout on nested cypress obj', function () {
|
||||
return this.setup({ cypress: { visitTimeout: 30000, foo: 'bar' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ pageLoadTimeout: 30000, foo: 'bar' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.write', () => {
|
||||
it('promises cypress.json updates', function () {
|
||||
return this.setup().then(() => {
|
||||
return settings.write(projectRoot, { foo: 'bar' })
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('only writes over conflicting keys', function () {
|
||||
return this.setup({ projectId: '12345', autoOpen: true })
|
||||
.then(() => {
|
||||
return settings.write(projectRoot, { projectId: 'abc123' })
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ projectId: 'abc123', autoOpen: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('with configFile: false', () => {
|
||||
beforeEach(function () {
|
||||
this.projectRoot = path.join(projectRoot, '_test-output/path/to/project/')
|
||||
|
||||
this.options = {
|
||||
configFile: false,
|
||||
}
|
||||
})
|
||||
|
||||
it('.exists passes', function () {
|
||||
return settings.exists(this.projectRoot, this.options)
|
||||
.then((exists) => {
|
||||
expect(exists).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
it('.write does not create a file', function () {
|
||||
return settings.write(this.projectRoot, {}, this.options)
|
||||
.then(() => {
|
||||
return fs.exists(path.join(this.projectRoot, 'cypress.json'))
|
||||
.then((exists) => {
|
||||
expect(exists).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('.read returns empty object', function () {
|
||||
return settings.read(this.projectRoot, this.options)
|
||||
.then((settings) => {
|
||||
expect(settings).to.deep.equal({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('with a configFile set', () => {
|
||||
beforeEach(function () {
|
||||
this.projectRoot = path.join(projectRoot, '_test-output/path/to/project/')
|
||||
|
||||
this.options = {
|
||||
configFile: 'my-test-config-file.json',
|
||||
}
|
||||
|
||||
this.optionsJs = {
|
||||
configFile: 'my-test-config-file.js',
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return fs.removeAsync(`${this.projectRoot}${this.options.configFile}`)
|
||||
})
|
||||
|
||||
it('.exists fails when configFile doesn\'t exist', function () {
|
||||
return settings.exists(this.projectRoot, this.options)
|
||||
.catch((error) => {
|
||||
expect(error.type).to.equal('CONFIG_FILE_NOT_FOUND')
|
||||
})
|
||||
})
|
||||
|
||||
it('.write creates configFile', function () {
|
||||
return settings.write(this.projectRoot, { foo: 'bar' }, this.options)
|
||||
.then(() => {
|
||||
return fs.readJsonAsync(path.join(this.projectRoot, this.options.configFile))
|
||||
.then((json) => {
|
||||
expect(json).to.deep.equal({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('.read returns from configFile', function () {
|
||||
return fs.writeJsonAsync(path.join(this.projectRoot, this.options.configFile), { foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(this.projectRoot, this.options)
|
||||
.then((settings) => {
|
||||
expect(settings).to.deep.equal({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('.read returns from configFile when its a JavaScript file', function () {
|
||||
return fs.writeFile(path.join(this.projectRoot, this.optionsJs.configFile), `module.exports = { baz: 'lurman' }`)
|
||||
.then(() => {
|
||||
return settings.read(this.projectRoot, this.optionsJs)
|
||||
.then((settings) => {
|
||||
expect(settings).to.deep.equal({ baz: 'lurman' })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,253 @@
|
||||
const path = require('path')
|
||||
|
||||
require('../../spec_helper')
|
||||
const setting = require(`../../../lib/util/settings`)
|
||||
const { fs } = require('../../../lib/util/fs')
|
||||
const settings = require(`../../../lib/util/settings`)
|
||||
|
||||
const projectRoot = process.cwd()
|
||||
const defaultOptions = {
|
||||
configFile: 'cypress.json',
|
||||
}
|
||||
|
||||
describe('lib/util/settings', () => {
|
||||
describe('pathToConfigFile', () => {
|
||||
context('with default configFile option', () => {
|
||||
beforeEach(function () {
|
||||
this.setup = (obj = {}) => {
|
||||
return fs.writeJsonAsync('cypress.json', obj)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
return fs.removeAsync('cypress.json')
|
||||
})
|
||||
|
||||
context('nested cypress object', () => {
|
||||
it('flattens object on read', function () {
|
||||
return this.setup({ cypress: { foo: 'bar' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
|
||||
return fs.readJsonAsync('cypress.json')
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.readEnv', () => {
|
||||
afterEach(() => {
|
||||
return fs.removeAsync('cypress.env.json')
|
||||
})
|
||||
|
||||
it('parses json', () => {
|
||||
const json = { foo: 'bar', baz: 'quux' }
|
||||
|
||||
fs.writeJsonSync('cypress.env.json', json)
|
||||
|
||||
return settings.readEnv(projectRoot)
|
||||
.then((obj) => {
|
||||
expect(obj).to.deep.eq(json)
|
||||
})
|
||||
})
|
||||
|
||||
it('throws when invalid json', () => {
|
||||
fs.writeFileSync('cypress.env.json', '{\'foo;: \'bar}')
|
||||
|
||||
return settings.readEnv(projectRoot)
|
||||
.catch((err) => {
|
||||
expect(err.type).to.eq('ERROR_READING_FILE')
|
||||
expect(err.message).to.include('SyntaxError')
|
||||
|
||||
expect(err.message).to.include(projectRoot)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not write initial file', () => {
|
||||
return settings.readEnv(projectRoot)
|
||||
.then((obj) => {
|
||||
expect(obj).to.deep.eq({})
|
||||
}).then(() => {
|
||||
return fs.pathExists('cypress.env.json')
|
||||
}).then((found) => {
|
||||
expect(found).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.id', () => {
|
||||
beforeEach(function () {
|
||||
this.projectRoot = path.join(projectRoot, '_test-output/path/to/project/')
|
||||
|
||||
return fs.ensureDirAsync(this.projectRoot)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
return fs.removeAsync(`${this.projectRoot}cypress.json`)
|
||||
})
|
||||
|
||||
it('returns project id for project', function () {
|
||||
return fs.writeJsonAsync(`${this.projectRoot}cypress.json`, {
|
||||
projectId: 'id-123',
|
||||
})
|
||||
.then(() => {
|
||||
return settings.id(this.projectRoot, defaultOptions)
|
||||
}).then((id) => {
|
||||
expect(id).to.equal('id-123')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.read', () => {
|
||||
it('promises cypress.json', function () {
|
||||
return this.setup({ foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('promises cypress.json and merges CT specific properties for via testingType: component', function () {
|
||||
return this.setup({ a: 'b', component: { a: 'c' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, { ...defaultOptions, testingType: 'component' })
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ a: 'c', component: { a: 'c' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('promises cypress.json and merges e2e specific properties', function () {
|
||||
return this.setup({ a: 'b', e2e: { a: 'c' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ a: 'c', e2e: { a: 'c' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames commandTimeout -> defaultCommandTimeout', function () {
|
||||
return this.setup({ commandTimeout: 30000, foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ defaultCommandTimeout: 30000, foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames supportFolder -> supportFile', function () {
|
||||
return this.setup({ supportFolder: 'foo', foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ supportFile: 'foo', foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames visitTimeout -> pageLoadTimeout', function () {
|
||||
return this.setup({ visitTimeout: 30000, foo: 'bar' })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ pageLoadTimeout: 30000, foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('renames visitTimeout -> pageLoadTimeout on nested cypress obj', function () {
|
||||
return this.setup({ cypress: { visitTimeout: 30000, foo: 'bar' } })
|
||||
.then(() => {
|
||||
return settings.read(projectRoot, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ pageLoadTimeout: 30000, foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('errors if in run mode and can\'t find file', function () {
|
||||
return settings.read(projectRoot, { ...defaultOptions, args: { runProject: 'path' } })
|
||||
.then(() => {
|
||||
throw Error('read should have failed with no config file in run mode')
|
||||
}).catch((err) => {
|
||||
expect(err.type).to.equal('CONFIG_FILE_NOT_FOUND')
|
||||
|
||||
return fs.access(path.join(projectRoot, 'cypress.json'))
|
||||
.then(() => {
|
||||
throw Error('file should not have been created here')
|
||||
}).catch((err) => {
|
||||
expect(err.code).to.equal('ENOENT')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.write', () => {
|
||||
it('promises cypress.json updates', function () {
|
||||
return this.setup().then(() => {
|
||||
return settings.write(projectRoot, { foo: 'bar' }, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
it('only writes over conflicting keys', function () {
|
||||
return this.setup({ projectId: '12345', autoOpen: true })
|
||||
.then(() => {
|
||||
return settings.write(projectRoot, { projectId: 'abc123' }, defaultOptions)
|
||||
}).then((obj) => {
|
||||
expect(obj).to.deep.eq({ projectId: 'abc123', autoOpen: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('with configFile: false', () => {
|
||||
beforeEach(function () {
|
||||
this.projectRoot = path.join(projectRoot, '_test-output/path/to/project/')
|
||||
|
||||
this.options = {
|
||||
configFile: false,
|
||||
}
|
||||
})
|
||||
|
||||
it('.write does not create a file', function () {
|
||||
return settings.write(this.projectRoot, {}, this.options)
|
||||
.then(() => {
|
||||
return fs.access(path.join(this.projectRoot, 'cypress.json'))
|
||||
.then(() => {
|
||||
throw Error('file shuold not have been created here')
|
||||
}).catch((err) => {
|
||||
expect(err.code).to.equal('ENOENT')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('.read returns empty object', function () {
|
||||
return settings.read(this.projectRoot, this.options)
|
||||
.then((settings) => {
|
||||
expect(settings).to.deep.equal({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('with js files', () => {
|
||||
it('.read returns from configFile when its a JavaScript file', function () {
|
||||
this.projectRoot = path.join(projectRoot, '_test-output/path/to/project/')
|
||||
|
||||
return fs.writeFile(path.join(this.projectRoot, 'cypress.custom.js'), `module.exports = { baz: 'lurman' }`)
|
||||
.then(() => {
|
||||
return settings.read(this.projectRoot, { configFile: 'cypress.custom.js' })
|
||||
.then((settings) => {
|
||||
expect(settings).to.deep.equal({ baz: 'lurman' })
|
||||
}).then(() => {
|
||||
return fs.remove(path.join(this.projectRoot, 'cypress.custom.js'))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.pathToConfigFile', () => {
|
||||
it('supports relative path', () => {
|
||||
const path = setting.pathToConfigFile('/users/tony/cypress', {
|
||||
const path = settings.pathToConfigFile('/users/tony/cypress', {
|
||||
configFile: 'e2e/config.json',
|
||||
})
|
||||
|
||||
@@ -12,7 +255,7 @@ describe('lib/util/settings', () => {
|
||||
})
|
||||
|
||||
it('supports absolute path', () => {
|
||||
const path = setting.pathToConfigFile('/users/tony/cypress', {
|
||||
const path = settings.pathToConfigFile('/users/tony/cypress', {
|
||||
configFile: '/users/pepper/cypress/e2e/cypress.config.json',
|
||||
})
|
||||
|
||||
|
||||
75
yarn.lock
75
yarn.lock
@@ -14981,7 +14981,7 @@ corser@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
|
||||
integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=
|
||||
|
||||
cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.0, cosmiconfig@^5.2.1:
|
||||
cosmiconfig@^5.0.0, cosmiconfig@^5.1.0, cosmiconfig@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
|
||||
integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
|
||||
@@ -17270,10 +17270,10 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromi
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf"
|
||||
integrity sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==
|
||||
|
||||
electron@13.2.0:
|
||||
version "13.2.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-13.2.0.tgz#54c8387359c6fa7aede2d06f9be21073afdfe616"
|
||||
integrity sha512-ZnRm1WWhHIKyoNAKVz7nPOHG42v5dhe0uqFsGW5x/KLK8kikHEXIduRnC4Y2XanckHeUFI9tZddWVSIBgqGBGg==
|
||||
electron@14.1.0:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-14.1.0.tgz#126361b7c2a38057004f888b94a52246a502157c"
|
||||
integrity sha512-MnZSITjtdrY6jM/z/qXcuJqbIvz7MbxHp9f1O93mq/vt7aTxHYgjerPSqwya/RoUjkPEm1gkz669FsRk6ZtMdQ==
|
||||
dependencies:
|
||||
"@electron/get" "^1.0.1"
|
||||
"@types/node" "^14.6.2"
|
||||
@@ -18875,6 +18875,17 @@ fast-glob@3.1.1:
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
fast-glob@3.2.7, fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.4:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
||||
integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-glob@^2.0.2, fast-glob@^2.2.6:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
|
||||
@@ -18887,18 +18898,6 @@ fast-glob@^2.0.2, fast-glob@^2.2.6:
|
||||
merge2 "^1.2.3"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.4:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
|
||||
integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.0"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.2"
|
||||
picomatch "^2.2.1"
|
||||
|
||||
fast-json-parse@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d"
|
||||
@@ -20205,11 +20204,6 @@ get-stdin@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
|
||||
integrity sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=
|
||||
|
||||
get-stdin@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6"
|
||||
integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==
|
||||
|
||||
get-stream@3.0.0, get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
@@ -20397,7 +20391,7 @@ glob-parent@^3.1.0:
|
||||
is-glob "^3.1.0"
|
||||
path-dirname "^1.0.0"
|
||||
|
||||
glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0, glob-parent@~5.1.2:
|
||||
glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@@ -21926,21 +21920,10 @@ humanize-ms@^1.2.1:
|
||||
dependencies:
|
||||
ms "^2.0.0"
|
||||
|
||||
husky@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-2.4.1.tgz#dd00f9646f8693b93f7b3a12ba4be00be0eff7ab"
|
||||
integrity sha512-ZRwMWHr7QruR22dQ5l3rEGXQ7rAQYsJYqaeCd+NyOsIFczAtqaApZQP3P4HwLZjCtFbm3SUNYoKuoBXX3AYYfw==
|
||||
dependencies:
|
||||
cosmiconfig "^5.2.0"
|
||||
execa "^1.0.0"
|
||||
find-up "^3.0.0"
|
||||
get-stdin "^7.0.0"
|
||||
is-ci "^2.0.0"
|
||||
pkg-dir "^4.1.0"
|
||||
please-upgrade-node "^3.1.1"
|
||||
read-pkg "^5.1.1"
|
||||
run-node "^1.0.0"
|
||||
slash "^3.0.0"
|
||||
husky@7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.2.tgz#21900da0f30199acca43a46c043c4ad84ae88dff"
|
||||
integrity sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==
|
||||
|
||||
hyphenate-style-name@^1.0.3:
|
||||
version "1.0.4"
|
||||
@@ -25472,17 +25455,16 @@ linkify-it@^3.0.1:
|
||||
dependencies:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
lint-staged@11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712"
|
||||
integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw==
|
||||
lint-staged@11.1.2:
|
||||
version "11.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.1.2.tgz#4dd78782ae43ee6ebf2969cad9af67a46b33cd90"
|
||||
integrity sha512-6lYpNoA9wGqkL6Hew/4n1H6lRqF3qCsujVT0Oq5Z4hiSAM7S6NksPJ3gnr7A7R52xCtiZMcEUNNQ6d6X5Bvh9w==
|
||||
dependencies:
|
||||
chalk "^4.1.1"
|
||||
cli-truncate "^2.1.0"
|
||||
commander "^7.2.0"
|
||||
cosmiconfig "^7.0.0"
|
||||
debug "^4.3.1"
|
||||
dedent "^0.7.0"
|
||||
enquirer "^2.3.6"
|
||||
execa "^5.0.0"
|
||||
listr2 "^3.8.2"
|
||||
@@ -30217,7 +30199,7 @@ platform@1.3.6:
|
||||
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
|
||||
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
|
||||
|
||||
please-upgrade-node@^3.1.1, please-upgrade-node@^3.2.0:
|
||||
please-upgrade-node@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
|
||||
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
|
||||
@@ -34409,11 +34391,6 @@ run-async@^2.2.0, run-async@^2.4.0:
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
|
||||
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
|
||||
|
||||
run-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e"
|
||||
integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
|
||||
Reference in New Issue
Block a user