diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index c16990c9cd69..081d482fd703 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -8,6 +8,10 @@ _Released 03/14/2023 (PENDING)_ - It is now possible to control the number of connection attempts to the browser using the CYPRESS_CONNECT_RETRY_THRESHOLD Environment Variable. Learn more [here](https://docs.cypress.io/guides/references/advanced-installation#Environment-variables). Addressed in [#25848](https://github.com/cypress-io/cypress/pull/25848). - The Debug page is now able to show real-time results from in-progress runs. Addresses [#25759](https://github.com/cypress-io/cypress/issues/25759). +**Bugfixes:** + +- Fixed an issue where using `Cypress.require()` would throw the error `Cannot find module 'typescript'`. Fixes [#25885](https://github.com/cypress-io/cypress/issues/25885). + **Misc:** - Removed "New" badge in the navigation bar for the debug page icon. Addresses [#25925](https://github.com/cypress-io/cypress/issues/25925) diff --git a/npm/webpack-batteries-included-preprocessor/index.js b/npm/webpack-batteries-included-preprocessor/index.js index 897a5dbecbd6..05130765dc02 100644 --- a/npm/webpack-batteries-included-preprocessor/index.js +++ b/npm/webpack-batteries-included-preprocessor/index.js @@ -13,16 +13,16 @@ const hasTsLoader = (rules) => { const addTypeScriptConfig = (file, options) => { // shortcut if we know we've already added typescript support - if (options.__typescriptSupportAdded) return + if (options.__typescriptSupportAdded) return options const webpackOptions = options.webpackOptions const rules = webpackOptions.module && webpackOptions.module.rules // if there are no rules defined or it's not an array, we can't add to them - if (!rules || !Array.isArray(rules)) return + if (!rules || !Array.isArray(rules)) return options // if we find ts-loader configured, don't add it again - if (hasTsLoader(rules)) return + if (hasTsLoader(rules)) return options const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin') // node will try to load a projects tsconfig.json instead of the node @@ -57,6 +57,8 @@ const addTypeScriptConfig = (file, options) => { })] options.__typescriptSupportAdded = true + + return options } /** @@ -163,7 +165,7 @@ const preprocessor = (options = {}) => { options.webpackOptions = options.webpackOptions || getDefaultWebpackOptions() if (options.typescript) { - addTypeScriptConfig(file, options) + options = addTypeScriptConfig(file, options) } if (process.versions.pnp) { @@ -181,13 +183,13 @@ preprocessor.defaultOptions = { } preprocessor.getFullWebpackOptions = (filePath, typescript) => { - const options = { typescript } - - options.webpackOptions = getDefaultWebpackOptions() + const webpackOptions = getDefaultWebpackOptions() - addTypeScriptConfig({ filePath }, options) + if (typescript) { + return addTypeScriptConfig({ filePath }, { typescript, webpackOptions }).webpackOptions + } - return options.webpackOptions + return webpackOptions } // for testing purposes, but do not add this to the typescript interface diff --git a/npm/webpack-batteries-included-preprocessor/package.json b/npm/webpack-batteries-included-preprocessor/package.json index d1ba6f9a3c73..5ddc6c3bb876 100644 --- a/npm/webpack-batteries-included-preprocessor/package.json +++ b/npm/webpack-batteries-included-preprocessor/package.json @@ -4,7 +4,7 @@ "description": "Cypress preprocessor for bundling JavaScript via webpack with dependencies included and support for various ES features, TypeScript, and CoffeeScript", "private": false, "scripts": { - "test": "mocha test/e2e/*.spec.* --timeout 4000", + "test": "mocha test/**/*.spec.* --timeout 4000", "lint": "eslint --ext .js,.ts,.json, ." }, "dependencies": { diff --git a/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js b/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js index 813870f23efb..ff5e436ed771 100644 --- a/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js +++ b/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js @@ -24,7 +24,7 @@ const runAndEval = async (fileName, options) => { eval(contents.toString()) } -describe('features', () => { +describe('webpack-batteries-included-preprocessor features', () => { beforeEach(async () => { preprocessor.__reset() diff --git a/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js b/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js new file mode 100644 index 000000000000..d0c7cc82cf08 --- /dev/null +++ b/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js @@ -0,0 +1,22 @@ +const { expect } = require('chai') + +const preprocessor = require('../../index') + +describe('webpack-batteries-included-preprocessor', () => { + context('#getFullWebpackOptions', () => { + it('returns default webpack options (and does not add typescript config if no path specified)', () => { + const result = preprocessor.getFullWebpackOptions() + + expect(result.node.global).to.be.true + expect(result.module.rules).to.have.length(3) + expect(result.resolve.extensions).to.eql(['.js', '.json', '.jsx', '.mjs', '.coffee']) + }) + + it('adds typescript config if path is specified', () => { + const result = preprocessor.getFullWebpackOptions('file/path', 'typescript/path') + + expect(result.module.rules).to.have.length(4) + expect(result.module.rules[3].use[0].loader).to.include('ts-loader') + }) + }) +}) diff --git a/packages/app/cypress/e2e/settings.cy.ts b/packages/app/cypress/e2e/settings.cy.ts index 89b693907bcd..d64dafc6f460 100644 --- a/packages/app/cypress/e2e/settings.cy.ts +++ b/packages/app/cypress/e2e/settings.cy.ts @@ -156,8 +156,8 @@ describe('App: Settings', () => { cy.visitApp() cy.get(SidebarSettingsLinkSelector).click() cy.findByText('Project settings').click() - cy.get('[data-cy="file-match-indicator"]').contains('41 matches') - cy.get('[data-cy="spec-pattern"]').contains('tests/**/*') + cy.get('[data-cy="file-match-indicator"]').contains('19 matches') + cy.get('[data-cy="spec-pattern"]').contains('tests/**/*.(js|ts|coffee)') }) it('shows the Experiments section', () => { @@ -237,7 +237,7 @@ describe('App: Settings', () => { cy.get('[data-cy="config-code"]').within(() => { cy.get('[data-cy-config="config"]').contains('tests/_fixtures') - cy.get('[data-cy-config="config"]').contains('tests/**/*') + cy.get('[data-cy-config="config"]').contains('tests/**/*.(js|ts|coffee)') cy.get('[data-cy-config="config"]').contains('tests/_support/spec_helper.js') cy.get('[data-cy-config="env"]').contains('REMOTE_DEBUGGING_PORT') cy.get('[data-cy-config="env"]').contains('INTERNAL_E2E_TESTING_SELF') diff --git a/packages/driver/src/cross-origin/origin_fn.ts b/packages/driver/src/cross-origin/origin_fn.ts index b4966858bfe8..ac466ad42a8c 100644 --- a/packages/driver/src/cross-origin/origin_fn.ts +++ b/packages/driver/src/cross-origin/origin_fn.ts @@ -82,8 +82,13 @@ const getCallbackFn = async (fn: string, file?: string) => { // in the outer scope (see the return value below), assign the function to it // in the inner scope, then call the function with the args const callbackName = '__cypressCallback' + const response = await fetch('/__cypress/process-origin-callback', { - body: JSON.stringify({ file, fn: `${callbackName} = ${fn};` }), + body: JSON.stringify({ + file, + fn: `${callbackName} = ${fn};`, + projectRoot: Cypress.config('projectRoot'), + }), headers: { 'Content-Type': 'application/json', }, diff --git a/packages/server/lib/cross-origin/process-callback.ts b/packages/server/lib/plugins/child/cross_origin.js similarity index 59% rename from packages/server/lib/cross-origin/process-callback.ts rename to packages/server/lib/plugins/child/cross_origin.js index 6db08cabeb01..97d711753641 100644 --- a/packages/server/lib/cross-origin/process-callback.ts +++ b/packages/server/lib/plugins/child/cross_origin.js @@ -1,22 +1,19 @@ -import { getFullWebpackOptions } from '@cypress/webpack-batteries-included-preprocessor' -import md5 from 'md5' -import { fs } from 'memfs' -import * as path from 'path' -import webpack from 'webpack' - +const md5 = require('md5') +const { fs } = require('memfs') +const path = require('path') +const webpack = require('webpack') const VirtualModulesPlugin = require('webpack-virtual-modules') -interface Options { - file: string - fn: string -} +const resolve = require('../../util/resolve') -// @ts-expect-error - webpack expects `fs.join` to exist for some reason fs.join = path.join -export const processCallback = ({ file, fn }: Options) => { +const processCallback = ({ file, fn, projectRoot }) => { + const { getFullWebpackOptions } = require('@cypress/webpack-batteries-included-preprocessor') + const source = fn.replace(/Cypress\.require/g, 'require') - const webpackOptions = getFullWebpackOptions(file, require.resolve('typescript')) + const typescriptPath = resolve.typescript(projectRoot) + const webpackOptions = getFullWebpackOptions(file, typescriptPath) const inputFileName = md5(source) const inputDir = path.dirname(file) @@ -45,17 +42,16 @@ export const processCallback = ({ file, fn }: Options) => { const compiler = webpack(modifiedWebpackOptions) - // @ts-expect-error compiler.outputFileSystem = fs - return new Promise((resolve, reject) => { - const handle = (err: Error) => { + return new Promise((resolve, reject) => { + const handle = (err) => { if (err) { return reject(err) } - // Using an in-memory file system, so the usual restrictions on sync - // methods don't apply, since this won't throw an EMFILE error + // this won't throw an EMFILE error since it's using an in-memory file + // system, so the usual restrictions on sync methods don't apply // eslint-disable-next-line no-restricted-syntax const result = fs.readFileSync(outputPath).toString() @@ -65,3 +61,7 @@ export const processCallback = ({ file, fn }: Options) => { compiler.run(handle) }) } + +module.exports = { + processCallback, +} diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index 4e7fee4afef1..f86a35d5cdf9 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -14,6 +14,7 @@ const resolve = require('../../util/resolve') const browserLaunch = require('./browser_launch') const util = require('../util') const validateEvent = require('./validate_event') +const crossOrigin = require('./cross_origin') const UNDEFINED_SERIALIZED = '__cypress_undefined__' @@ -37,23 +38,25 @@ class RunPlugins { this.registeredEventsByName = {} } - invoke = (eventId, args = []) => { - const event = this.registeredEventsById[eventId] - - return event.handler(...args) - } - - getDefaultPreprocessor (config) { - const tsPath = resolve.typescript(config.projectRoot) - const options = { - ...tsPath && { typescript: tsPath }, + /** + * This is the only publicly-used method of this class + * + * @param {Object} config + * @param {Function} setupNodeEventsFn + */ + runSetupNodeEvents (config, setupNodeEventsFn) { + debug('project root:', this.projectRoot) + if (!this.projectRoot) { + throw new Error('Unexpected: projectRoot should be a string') } - debug('creating webpack preprocessor with options %o', options) + debug('passing config %o', config) - const webpackPreprocessor = require('@cypress/webpack-batteries-included-preprocessor') + this.ipc.on('execute:plugins', (event, ids, args) => { + this.execute(event, ids, args) + }) - return webpackPreprocessor(options) + return this.load(config, setupNodeEventsFn) } load (initialConfig, setupNodeEvents) { @@ -110,8 +113,9 @@ class RunPlugins { // events used for parent/child communication registerChildEvent('_get:task:body', () => {}) registerChildEvent('_get:task:keys', () => {}) + registerChildEvent('_process:cross:origin:callback', crossOrigin.processCallback) - Promise + return Promise .try(() => { debug('Calling setupNodeEvents') @@ -120,7 +124,7 @@ class RunPlugins { .tap(() => { if (!this.registeredEventsByName['file:preprocessor']) { debug('register default preprocessor') - registerChildEvent('file:preprocessor', this.getDefaultPreprocessor(initialConfig)) + registerChildEvent('file:preprocessor', this._getDefaultPreprocessor(initialConfig)) } }) .then((modifiedCfg) => { @@ -156,6 +160,7 @@ class RunPlugins { case 'after:run': case 'after:spec': case 'after:screenshot': + case '_process:cross:origin:callback': return util.wrapChildPromise(this.ipc, this.invoke, ids, args) case 'task': return this.taskExecute(ids, args) @@ -172,6 +177,12 @@ class RunPlugins { } } + invoke = (eventId, args = []) => { + const event = this.registeredEventsById[eventId] + + return event.handler(...args) + } + wrapChildPromise (invoke, ids, args = []) { return Promise.try(() => { return invoke(ids.eventId, args) @@ -191,9 +202,9 @@ class RunPlugins { taskGetBody (ids, args) { const [event] = args - const taskEvent = _.find(this.registeredEventsById, { event: 'task' }).handler + const taskEvent = _.find(this.registeredEventsById, { event: 'task' }) const invoke = () => { - const fn = taskEvent[event] + const fn = taskEvent && taskEvent.handler[event] return _.isFunction(fn) ? fn.toString() : '' } @@ -202,8 +213,8 @@ class RunPlugins { } taskGetKeys (ids) { - const taskEvent = _.find(this.registeredEventsById, { event: 'task' }).handler - const invoke = () => _.keys(taskEvent) + const taskEvent = _.find(this.registeredEventsById, { event: 'task' }) + const invoke = () => _.keys(taskEvent ? taskEvent.handler : {}) util.wrapChildPromise(this.ipc, invoke, ids) } @@ -241,22 +252,17 @@ class RunPlugins { util.wrapChildPromise(this.ipc, invoke, ids, [arg]) } - /** - * - * @param {Function} setupNodeEventsFn - */ - runSetupNodeEvents (config, setupNodeEventsFn) { - debug('project root:', this.projectRoot) - if (!this.projectRoot) { - throw new Error('Unexpected: projectRoot should be a string') + _getDefaultPreprocessor (config) { + const tsPath = resolve.typescript(config.projectRoot) + const options = { + ...tsPath && { typescript: tsPath }, } - debug('passing config %o', config) - this.load(config, setupNodeEventsFn) + debug('creating webpack preprocessor with options %o', options) - this.ipc.on('execute:plugins', (event, ids, args) => { - this.execute(event, ids, args) - }) + const webpackPreprocessor = require('@cypress/webpack-batteries-included-preprocessor') + + return webpackPreprocessor(options) } } diff --git a/packages/server/lib/plugins/child/validate_event.js b/packages/server/lib/plugins/child/validate_event.js index 561377e95895..0a82f902d7d8 100644 --- a/packages/server/lib/plugins/child/validate_event.js +++ b/packages/server/lib/plugins/child/validate_event.js @@ -26,6 +26,7 @@ const isObject = (event, handler) => { const eventValidators = { '_get:task:body': isFunction, '_get:task:keys': isFunction, + '_process:cross:origin:callback': isFunction, 'after:run': isFunction, 'after:screenshot': isFunction, 'after:spec': isFunction, diff --git a/packages/server/lib/routes-e2e.ts b/packages/server/lib/routes-e2e.ts index b75efd74208a..426c62ee56a8 100644 --- a/packages/server/lib/routes-e2e.ts +++ b/packages/server/lib/routes-e2e.ts @@ -11,7 +11,7 @@ import reporter from './controllers/reporter' import client from './controllers/client' import files from './controllers/files' import type { InitializeRoutes } from './routes' -import { processCallback } from './cross-origin/process-callback' +import * as plugins from './plugins' const debug = Debug('cypress:server:routes-e2e') @@ -54,11 +54,11 @@ export const createRoutesE2E = ({ routesE2E.post(`/${config.namespace}/process-origin-callback`, bodyParser.json(), async (req, res) => { try { - const { file, fn } = req.body + const { file, fn, projectRoot } = req.body debug('process origin callback: %s', fn) - const contents = await processCallback({ file, fn }) + const contents = await plugins.execute('_process:cross:origin:callback', { file, fn, projectRoot }) res.json({ contents }) } catch (err) { diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.js b/packages/server/test/unit/plugins/child/run_plugins_spec.js index 285a9761bde8..f87555f27c0f 100644 --- a/packages/server/test/unit/plugins/child/run_plugins_spec.js +++ b/packages/server/test/unit/plugins/child/run_plugins_spec.js @@ -1,433 +1,278 @@ require('../../../spec_helper') const _ = require('lodash') -const snapshot = require('snap-shot-it') -const Promise = require('bluebird') - -const preprocessor = require(`../../../../lib/plugins/child/preprocessor`) -const util = require(`../../../../lib/plugins/util`) -const resolve = require(`../../../../lib/util/resolve`) -const browserUtils = require(`../../../../lib/browsers/utils`) -const Fixtures = require('@tooling/system-tests') -const { RunPlugins } = require(`../../../../lib/plugins/child/run_plugins`) -const { deferred } = require('../../../support/helpers/deferred') - -const colorCodeRe = /\[[0-9;]+m/gm -const pathRe = /\/?([a-z0-9_-]+\/)*[a-z0-9_-]+\/([a-z_]+\.\w+)[:0-9]+/gmi - -const withoutColorCodes = (str) => { - return str.replace(colorCodeRe, '') -} -const withoutPath = (str) => { - return str.replace(pathRe, '$2)') -} - -// TODO: tim, come back to this later -describe.skip('lib/plugins/child/run_plugins', () => { + +const preprocessor = require('../../../../lib/plugins/child/preprocessor') +const util = require('../../../../lib/plugins/util') +const resolve = require('../../../../lib/util/resolve') +const browserUtils = require('../../../../lib/browsers/utils') +const { RunPlugins } = require('../../../../lib/plugins/child/run_plugins') +const crossOrigin = require('../../../../lib/plugins/child/cross_origin') + +describe('lib/plugins/child/run_plugins', () => { + let ipc let runPlugins - beforeEach(function () { - this.ipc = { + beforeEach(() => { + ipc = { send: sinon.spy(), on: sinon.stub(), removeListener: sinon.spy(), } - runPlugins = new RunPlugins(this.ipc, 'proj-root', 'cypress.config.js') + runPlugins = new RunPlugins(ipc, 'proj-root', 'cypress.config.js') }) afterEach(() => { mockery.deregisterMock('@cypress/webpack-batteries-included-preprocessor') }) - it('sends error message if configFile has syntax error', function () { - // path for substitute is relative to lib/plugins/child/plugins_child.js - mockery.registerSubstitute( - 'plugins-file', - Fixtures.path('server/syntax_error.js'), - ) - - runPlugins(this.ipc, 'plugins-file', 'proj-root') - expect(this.ipc.send).to.be.calledWith('load:error', 'PLUGINS_FILE_ERROR', 'plugins-file') - - return snapshot(withoutColorCodes(withoutPath(this.ipc.send.lastCall.args[3].stack.replace(/( +at[^$]+$)+/g, '[stack trace]')))) - }) - - it('sends error message if setupNodeEvents does not export a function', function () { - mockery.registerMock('plugins-file', null) - runPlugins(this.ipc, 'plugins-file', 'proj-root') - expect(this.ipc.send).to.be.calledWith('load:error', 'SETUP_NODE_EVENTS_IS_NOT_FUNCTION', 'plugins-file') - }) - - it('sends error message if setupNodeEvents is not a function', function () { - const config = { projectRoot: '/project/root' } - - const setupNodeEventsFn = (on, config) => { - on('dev-server:start', (options) => {}) - on('after:screenshot', () => {}) - on('task', {}) - - return config - } - - const foo = ((on, config) => { - on('dev-server:start', (options) => {}) - - return setupNodeEventsFn(on, config) - }) - - runPlugins.runSetupNodeEvents(foo, setupNodeEventsFn) + context('#runSetupNodeEvents', () => { + let config + let setupNodeEventsFn - this.ipc.on.withArgs('setupTestingType').yield(config) + beforeEach(() => { + config = { projectRoot: '/project/root' } - return Promise - .delay(10) - .then(() => { - expect(this.ipc.send).to.be.calledWith('setupTestingType:reply', config) - expect(this.ipc.send).to.be.calledWith('setupTestingType:error', 'SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER', 'cypress.config.js') - }) - }) - - describe('on \'load\' message', () => { - it('sends loaded event with registrations', function () { - const pluginsDeferred = deferred() - const config = { projectRoot: '/project/root' } - - const setupNodeEventsFn = (on) => { + setupNodeEventsFn = sinon.stub().callsFake((on) => { on('after:screenshot', () => {}) on('task', {}) - return config - } - - runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - - this.ipc.on.withArgs('load:plugins').yield(config) - - pluginsDeferred.resolve(config) - - return Promise - .delay(10) - .then(() => { - expect(this.ipc.send).to.be.calledWith('setupTestingType:reply', config) - const registrations = this.ipc.send.lastCall.args[2] - - expect(registrations).to.have.length(5) - - expect(_.map(registrations, 'event')).to.eql([ - '_get:task:body', - '_get:task:keys', - 'after:screenshot', - 'task', - 'file:preprocessor', - ]) + return { includeShadowDom: true } }) }) - it('registers default preprocessor if none registered by user', function () { - const pluginsDeferred = deferred() - const config = { projectRoot: '/project/root' } - const webpackPreprocessorFn = sinon.spy() - const webpackPreprocessor = sinon.stub().returns(webpackPreprocessorFn) - - sinon.stub(resolve, 'typescript').returns('/path/to/typescript.js') + describe('#load', () => { + it('calls setupNodeEventsFn with `registerChildEvent` function and initial config', async () => { + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - const setupNodeEventsFn = (on) => { - on('after:screenshot', () => {}) - on('task', {}) - - return config - } - - mockery.registerMock('@cypress/webpack-batteries-included-preprocessor', webpackPreprocessor) + expect(setupNodeEventsFn).to.be.calledWith(sinon.match.func, config) + }) - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + it('registers default preprocessor if none registered by user', async () => { + const webpackPreprocessorFn = sinon.spy() + const webpackPreprocessor = sinon.stub().returns(webpackPreprocessorFn) - this.ipc.on.withArgs('load:plugins').yield(config) + sinon.stub(resolve, 'typescript').returns('/path/to/typescript.js') + mockery.registerMock('@cypress/webpack-batteries-included-preprocessor', webpackPreprocessor) - pluginsDeferred.resolve(config) + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - return Promise - .delay(10) - .then(() => { - const registrations = this.ipc.send.lastCall.args[2] + const registrations = ipc.send.withArgs('setupTestingType:reply').args[0][1].registrations expect(webpackPreprocessor).to.be.calledWith({ typescript: '/path/to/typescript.js', }) - expect(registrations[4]).to.eql({ + expect(_.last(registrations)).to.eql({ event: 'file:preprocessor', - eventId: 4, + eventId: 5, }) - this.ipc.on.withArgs('execute:plugins').yield('file:preprocessor', { eventId: 4, invocationId: '00' }, ['arg1', 'arg2']) + ipc.on.withArgs('execute:plugins').yield('file:preprocessor', { eventId: 5, invocationId: '00' }, ['arg1', 'arg2']) expect(webpackPreprocessorFn, 'webpackPreprocessor').to.be.called }) - }) - - it('does not register default preprocessor if registered by user', function () { - const pluginsDeferred = deferred() - const config = { projectRoot: '/project/root' } - const userPreprocessorFn = sinon.spy() - const webpackPreprocessor = sinon.spy() - sinon.stub(resolve, 'typescript').returns('/path/to/typescript.js') - - const setupNodeEventsFn = (on) => { - on('after:screenshot', () => {}) - on('file:preprocessor', userPreprocessorFn) - on('task', {}) + it('does not register default preprocessor if registered by user', async () => { + const userPreprocessorFn = sinon.spy() + const webpackPreprocessor = sinon.spy() - return config - } + sinon.stub(resolve, 'typescript').returns('/path/to/typescript.js') - mockery.registerMock('@cypress/webpack-batteries-included-preprocessor', webpackPreprocessor) - runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) + const setupNodeEventsFn = (on) => { + on('after:screenshot', () => {}) + on('file:preprocessor', userPreprocessorFn) + on('task', {}) - this.ipc.on.withArgs('load:plugins').yield(config) + return config + } - pluginsDeferred.resolve(config) + mockery.registerMock('@cypress/webpack-batteries-included-preprocessor', webpackPreprocessor) + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - return Promise - .delay(10) - .then(() => { - const registrations = this.ipc.send.lastCall.args[2] + const registrations = ipc.send.withArgs('setupTestingType:reply').args[0][1].registrations expect(webpackPreprocessor).not.to.be.called - expect(registrations[3]).to.eql({ + expect(registrations[4]).to.eql({ event: 'file:preprocessor', - eventId: 3, + eventId: 4, }) - this.ipc.on.withArgs('execute:plugins').yield('file:preprocessor', { eventId: 3, invocationId: '00' }, ['arg1', 'arg2']) + ipc.on.withArgs('execute:plugins').yield('file:preprocessor', { eventId: 4, invocationId: '00' }, ['arg1', 'arg2']) expect(userPreprocessorFn).to.be.called }) - }) - it('sends error if setupNodeEvents function rejects the promise', function (done) { - const err = new Error('foo') - const setupNodeEventsFn = sinon.stub().rejects(err) + it(`sends 'setupTestingType:reply' event with modified config, registrations, and requires`, async () => { + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - this.ipc.on.withArgs('load:plugins').yields({}) - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + expect(ipc.send).to.be.calledWith('setupTestingType:reply') - this.ipc.send = _.once((event, errorType, pluginsFile, result) => { - expect(event).to.eq('setupTestingType:error') - expect(errorType).to.eq('CHILD_PROCESS_UNEXPECTED_ERROR') - expect(pluginsFile).to.eq('plugins-file') - expect(result.stack).to.eq(err.stack) - - return done() - }) - }) + const { setupConfig, registrations, requires } = ipc.send.withArgs('setupTestingType:reply').args[0][1] - it('calls function exported by pluginsFile with register function and config', function () { - const setupNodeEventsFn = sinon.spy() + expect(setupConfig).to.eql({ includeShadowDom: true }) - runPlugins.runSetupNodeEvents(setupNodeEventsFn) - - const config = {} - - this.ipc.on.withArgs('load:plugins').yield(config) - expect(setupNodeEventsFn).to.be.called - expect(setupNodeEventsFn.lastCall.args[0]).to.be.a('function') - - expect(setupNodeEventsFn.lastCall.args[1]).to.equal(config) - }) + expect(registrations).to.have.length(6) + expect(_.map(registrations, 'event')).to.eql([ + '_get:task:body', + '_get:task:keys', + '_process:cross:origin:callback', + 'after:screenshot', + 'task', + 'file:preprocessor', + ]) - it('sends error if pluginsFile function throws an error', function (done) { - const err = new Error('foo') + expect(requires).to.be.an('array') + }) - const setupNodeEventsFn = () => { - throw err - } + it('sends error if setupNodeEvents function rejects the promise', async () => { + const err = new Error('foo') + const setupNodeEventsFn = sinon.stub().rejects(err) - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - this.ipc.on.withArgs('load:plugins').yield({}) + expect(ipc.send).to.be.calledWith('setupTestingType:error') - this.ipc.send = _.once((event, errorType, pluginsFile, serializedErr) => { - expect(event).to.eq('setupTestingType:error') - expect(errorType).to.eq('CHILD_PROCESS_UNEXPECTED_ERROR') - expect(pluginsFile).to.eq('plugins-file') - expect(serializedErr.stack).to.eq(err.stack) + const error = ipc.send.withArgs('setupTestingType:error').args[0][1] - return done() + expect(error.originalError.message).to.equal('foo') }) }) - it('defines global __cypressCallbackReplacementCommands', function () { - const setupNodeEventsFn = sinon.spy() + describe(`on 'execute:plugins' message`, () => { + let onFilePreprocessor + let beforeBrowserLaunch + let taskRequested + let setupNodeEventsFn - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + beforeEach(async () => { + sinon.stub(preprocessor, 'wrap') - this.ipc.on.withArgs('load:plugins').yield() + onFilePreprocessor = sinon.stub().resolves() + beforeBrowserLaunch = sinon.stub().resolves() + taskRequested = sinon.stub().resolves('foo') - expect(global.__cypressCallbackReplacementCommands).to.deep.equal(['origin']) - }) - - it('defines global __cypressCallbackReplacementCommands if experimentalOriginDependencies: true', function () { - const setupNodeEventsFn = sinon.spy() - - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + setupNodeEventsFn = (on) => { + on('file:preprocessor', onFilePreprocessor) + on('before:browser:launch', beforeBrowserLaunch) + on('task', taskRequested) + } + }) - const config = { - experimentalOriginDependencies: true, - } + context('file:preprocessor', () => { + const ids = { eventId: 0, invocationId: '00' } + const args = ['arg1', 'arg2'] - this.ipc.on.withArgs('load:plugins').yield(config) + beforeEach(async () => { + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - expect(global.__cypressCallbackReplacementCommands).to.deep.equal(['origin']) - }) + ipc.on.withArgs('execute:plugins').yield('file:preprocessor', ids, args) + }) - it('does not define global __cypressCallbackReplacementCommands if experimentalOriginDependencies: false', function () { - const setupNodeEventsFn = sinon.spy() + it('calls preprocessor handler', () => { + expect(preprocessor.wrap).to.be.called + expect(preprocessor.wrap.lastCall.args[0]).to.equal(ipc) + expect(preprocessor.wrap.lastCall.args[1]).to.be.a('function') + expect(preprocessor.wrap.lastCall.args[2]).to.equal(ids) + expect(preprocessor.wrap.lastCall.args[3]).to.equal(args) + }) - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + it('invokes registered function when invoked by handler', () => { + preprocessor.wrap.lastCall.args[1](3, ['one', 'two']) - const config = { - experimentalOriginDependencies: false, - } + expect(onFilePreprocessor).to.be.calledWith('one', 'two') + }) + }) - this.ipc.on.withArgs('load:plugins').yield(config) + context('before:browser:launch', () => { + let args + const ids = { eventId: 1, invocationId: '00' } - expect(global.__cypressCallbackReplacementCommands).to.be.undefined - }) - }) + beforeEach(async () => { + sinon.stub(util, 'wrapChildPromise') - describe('on \'execute\' message', () => { - beforeEach(function () { - sinon.stub(preprocessor, 'wrap') + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) - this.onFilePreprocessor = sinon.stub().resolves() - this.beforeBrowserLaunch = sinon.stub().resolves() - this.taskRequested = sinon.stub().resolves('foo') + const browser = {} + const launchOptions = browserUtils.getDefaultLaunchOptions({}) - const setupNodeEventsFn = (register) => { - register('file:preprocessor', this.onFilePreprocessor) - register('before:browser:launch', this.beforeBrowserLaunch) + args = [browser, launchOptions] - return register('task', this.taskRequested) - } + ipc.on.withArgs('execute:plugins').yield('before:browser:launch', ids, args) + }) - runPlugins.runSetupNodeEvents(setupNodeEventsFn) + it('wraps child promise', () => { + expect(util.wrapChildPromise).to.be.called + expect(util.wrapChildPromise.lastCall.args[0]).to.equal(ipc) + expect(util.wrapChildPromise.lastCall.args[1]).to.be.a('function') + expect(util.wrapChildPromise.lastCall.args[2]).to.equal(ids) + expect(util.wrapChildPromise.lastCall.args[3]).to.equal(args) + }) - return this.ipc.on.withArgs('load:plugins').yield({}) - }) + it('invokes registered function when invoked by handler', () => { + util.wrapChildPromise.lastCall.args[1](4, args) - context('file:preprocessor', () => { - beforeEach(function () { - this.ids = { eventId: 0, invocationId: '00' } + expect(beforeBrowserLaunch).to.be.calledWith(...args) + }) }) - it('calls preprocessor handler', function () { - const args = ['arg1', 'arg2'] + context('_process:cross:origin:callback', () => { + it('calls processCallback with args', async () => { + const ids = { eventId: '2' } - this.ipc.on.withArgs('execute:plugins').yield('file:preprocessor', this.ids, args) - expect(preprocessor.wrap).to.be.called - expect(preprocessor.wrap.lastCall.args[0]).to.equal(this.ipc) - expect(preprocessor.wrap.lastCall.args[1]).to.be.a('function') - expect(preprocessor.wrap.lastCall.args[2]).to.equal(this.ids) + sinon.stub(crossOrigin, 'processCallback') - expect(preprocessor.wrap.lastCall.args[3]).to.equal(args) - }) - - it('invokes registered function when invoked by handler', function () { - this.ipc.on.withArgs('execute:plugins').yield('file:preprocessor', this.ids, []) - preprocessor.wrap.lastCall.args[1](2, ['one', 'two']) + await runPlugins.runSetupNodeEvents({}, setupNodeEventsFn) + await runPlugins.execute('_process:cross:origin:callback', ids, ['arg1', 'arg2']) - expect(this.onFilePreprocessor).to.be.calledWith('one', 'two') + expect(crossOrigin.processCallback).to.be.calledWith('arg1', 'arg2') + }) }) }) + }) - context('before:browser:launch', () => { - beforeEach(function () { - sinon.stub(util, 'wrapChildPromise') - - const browser = {} - const launchOptions = browserUtils.getDefaultLaunchOptions({}) - - this.args = [browser, launchOptions] - this.ids = { eventId: 1, invocationId: '00' } - }) - - it('wraps child promise', function () { - this.ipc.on.withArgs('execute:plugins').yield('before:browser:launch', this.ids, this.args) - expect(util.wrapChildPromise).to.be.called - expect(util.wrapChildPromise.lastCall.args[0]).to.equal(this.ipc) - expect(util.wrapChildPromise.lastCall.args[1]).to.be.a('function') - expect(util.wrapChildPromise.lastCall.args[2]).to.equal(this.ids) - - expect(util.wrapChildPromise.lastCall.args[3]).to.equal(this.args) - }) + context('#invoke', () => { + it('calls the handler for the specified eventId with the specified args', () => { + const handler = sinon.spy() - it('invokes registered function when invoked by handler', function () { - this.ipc.on.withArgs('execute:plugins').yield('before:browser:launch', this.ids, this.args) - util.wrapChildPromise.lastCall.args[1](3, this.args) + runPlugins.registeredEventsById['id-1'] = { handler } + runPlugins.invoke('id-1', [1, 2, 3]) - expect(this.beforeBrowserLaunch).to.be.calledWith(...this.args) - }) + expect(handler).to.be.calledWith(1, 2, 3) }) + }) - context('task', () => { - beforeEach(function () { - sinon.stub(runPlugins, 'execute') - this.ids = { eventId: 5, invocationId: '00' } - }) - - it('calls task handler', function () { - const args = ['arg1'] + describe('tasks', () => { + const events = { + 'the:task': sinon.stub().returns('result 1'), + 'another:task': sinon.stub().returns('result 2'), + 'a:third:task' () { + return 'foo' + }, + } + const ids = {} - this.ipc.on.withArgs('execute:plugins').yield('task', this.ids, args) - expect(runPlugins.execute).to.be.called - expect(runPlugins.execute.lastCall.args[0]).to.equal(this.ipc) - expect(runPlugins.execute.lastCall.args[1]).to.be.an('object') - expect(runPlugins.execute.lastCall.args[2]).to.equal(this.ids) + beforeEach(async () => { + sinon.stub(util, 'wrapChildPromise') - expect(runPlugins.execute.lastCall.args[3]).to.equal(args) + const setupNodeEventsFn = sinon.stub().callsFake((on) => { + on('task', events) }) - }) - }) - describe('tasks', () => { - beforeEach(function () { - this.ipc = { - send: sinon.spy(), - on: sinon.stub(), - removeListener: sinon.spy(), - } - - this.events = { - '1': { - event: 'task', - handler: { - 'the:task': sinon.stub().returns('result'), - 'another:task': sinon.stub().returns('result'), - 'a:third:task' () { - return 'foo' - }, - }, - }, - } - - this.ids = {} - - return sinon.stub(util, 'wrapChildPromise') + await runPlugins.runSetupNodeEvents({}, setupNodeEventsFn) }) context('.taskGetBody', () => { - it('returns the stringified body of the event handler', function () { - runPlugins.taskGetBody(this.ids, ['a:third:task']) + it('returns the stringified body of the event handler', () => { + runPlugins.taskGetBody(ids, ['a:third:task']) expect(util.wrapChildPromise).to.be.called const result = util.wrapChildPromise.lastCall.args[1]('1') expect(result.replace(/\s+/g, '')).to.equal('\'a:third:task\'(){return\'foo\'}') }) - it('returns an empty string if event handler cannot be found', function () { - runPlugins.taskGetBody(this.ids, ['non:existent']) + it('returns an empty string if event handler cannot be found', () => { + runPlugins.taskGetBody(ids, ['non:existent']) expect(util.wrapChildPromise).to.be.called const result = util.wrapChildPromise.lastCall.args[1]('1') @@ -436,8 +281,8 @@ describe.skip('lib/plugins/child/run_plugins', () => { }) context('.taskGetKeys', () => { - it('returns the registered task keys', function () { - runPlugins.taskGetKeys(this.ipc, this.events, this.ids) + it('returns the registered task keys', () => { + runPlugins.taskGetKeys(ipc, this.events, ids) expect(util.wrapChildPromise).to.be.called const result = util.wrapChildPromise.lastCall.args[1]('1') @@ -446,25 +291,24 @@ describe.skip('lib/plugins/child/run_plugins', () => { }) context('.taskExecute', () => { - it('passes through ipc and ids', function () { - runPlugins.taskExecute(this.ids, ['the:task']) + it('passes through ipc and ids', () => { + runPlugins.taskExecute(ids, ['the:task']) expect(util.wrapChildPromise).to.be.called - expect(util.wrapChildPromise.lastCall.args[0]).to.be.equal(this.ipc) - - expect(util.wrapChildPromise.lastCall.args[2]).to.be.equal(this.ids) + expect(util.wrapChildPromise.lastCall.args[0]).to.be.equal(ipc) + expect(util.wrapChildPromise.lastCall.args[2]).to.be.equal(ids) }) - it('invokes the callback for the given task if it exists and returns the result', function () { - runPlugins.taskExecute(this.ids, ['the:task', 'the:arg']) - const result = util.wrapChildPromise.lastCall.args[1]('1', ['the:arg']) + it('invokes the callback for the given task if it exists and returns the result', () => { + runPlugins.taskExecute(ids, ['the:task', 'the:arg']) - expect(this.events['1'].handler['the:task']).to.be.calledWith('the:arg') + const result = util.wrapChildPromise.lastCall.args[1]('3', ['the:arg']) - expect(result).to.equal('result') + expect(events['the:task']).to.be.calledWith('the:arg') + expect(result).to.equal('result 1') }) - it(`returns __cypress_unhandled__ if the task doesn't exist`, function () { - runPlugins.taskExecute(this.ids, ['nope']) + it('returns __cypress_unhandled__ if the task does not exist', () => { + runPlugins.taskExecute(ids, ['nope']) expect(util.wrapChildPromise.lastCall.args[1]('1')).to.equal('__cypress_unhandled__') }) diff --git a/system-tests/projects/origin-dependencies/.gitignore b/system-tests/projects/origin-dependencies/.gitignore deleted file mode 100644 index cf4bab9ddde9..000000000000 --- a/system-tests/projects/origin-dependencies/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!node_modules diff --git a/system-tests/projects/origin-dependencies/cypress.config.js b/system-tests/projects/origin-dependencies/cypress.config.js deleted file mode 100644 index 091d7c0451d5..000000000000 --- a/system-tests/projects/origin-dependencies/cypress.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - e2e: { - setupNodeEvents (on) {}, - }, -} diff --git a/system-tests/projects/origin-dependencies/cypress/support/e2e.js b/system-tests/projects/origin-dependencies/cypress/support/e2e.js deleted file mode 100644 index 3125df81f8a8..000000000000 --- a/system-tests/projects/origin-dependencies/cypress/support/e2e.js +++ /dev/null @@ -1 +0,0 @@ -import 'custom-origin-command' diff --git a/system-tests/projects/origin-dependencies/node_modules/custom-origin-command/index.js b/system-tests/projects/origin-dependencies/node_modules/custom-origin-command/index.js deleted file mode 100644 index c8ea51e8c29c..000000000000 --- a/system-tests/projects/origin-dependencies/node_modules/custom-origin-command/index.js +++ /dev/null @@ -1,5 +0,0 @@ -Cypress.Commands.add('customOriginCommand', () => { - cy.origin('foobar.com', () => { - require('lodash') - }) -}) diff --git a/system-tests/projects/todos/cypress.config.js b/system-tests/projects/todos/cypress.config.js index c5262563665d..67b37b3f1625 100644 --- a/system-tests/projects/todos/cypress.config.js +++ b/system-tests/projects/todos/cypress.config.js @@ -14,7 +14,7 @@ module.exports = { }, 'e2e': { 'supportFile': 'tests/_support/spec_helper.js', - 'specPattern': 'tests/**/*', + 'specPattern': 'tests/**/*.(js|ts|coffee)', 'setupNodeEvents': (on, config) => config, }, }