diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index 38a106116023..95e15649c084 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -75,7 +75,7 @@ declare namespace Cypress { (task: 'firefox:force:gc'): Promise } - type BrowserName = 'electron' | 'chrome' | 'chromium' | 'firefox' | string + type BrowserName = 'electron' | 'chrome' | 'chromium' | 'firefox' | 'edge' | 'brave' | string type BrowserChannel = 'stable' | 'canary' | 'beta' | 'dev' | 'nightly' | string @@ -122,6 +122,8 @@ declare namespace Cypress { clear: (keys?: string[]) => void } + type IsBrowserMatcher = BrowserName | Partial + /** * Several libraries are bundled with Cypress by default. * @@ -335,11 +337,12 @@ declare namespace Cypress { isCy(obj: any): obj is Chainable /** - * Checks if you're running in the supplied browser family. - * e.g. isBrowser('Chrome') will be true for the browser 'Canary' - * @param name browser family name to check + * Returns true if currently running the supplied browser name or matcher object. + * @example isBrowser('chrome') will be true for the browser 'chrome:canary' and 'chrome:stable' + * @example isBrowser({ name: 'firefox' channel: 'dev' }) will be true only for the browser 'firefox:dev' (Firefox Developer Edition) + * @param matcher browser name or matcher object to check. */ - isBrowser(name: string): boolean + isBrowser(matcher: IsBrowserMatcher): boolean /** * Internal options for "cy.log" used in custom commands. @@ -2281,6 +2284,11 @@ declare namespace Cypress { firefoxGcInterval: Nullable, openMode: Nullable }> } + interface TestConfigOptions extends Partial> { + // retries?: number + browser?: IsBrowserMatcher | IsBrowserMatcher[] + } + interface DebugOptions { verbose: boolean } @@ -4662,3 +4670,67 @@ Cypress._ // => Lodash _ ``` */ declare const Cypress: Cypress.Cypress & EventEmitter + +declare namespace Mocha { + interface TestFunction { + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn?: Func): Test + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn?: AsyncFunc): Test + } + interface ExclusiveTestFunction { + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn?: Func): Test + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn?: AsyncFunc): Test + } + interface PendingTestFunction { + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn?: Func): Test + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn?: AsyncFunc): Test + } + + interface SuiteFunction { + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, config: Cypress.TestConfigOptions, fn: (this: Suite) => void): Suite + } +} diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index c6edab2dc297..782a9f55c131 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -338,3 +338,31 @@ namespace CypressLocationTests { cy.location('path') // $ExpectError cy.location('pathname') // $ExpectType Chainable } + +namespace CypressBrowserTests { + Cypress.isBrowser('chrome')// $ExpectType boolean + Cypress.isBrowser('firefox')// $ExpectType boolean + Cypress.isBrowser('edge')// $ExpectType boolean + Cypress.isBrowser('brave')// $ExpectType boolean + Cypress.isBrowser({channel: 'stable'})// $ExpectType boolean + Cypress.isBrowser({family: 'chromium'})// $ExpectType boolean + Cypress.isBrowser({name: 'chrome'})// $ExpectType boolean + Cypress.isBrowser({family: 'foo'}) // $ExpectError + Cypress.isBrowser() // $ExpectError +} + +namespace CypressTestConfigTests { + it('test', { + browser: {name: 'firefox'} + }, () => {}) + it('test', { + browser: [{name: 'firefox'}, {name: 'chrome'}] + }, () => {}) + it('test', { + baseUrl: 'www.foobar.com', + browser: 'firefox' + }, () => {}) + it('test', { + browser: {foo: 'bar'} // $ExpectError + }, () => {}) +} diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index 580405f51623..499214a61453 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -18,6 +18,69 @@ const runnableResetTimeout = Runnable.prototype.resetTimeout delete window.mocha delete window.Mocha +function overrideMochaIt (specWindow) { + const _it = specWindow.it + + function overrideIt (fn) { + specWindow.it = fn() + specWindow.it['only'] = fn('only') + specWindow.it['skip'] = fn('skip') + } + + overrideIt(function (subFn) { + return function (...args) { + /** + * @type {Cypress.Cypress} + */ + const Cypress = specWindow.Cypress + + function shouldRunBrowser (browserlist, browser) { + if (browserlist === null) { + return true + } + + if (!_.isArray(browserlist)) { + browserlist = [browserlist] + } + + return _.some(browserlist, Cypress.isBrowser) + } + + const origIt = subFn ? _it[subFn] : _it + + if (args.length > 2 && _.isObject(args[1])) { + const opts = _.defaults({}, args[1], { + browser: null, + }) + + const mochaArgs = [args[0], args[2]] + + if (!shouldRunBrowser(opts.browser)) { + mochaArgs[0] = `[browser skip (${opts.browser})]${mochaArgs[0]}` + + if (subFn === 'only') { + mochaArgs[1] = function () { + this.skip() + } + + return origIt.apply(this, mochaArgs) + } + + return _it['skip'].apply(this, mochaArgs) + } + + const ret = origIt.apply(this, mochaArgs) + + ret.testConfiguration = opts + + return ret + } + + return origIt.apply(this, args) + } + }) +} + const ui = (specWindow, _mocha) => { // Override mocha.ui so that the pre-require event is emitted // with the iframe's `window` reference, rather than the parent's. @@ -34,13 +97,15 @@ const ui = (specWindow, _mocha) => { // such as describe, it, before, beforeEach, etc this.suite.emit('pre-require', specWindow, null, this) + overrideMochaIt(specWindow) + return this } return _mocha.ui('bdd') } -const set = (specWindow, _mocha) => { +const setMochaProps = (specWindow, _mocha) => { // Mocha is usually defined in the spec when used normally // in the browser or node, so we add it as a global // for our users too @@ -67,7 +132,7 @@ const globals = (specWindow, reporter) => { }) // set mocha props on the specWindow - set(specWindow, _mocha) + setMochaProps(specWindow, _mocha) // return the newly created mocha instance return _mocha diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index 7a42738b482c..b83e63626af9 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -17,7 +17,7 @@ const TEST_AFTER_RUN_EVENT = 'runner:test:after:run' const ERROR_PROPS = 'message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending'.split(' ') const RUNNABLE_LOGS = 'routes agents commands'.split(' ') -const RUNNABLE_PROPS = 'id order title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file'.split(' ') +const RUNNABLE_PROPS = 'id order title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file testConfiguration'.split(' ') // ## initial payload // { diff --git a/packages/driver/test/cypress/integration/e2e/test-config.spec.js b/packages/driver/test/cypress/integration/e2e/test-config.spec.js new file mode 100644 index 000000000000..893504fbcbb5 --- /dev/null +++ b/packages/driver/test/cypress/integration/e2e/test-config.spec.js @@ -0,0 +1,106 @@ +/* eslint-disable @cypress/dev/skip-comment,mocha/no-exclusive-tests */ +/// +// @ts-check + +const testState = { + ranFirefox: false, + ranChrome: false, +} + +describe('per-test config', () => { + after(() => { + if (Cypress.browser.name === 'firefox') { + return expect(testState).deep.eq({ + ranChrome: false, + ranFirefox: true, + }) + } + + if (Cypress.browser.name === 'chrome') { + return expect(testState).deep.eq({ + ranChrome: true, + ranFirefox: false, + }) + } + + throw new Error('should have made assertion') + }) + + it('set various config values', { + defaultCommandTimeout: 200, + env: { + FOO_VALUE: 'foo', + }, + }, () => { + expect(Cypress.config().defaultCommandTimeout).eq(200) + expect(Cypress.config('defaultCommandTimeout')).eq(200) + expect(Cypress.env('FOO_VALUE')).eq('foo') + }) + + it('does not leak various config values', { + }, () => { + expect(Cypress.config().defaultCommandTimeout).not.eq(200) + expect(Cypress.config('defaultCommandTimeout')).not.eq(200) + expect(Cypress.env('FOO_VALUE')).not.eq('foo') + }) + + it('can set viewport', { + viewportWidth: 400, + viewportHeight: 200, + }, () => { + expect(Cypress.config().viewportHeight).eq(200) + expect(Cypress.config().viewportWidth).eq(400) + }) + + it('can specify only run in chrome', { + browser: 'chrome', + }, () => { + testState.ranChrome = true + expect(Cypress.browser.name).eq('chrome') + }) + + it('can specify only run in firefox', { + browser: 'firefox', + }, () => { + testState.ranFirefox = true + expect(Cypress.browser.name).eq('firefox') + }) + + describe('in beforeEach', () => { + it('set various config values', { + defaultCommandTimeout: 200, + env: { + FOO_VALUE: 'foo', + }, + }, () => { + expect(Cypress.config().defaultCommandTimeout).eq(200) + expect(Cypress.config('defaultCommandTimeout')).eq(200) + expect(Cypress.env('FOO_VALUE')).eq('foo') + }) + + beforeEach(() => { + expect(Cypress.config().defaultCommandTimeout).eq(200) + expect(Cypress.config('defaultCommandTimeout')).eq(200) + expect(Cypress.env('FOO_VALUE')).eq('foo') + }) + }) + + describe('in afterEach', () => { + it('set various config values', { + defaultCommandTimeout: 200, + env: { + FOO_VALUE: 'foo', + }, + }, () => { + expect(Cypress.config().defaultCommandTimeout).eq(200) + expect(Cypress.config('defaultCommandTimeout')).eq(200) + expect(Cypress.env('FOO_VALUE')).eq('foo') + }) + + afterEach(() => { + expect(Cypress.config().defaultCommandTimeout).eq(200) + expect(Cypress.config('defaultCommandTimeout')).eq(200) + expect(Cypress.env('FOO_VALUE')).eq('foo') + }) + }) +}) diff --git a/packages/driver/test/cypress/support/defaults.coffee b/packages/driver/test/cypress/support/defaults.coffee index a53bebd7564d..535cf88e9d70 100644 --- a/packages/driver/test/cypress/support/defaults.coffee +++ b/packages/driver/test/cypress/support/defaults.coffee @@ -4,21 +4,21 @@ $ = Cypress.$ ## backup the original config ORIG_CONFIG = _.clone(Cypress.config()) -beforeEach -> - ## restore it before each test - Cypress.config(ORIG_CONFIG) +# beforeEach -> +# ## restore it before each test +# Cypress.config(ORIG_CONFIG) - ## always set that we're interactive so we - ## get consistent passes and failures when running - ## from CI and when running in GUI mode - Cypress.config("isInteractive", true) - ## necessary or else snapshots will not be taken - ## and we can't test them - Cypress.config("numTestsKeptInMemory", 1) +# ## always set that we're interactive so we +# ## get consistent passes and failures when running +# ## from CI and when running in GUI mode +# Cypress.config("isInteractive", true) +# ## necessary or else snapshots will not be taken +# ## and we can't test them +# Cypress.config("numTestsKeptInMemory", 1) - ## remove all event listeners - ## from the window - ## this could fail if this window - ## is a cross origin window - try - $(cy.state("window")).off() +# ## remove all event listeners +# ## from the window +# ## this could fail if this window +# ## is a cross origin window +# try +# $(cy.state("window")).off() diff --git a/packages/runner/src/lib/event-manager.js b/packages/runner/src/lib/event-manager.js index c1034b32658e..951e83634394 100644 --- a/packages/runner/src/lib/event-manager.js +++ b/packages/runner/src/lib/event-manager.js @@ -30,6 +30,9 @@ const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') const localBus = new EventEmitter() const reporterBus = new EventEmitter() +/** + * @type {Cypress.Cypress} + */ let Cypress const eventManager = { @@ -238,6 +241,49 @@ const eventManager = { ws.emit('client:request', msg, data, cb) }) + let restoreConfigurationFn = null + + Cypress.on('test:before:run', (test) => { + /** + * @type {Cypress.TestConfigOptions} + */ + const testConfig = test.testConfiguration + + restoreConfigurationFn = null + if (!testConfig) return + + const config = Cypress.config() + + const backupConfig = _.clone(config) + + _.extend(config, _.omit(testConfig, 'browser')) + + let backupEnv + let env + + if (testConfig.env) { + env = Cypress.env() + backupEnv = _.clone(env) + _.extend(env, testConfig.env) + } + + restoreConfigurationFn = function () { + _.extend(config, backupConfig) + console.log(backupEnv) + if (env) { + Object.keys(env).forEach((key) => { + delete env[key] + }) + + _.extend(env, backupEnv) + } + } + }) + + Cypress.on('test:after:run', (test) => { + restoreConfigurationFn && restoreConfigurationFn() + }) + _.each(driverToSocketEvents, (event) => { Cypress.on(event, (...args) => { return ws.emit(event, ...args)