Skip to content

Commit

Permalink
feat: Enable requiring cy.origin dependencies with require() and impo…
Browse files Browse the repository at this point in the history
…rt() (#24294)
  • Loading branch information
chrisbreiding authored Oct 18, 2022
1 parent b20ec54 commit 1b29ce7
Show file tree
Hide file tree
Showing 15 changed files with 97 additions and 110 deletions.
6 changes: 0 additions & 6 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,12 +641,6 @@ declare namespace Cypress {
*/
off: Actions

/**
* Used to import dependencies within the cy.origin() callback
* @see https://on.cypress.io/origin
*/
require: (id: string) => any

/**
* Trigger action
* @private
Expand Down
4 changes: 2 additions & 2 deletions npm/webpack-preprocessor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
bundles[filePath].deferreds.length = 0
}

// the cross-origin-callback-loader extracts any cy.origin() callback
// functions that contains Cypress.require() and stores their sources
// the cross-origin-callback-loader extracts any cross-origin callback
// functions that require dependencies and stores their sources
// in the CrossOriginCallbackStore. it saves the callbacks per source
// files, since that's the context it has. here we need to unfurl
// what dependencies the input source file has so we can know which
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ interface CompileOptions {
}

// the cross-origin-callback-loader extracts any cy.origin() callback functions
// that contains Cypress.require() and stores their sources in the
// that includes dependencies and stores their sources in the
// CrossOriginCallbackStore. this sends those sources through webpack again
// to process any dependencies and create bundles for each callback function
export const compileCrossOriginCallbackFiles = (files: CrossOriginCallbackStoreFile[], options: CompileOptions): Promise<void> => {
Expand Down
52 changes: 20 additions & 32 deletions npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,13 @@ import utils from './utils'

const debug = Debug('cypress:webpack')

// this loader makes supporting dependencies within the cy.origin() callbacks
// possible. it does this by doing the following:
// - extracting callback(s)
// - the callback(s) is/are kept in memory and then run back through webpack
// this loader makes supporting dependencies within cross-origin callbacks
// possible. if there are no dependencies (e.g. no requires/imports), it's a
// noop. otherwise: it does this by doing the following:
// - extracts the callbacks
// - the callbacks are kept in memory and then run back through webpack
// once the initial file compilation is complete
// - users use Cypress.require() in their test code instead of require().
// this is because we don't want require()s nested within the callback
// to be processed in the initial compilation. this both improves
// performance and prevents errors (when the dependency has ES import
// statements, babel will error because they're not top-level since
// the require is not top-level)
// - replacing Cypress.require() with require()
// - this allows the require()s to be processed normally during the
// compilation of the callback itself.
// - replacing the callback(s) with object(s)
// - replaces the callbacks with objects
// - this object references the file the callback will be output to by
// its own compilation. this allows the runtime to get the file and
// run it in its origin's context.
Expand Down Expand Up @@ -77,9 +69,9 @@ export default function (source: string, map, meta, store = crossOriginCallbackS

const lastArg = _.last(path.get('arguments'))

// the user could try an invalid signature for cy.origin() where the
// last argument is not a function. in this case, we'll return the
// unmodified code and it will be a runtime validation error
// the user could try an invalid signature where the last argument is
// not a function. in this case, we'll return the unmodified code and
// it will be a runtime validation error
if (
!lastArg || (
!lastArg.isArrowFunctionExpression()
Expand All @@ -89,21 +81,17 @@ export default function (source: string, map, meta, store = crossOriginCallbackS
return
}

// replace instances of Cypress.require('dep') with require('dep')
// determine if there are any requires/imports within the callback
lastArg.traverse({
CallExpression (path) {
const callee = path.get('callee') as NodePath<t.MemberExpression>

// e.g. const dep = Cypress.require('../path/to/dep')
if (callee.matchesPattern('Cypress.require')) {
if (
// e.g. const dep = require('../path/to/dep')
// @ts-ignore
path.node.callee.name === 'require'
// e.g. const dep = await import('../path/to/dep')
|| path.node.callee.type as string === 'Import'
) {
hasDependencies = true

path.replaceWith(
t.callExpression(
callee.node.property as t.Expression, // 'require'
path.get('arguments').map((arg) => arg.node), // ['../path/to/dep']
),
)
}
},
}, this)
Expand Down Expand Up @@ -150,7 +138,7 @@ export default function (source: string, map, meta, store = crossOriginCallbackS
// replaces callback function with object referencing the extracted
// function's callback name and output file path in the form
// { callbackName: <callbackName>, outputFilePath: <outputFilePath> }
// this is used at runtime when cy.origin() is run to execute the bundle
// this is used at runtime when the command is run to execute the bundle
// generated for the extracted callback function
lastArg.replaceWith(
t.objectExpression([
Expand All @@ -167,7 +155,7 @@ export default function (source: string, map, meta, store = crossOriginCallbackS
},
})

// if we found Cypress.require()s, re-generate the code from the AST
// if we found requires/imports, re-generate the code from the AST
if (hasDependencies) {
debug('callback with modified source')

Expand All @@ -183,6 +171,6 @@ export default function (source: string, map, meta, store = crossOriginCallbackS
}

debug('callback with original source')
// if no Cypress.require()s were found, callback with the original source/map
// if no requires/imports were found, callback with the original source/map
this.callback(null, source, map)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('./lib/cross-origin-callback-loader', () => {
expect(store.addFile).not.to.be.called
})

it('is a noop when cy.origin() callback does not contain Cypress.require()', () => {
it('is a noop when cy.origin() callback does not contain require() or import()', () => {
const source = `it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {})
})`
Expand Down Expand Up @@ -90,11 +90,30 @@ describe('./lib/cross-origin-callback-loader', () => {
sinon.stub(utils, 'tmpdir').returns('/path/to/tmp')
})

it('replaces cy.origin() callback with an object', () => {
it('replaces cy.origin() callback with an object when using require()', () => {
const { resultingSource, resultingMap } = callLoader(stripIndent`
it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {
Cypress.require('../support/utils')
require('../support/utils')
})
})`)

expect(resultingSource).to.equal(stripIndent`
it('test', () => {
cy.origin('http://www.foobar.com:3500', {
"callbackName": "__cypressCrossOriginCallback",
"outputFilePath": "/path/to/tmp/cross-origin-cb-abc123.js"
});
});`)

expect(resultingMap).to.be.undefined
})

it('replaces cy.origin() callback with an object when using import()', () => {
const { resultingSource, resultingMap } = callLoader(stripIndent`
it('test', () => {
cy.origin('http://www.foobar.com:3500', async () => {
await import('../support/utils')
})
})`)

Expand All @@ -113,7 +132,7 @@ describe('./lib/cross-origin-callback-loader', () => {
const { resultingSource, resultingMap } = callLoader(stripIndent`
it('test', () => {
cy.other('http://www.foobar.com:3500', () => {
Cypress.require('../support/utils')
require('../support/utils')
})
})`,
['other'])
Expand All @@ -129,11 +148,11 @@ describe('./lib/cross-origin-callback-loader', () => {
expect(resultingMap).to.be.undefined
})

it('adds the file to the store, replacing Cypress.require() with require()', () => {
it('adds the file to the store, replacing require() with require()', () => {
const { store } = callLoader(
`it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {
Cypress.require('../support/utils')
require('../support/utils')
})
})`,
)
Expand All @@ -149,7 +168,7 @@ describe('./lib/cross-origin-callback-loader', () => {
const { store } = callLoader(
`it('test', () => {
cy.origin('http://www.foobar.com:3500', function () {
Cypress.require('../support/utils')
require('../support/utils')
})
})`,
)
Expand All @@ -164,7 +183,7 @@ describe('./lib/cross-origin-callback-loader', () => {
const { store } = callLoader(
`it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {
Cypress.require('../support/utils')
require('../support/utils')
})
})`,
)
Expand All @@ -179,7 +198,7 @@ describe('./lib/cross-origin-callback-loader', () => {
const { store } = callLoader(
`it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {
const utils = Cypress.require('../support/utils')
const utils = require('../support/utils')
utils.foo()
})
})`,
Expand All @@ -193,13 +212,13 @@ describe('./lib/cross-origin-callback-loader', () => {
}`)
})

it('works with multiple Cypress.require()s', () => {
it('works with multiple require()s', () => {
const { store } = callLoader(
`it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {
Cypress.require('../support/commands')
const utils = Cypress.require('../support/utils')
const _ = Cypress.require('lodash')
require('../support/commands')
const utils = require('../support/utils')
const _ = require('lodash')
})
})`,
)
Expand All @@ -220,7 +239,7 @@ describe('./lib/cross-origin-callback-loader', () => {
cy
.wrap({})
.origin('http://www.foobar.com:3500', () => {
Cypress.require('../support/commands')
require('../support/commands')
})
})`,
)
Expand All @@ -236,7 +255,7 @@ describe('./lib/cross-origin-callback-loader', () => {
`it('test', () => {
cy.origin('http://www.foobar.com:3500', () => {
const someVar = 'someValue'
const result = Cypress.require('./fn')(someVar)
const result = require('./fn')(someVar)
expect(result).to.equal('mutated someVar')
})
})`,
Expand All @@ -256,7 +275,7 @@ describe('./lib/cross-origin-callback-loader', () => {
const { store } = callLoader(
`it('test', () => {
cy.origin('http://www.foobar.com:3500', { args: { foo: 'foo'}}, ({ foo }) => {
const result = Cypress.require('./fn')(foo)
const result = require('./fn')(foo)
expect(result).to.equal('mutated someVar')
})
})`,
Expand Down
2 changes: 1 addition & 1 deletion packages/driver/cross-origin-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ In order to counteract this, we utilize the [proxy](../proxy) to capture cookies

## Dependencies

Users can utilize `Cypress.require()` to include dependencies. It's functionally the same as the CommonJs `require()`. We handle the dependency resolution and bundling with the webpack preprocessor. We add a webpack loader that runs last. If we find a `Cypress.require()` call inside a `cy.origin()` callback, we extract that callback from the output code and replace references to `Cypress.require()` with `require()` calls. We then run that extracted callback through webpack again, so that it gets its own output bundle with all dependencies included. The original callback is replaced with an object that references the output bundle. At runtime, when executing `cy.origin()`, it loads and executes the callback bundle.
Users can utilize `require()` or (dynamic) `import()` to include dependencies. We handle the dependency resolution and bundling with the webpack preprocessor. We add a webpack loader that runs last. If we find a `require()` or `import()` call inside a `cy.origin()` callback, we extract that callback from the output code. We then run that extracted callback through webpack again, so that it gets its own output bundle with all dependencies included. The original callback is replaced with an object that references the output bundle. At runtime, when executing `cy.origin()`, it loads and executes the callback bundle.

## Unsupported APIs

Expand Down
4 changes: 3 additions & 1 deletion packages/driver/cypress/e2e/commands/assertions.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ describe('src/cy/commands/assertions', () => {
})

it('has a pending state while retrying for commands with onFail', (done) => {
cy.on('command:retry', (command) => {
cy.on('command:retry', () => {
const [readFileLog, shouldLog] = cy.state('current').get('logs')

expect(readFileLog.get('state')).to.eq('pending')
Expand All @@ -658,6 +658,8 @@ describe('src/cy/commands/assertions', () => {
done()
})

cy.on('fail', () => {})

cy.readFile('does-not-exist.json').should('exist')
})

Expand Down
2 changes: 1 addition & 1 deletion packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('cy.origin dependencies - jsx', { browser: '!webkit' }, () => {

it('works with a jsx file', () => {
cy.origin('http://www.foobar.com:3500', () => {
const lodash = Cypress.require('lodash')
const lodash = require('lodash')

expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
})
Expand Down
Loading

5 comments on commit 1b29ce7

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 1b29ce7 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.11.0/linux-x64/develop-1b29ce74aafa0bc5015a93cb618b7fbda243e07a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 1b29ce7 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.11.0/linux-arm64/develop-1b29ce74aafa0bc5015a93cb618b7fbda243e07a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 1b29ce7 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.11.0/darwin-x64/develop-1b29ce74aafa0bc5015a93cb618b7fbda243e07a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 1b29ce7 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.11.0/darwin-arm64/develop-1b29ce74aafa0bc5015a93cb618b7fbda243e07a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 1b29ce7 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.11.0/win32-x64/develop-1b29ce74aafa0bc5015a93cb618b7fbda243e07a/cypress.tgz

Please sign in to comment.