Merge branch 'develop' into 9.0-release

This commit is contained in:
Zach Bloomquist
2021-10-06 21:55:30 +00:00
committed by GitHub
84 changed files with 1610 additions and 1012 deletions

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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 shouldnt need to use this command unless youre 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 shouldnt need to use this command unless youre 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 shouldnt need to use this command unless youre 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 shouldnt need to use this command unless youre 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 {

View File

@@ -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",

View 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)

View File

@@ -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",

View File

@@ -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}",

View File

@@ -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,
},
)

View 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 {}
}
}

View File

@@ -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, '/'))
}
}

View File

@@ -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}",

View File

@@ -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",

View File

@@ -147,6 +147,7 @@
"requestTimeout": 5000,
"resolvedNodePath": null,
"resolvedNodeVersion": "1.2.3",
"configFile": "cypress.json",
"hosts": {
"*.foobar.com": "127.0.0.1"
},

View File

@@ -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', () => {

View File

@@ -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' })
})
})

View File

@@ -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 {

View File

@@ -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></>
}

View File

@@ -103,3 +103,5 @@ export function stripSharedDirsFromDir2 (dir1, dir2, osName) {
.join(sep)
.value()
}
export const isFileJSON = (file) => file && /\.json$/.test(file)

View 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)

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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 = ')
})
})
})

View File

@@ -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', () => {

View File

@@ -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 () {

View File

@@ -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'],

View File

@@ -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))
})
})
})

View File

@@ -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[] = []

View File

@@ -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()

View File

@@ -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 `&nbsp;` in the text to `\us00a0` to find match.
// @see https://stackoverflow.com/a/53306311/1038927
return v.replace(/&nbsp;/g, '\u00a0')
return _.isNumber(v) ? v : v.replace(/&nbsp;/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(', ') },
})
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: {

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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) => {

View File

@@ -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"
},

View File

@@ -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.
`

View File

@@ -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
`

View File

@@ -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'

View File

@@ -0,0 +1 @@
export const CYPRESS_CONFIG_FILES = ['cypress.json', 'cypress.config.js', 'cypress.config.ts']

View File

@@ -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.

View File

@@ -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)

View File

@@ -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,
}))
})
}

View File

@@ -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 {

View File

@@ -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 = {}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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-*`,

View File

@@ -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
}

View File

@@ -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)
})

View File

@@ -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

View File

@@ -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')
}
}

View File

@@ -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: {

View File

@@ -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{

View File

@@ -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))

View File

@@ -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')
},
}

View 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')
}

View File

@@ -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')

View File

@@ -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,
})
})
})
})

View File

@@ -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)
})
})

View File

@@ -1,7 +1,9 @@
module.exports = {
const { defineConfig } = require('cypress')
module.exports = defineConfig({
pageLoadTimeout: 10000,
e2e: {
defaultCommandTimeout: 500,
videoCompression: 20,
},
}
})

View File

@@ -0,0 +1,9 @@
const config: Record<string, any> = {
pageLoadTimeout: 10000,
e2e: {
defaultCommandTimeout: 500,
videoCompression: 20,
},
}
export default config

View File

@@ -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)
})

View File

@@ -0,0 +1,7 @@
module.exports = {
pageLoadTimeout: 10000,
e2e: {
defaultCommandTimeout: 500,
videoCompression: 20,
},
}

View File

@@ -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)
})

View File

@@ -0,0 +1,7 @@
export default {
pageLoadTimeout: 10000,
e2e: {
defaultCommandTimeout: 500,
videoCompression: 20,
},
}

View File

@@ -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)
})

View File

@@ -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)}`, () => {

View File

@@ -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', () => {

View File

@@ -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',
})
})

View File

@@ -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-/)
})

View File

@@ -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', {

View File

@@ -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' })

View File

@@ -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`)

View File

@@ -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')

View File

@@ -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)
})
})

View File

@@ -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')
}
})
})
})

View File

@@ -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' })
})
})
})
})
})

View File

@@ -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',
})

View File

@@ -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"