From 6b827dd95dc3e0a27d4e495bedb0f2cb2844a6ce Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Wed, 26 May 2021 15:11:35 -0400 Subject: [PATCH 001/108] chore: Initial work on creating a multidomain bundle (#16230) * add multidomain bundle * add a spec for manually testing and experimenting with multidomain bundle * get cy.now('get') functioning * move majority of multidomain entry point into driver --- .../cypress/fixtures/multidomain-aut.html | 14 ++++++ .../cypress/fixtures/multidomain-sibling.html | 8 ++++ .../driver/cypress/fixtures/multidomain.html | 9 ++++ .../integration/e2e/multidomain_spec.ts | 5 ++ packages/driver/cypress/plugins/server.js | 8 ++++ packages/driver/src/cypress/cy.js | 8 ++++ packages/driver/src/multidomain/index.js | 48 +++++++++++++++++++ packages/runner/multidomain/.eslintrc.json | 5 ++ packages/runner/multidomain/index.js | 33 +++++++++++++ packages/runner/webpack.config.ts | 15 +++++- packages/web-config/webpack.config.base.ts | 45 +++++++++++++---- 11 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 packages/driver/cypress/fixtures/multidomain-aut.html create mode 100644 packages/driver/cypress/fixtures/multidomain-sibling.html create mode 100644 packages/driver/cypress/fixtures/multidomain.html create mode 100644 packages/driver/cypress/integration/e2e/multidomain_spec.ts create mode 100644 packages/driver/src/multidomain/index.js create mode 100644 packages/runner/multidomain/.eslintrc.json create mode 100644 packages/runner/multidomain/index.js diff --git a/packages/driver/cypress/fixtures/multidomain-aut.html b/packages/driver/cypress/fixtures/multidomain-aut.html new file mode 100644 index 000000000000..4a0c7d8838ce --- /dev/null +++ b/packages/driver/cypress/fixtures/multidomain-aut.html @@ -0,0 +1,14 @@ + + + + + +

Multidomain AUT

+

Some text in the cross-domain AUT

+ + + diff --git a/packages/driver/cypress/fixtures/multidomain-sibling.html b/packages/driver/cypress/fixtures/multidomain-sibling.html new file mode 100644 index 000000000000..d10ec54b3eb0 --- /dev/null +++ b/packages/driver/cypress/fixtures/multidomain-sibling.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/driver/cypress/fixtures/multidomain.html b/packages/driver/cypress/fixtures/multidomain.html new file mode 100644 index 000000000000..f5a1107a98b0 --- /dev/null +++ b/packages/driver/cypress/fixtures/multidomain.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/driver/cypress/integration/e2e/multidomain_spec.ts b/packages/driver/cypress/integration/e2e/multidomain_spec.ts new file mode 100644 index 000000000000..81c4d8b4aaba --- /dev/null +++ b/packages/driver/cypress/integration/e2e/multidomain_spec.ts @@ -0,0 +1,5 @@ +// NOTE: this test only exists for manual verification as the +// multidomain bundle is a very incomplete work-in-progress +it('loads multidomain playground', () => { + cy.visit('/fixtures/multidomain.html') +}) diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js index 03f810960682..565971fe09db 100644 --- a/packages/driver/cypress/plugins/server.js +++ b/packages/driver/cypress/plugins/server.js @@ -8,6 +8,7 @@ const path = require('path') const Promise = require('bluebird') const PATH_TO_SERVER_PKG = path.dirname(require.resolve('@packages/server')) +const PATH_TO_RUNNER_PKG = path.dirname(require.resolve('@packages/runner')) const httpPorts = [3500, 3501] const httpsPort = 3502 @@ -156,6 +157,13 @@ const createApp = (port) => { .send('server error') }) + app.get('/cypress_multidomain_runner.js', (req, res) => { + res.type('application/javascript') + res.sendFile(path.join('dist', 'cypress_multidomain_runner.js'), { + root: path.join(PATH_TO_RUNNER_PKG, '..'), + }) + }) + let _var = '' app.get('/set-var', (req, res) => { diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 939fac2b1597..cd9924054565 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -75,6 +75,14 @@ const setTopOnError = function (Cypress, cy) { curCy = cy + try { + // this will throw if AUT is cross-domain and we don't need to worry + // about top's error handler in that case anyway + top.__alreadySetErrorHandlers__ + } catch (err) { + return + } + // prevent overriding top.onerror twice when loading more than one // instance of test runner. if (top.__alreadySetErrorHandlers__) { diff --git a/packages/driver/src/multidomain/index.js b/packages/driver/src/multidomain/index.js new file mode 100644 index 000000000000..5737213f923c --- /dev/null +++ b/packages/driver/src/multidomain/index.js @@ -0,0 +1,48 @@ +import $Cypress from '../cypress' +import $Cy from '../cypress/cy' +import $Commands from '../cypress/commands' +import $Log from '../cypress/log' + +export const initialize = (autWindow) => { + const specWindow = { + Error, + } + const Cypress = $Cypress.create({ + browser: { + channel: 'stable', + displayName: 'Chrome', + family: 'chromium', + isChosen: true, + isHeaded: true, + isHeadless: false, + majorVersion: 90, + name: 'chrome', + path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + version: '90.0.4430.212', + }, + }) + const log = (...args) => { + return Cypress.log.apply(Cypress, args) + } + const cy = $Cy.create(specWindow, Cypress, Cypress.Cookies, Cypress.state, Cypress.config, log) + + Cypress.log = $Log.create(Cypress, cy, Cypress.state, Cypress.config) + Cypress.runner = { + addLog () {}, + } + + Cypress.state('window', autWindow) + Cypress.state('document', autWindow.document) + Cypress.state('runnable', { + ctx: {}, + clearTimeout () {}, + resetTimeout () {}, + timeout () {}, + }) + + $Commands.create(Cypress, cy, Cypress.state) + + return { + cy, + } +} diff --git a/packages/runner/multidomain/.eslintrc.json b/packages/runner/multidomain/.eslintrc.json new file mode 100644 index 000000000000..0d7b491c8f62 --- /dev/null +++ b/packages/runner/multidomain/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "globals": { + "window": true + } +} diff --git a/packages/runner/multidomain/index.js b/packages/runner/multidomain/index.js new file mode 100644 index 000000000000..e72284fc337e --- /dev/null +++ b/packages/runner/multidomain/index.js @@ -0,0 +1,33 @@ +import { initialize } from '@packages/driver/src/multidomain' + +const autWindow = window.parent.frames[0] + +const { cy } = initialize(autWindow) + +autWindow.onReady = () => { + cy.now('get', 'p').then(($el) => { + // eslint-disable-next-line no-console + console.log('got the paragaph with text:', $el.text()) + }) +} + +/* + +Need: +- Cypress +- cy, with + - built-in commands + - user-defined commands + +Commands need: +- state +- config +- events + +Don't need: +- UI components +- spec runner +- mocha +- wasm / source map utils + +*/ diff --git a/packages/runner/webpack.config.ts b/packages/runner/webpack.config.ts index 6803ea617aca..89d061e89a3b 100644 --- a/packages/runner/webpack.config.ts +++ b/packages/runner/webpack.config.ts @@ -91,4 +91,17 @@ const injectionConfig: webpack.Configuration = { }, } -export default [mainConfig, injectionConfig] +// @ts-ignore +const multiDomainConfig: webpack.Configuration = { + mode: 'development', + ...getSimpleConfig(), + entry: { + cypress_multidomain_runner: [path.resolve(__dirname, 'multidomain/index.js')], + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, +} + +export default [mainConfig, injectionConfig, multiDomainConfig] diff --git a/packages/web-config/webpack.config.base.ts b/packages/web-config/webpack.config.base.ts index 51486df217f6..746e8a9fa85a 100644 --- a/packages/web-config/webpack.config.base.ts +++ b/packages/web-config/webpack.config.base.ts @@ -97,6 +97,12 @@ function makeSassLoaders ({ modules }): RuleSetRule { } } +// the chrome version should be synced with +// npm/webpack-batteries-included-preprocessor/index.js and +// packages/server/lib/browsers/chrome.ts +const babelPresetEnvConfig = [require.resolve('@babel/preset-env'), { targets: { 'chrome': '64' } }] +const babelPresetTypeScriptConfig = [require.resolve('@babel/preset-typescript'), { allowNamespaces: true }] + export const getCommonConfig = () => { const commonConfig: Configuration = { mode: 'none', @@ -128,12 +134,9 @@ export const getCommonConfig = () => { [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], ], presets: [ - // the chrome version should be synced with - // npm/webpack-batteries-included-preprocessor/index.js and - // packages/server/lib/browsers/chrome.ts - [require.resolve('@babel/preset-env'), { targets: { 'chrome': '64' } }], + babelPresetEnvConfig, require.resolve('@babel/preset-react'), - [require.resolve('@babel/preset-typescript'), { allowNamespaces: true }], + babelPresetTypeScriptConfig, ], babelrc: false, }, @@ -216,8 +219,15 @@ export const getCommonConfig = () => { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces export const getSimpleConfig = () => ({ + node: { + fs: 'empty', + child_process: 'empty', + net: 'empty', + tls: 'empty', + module: 'empty', + }, resolve: { - extensions: ['.js'], + extensions: ['.js', '.ts', '.json'], }, stats, @@ -228,18 +238,37 @@ export const getSimpleConfig = () => ({ module: { rules: [ { - test: /\.(js)$/, + test: /\.(js|ts)$/, exclude: /node_modules/, use: { loader: require.resolve('babel-loader'), options: { + plugins: [ + [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], + ], presets: [ - [require.resolve('@babel/preset-env'), { targets: { 'chrome': 63 } }], + babelPresetEnvConfig, + babelPresetTypeScriptConfig, ], babelrc: false, }, }, }, + // FIXME: we don't actually want or need wasm support in the + // multidomain bundle that uses this config, but we need to refactor + // the driver so that it doesn't load the wasm code in + // packages/driver/src/cypress/source_map_utils.js when creating + // the multidomain bundle. for now, this is necessary so the build + // doesn't fail + { + test: /\.wasm$/, + type: 'javascript/auto', + use: [ + { + loader: require.resolve('arraybuffer-loader'), + }, + ], + }, ], }, From d028437d7236bd59e9d3212d04ef239c33d8ec52 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Mon, 7 Jun 2021 14:58:01 -0400 Subject: [PATCH 002/108] chore: Implement cross-domain sibling iframe (#16708) --- cli/types/cypress.d.ts | 2 ++ .../cypress/fixtures/multidomain-aut.html | 7 ++--- .../driver/cypress/fixtures/multidomain.html | 3 +-- .../integration/e2e/multidomain_spec.ts | 22 +++++++++++++--- packages/driver/src/cy/multidomain/index.ts | 14 ++++++++++ packages/driver/src/cypress.js | 12 +++++++++ packages/driver/src/cypress/commands.js | 1 + packages/driver/src/cypress/cy.js | 25 +++++++++++++----- packages/driver/src/multidomain/index.js | 14 +++++++++- packages/runner/multidomain/index.js | 11 +------- packages/runner/src/iframe/iframes.jsx | 26 ++++++++++++++++--- packages/runner/src/lib/event-manager.js | 20 +++++++++++++- packages/server/lib/controllers/files.js | 14 ++++++++++ .../server/lib/html/multidomain-iframe.html | 13 ++++++++++ packages/server/lib/routes.js | 7 +++++ 15 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 packages/driver/src/cy/multidomain/index.ts create mode 100644 packages/server/lib/html/multidomain-iframe.html diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 6d9b5625f126..77f8dde729c0 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -5498,6 +5498,8 @@ declare namespace Cypress { message: any /** Set to false if you want to control the finishing of the command in the log yourself */ autoEnd: boolean + /** Set to false if you want to control the finishing of the command in the log yourself */ + end: boolean /** Return an object that will be printed in the dev tools console */ consoleProps(): ObjectLike } diff --git a/packages/driver/cypress/fixtures/multidomain-aut.html b/packages/driver/cypress/fixtures/multidomain-aut.html index 4a0c7d8838ce..dafe310f6bdf 100644 --- a/packages/driver/cypress/fixtures/multidomain-aut.html +++ b/packages/driver/cypress/fixtures/multidomain-aut.html @@ -6,9 +6,10 @@

Multidomain AUT

Some text in the cross-domain AUT

diff --git a/packages/driver/cypress/fixtures/multidomain.html b/packages/driver/cypress/fixtures/multidomain.html index f5a1107a98b0..1bd4b1f87b72 100644 --- a/packages/driver/cypress/fixtures/multidomain.html +++ b/packages/driver/cypress/fixtures/multidomain.html @@ -3,7 +3,6 @@ - - + Go to localhost:3501 diff --git a/packages/driver/cypress/integration/e2e/multidomain_spec.ts b/packages/driver/cypress/integration/e2e/multidomain_spec.ts index 81c4d8b4aaba..5058a3ce08c5 100644 --- a/packages/driver/cypress/integration/e2e/multidomain_spec.ts +++ b/packages/driver/cypress/integration/e2e/multidomain_spec.ts @@ -1,5 +1,21 @@ -// NOTE: this test only exists for manual verification as the -// multidomain bundle is a very incomplete work-in-progress -it('loads multidomain playground', () => { +// FIXME: Skip this for now since it's flaky +it.skip('verifies initial implementation of sibling iframe and switchToDomain', (done) => { + top.addEventListener('message', (event) => { + if (event.data && event.data.text) { + expect(event.data.text).to.equal('Some text in the cross-domain AUT') + expect(event.data.host).to.equal('localhost:3501') + done() + } + }, false) + + cy.viewport(900, 300) cy.visit('/fixtures/multidomain.html') + cy.get('a').click() + // @ts-ignore + cy.switchToDomain('localhost:3501', () => { + // @ts-ignore + cy.now('get', 'p').then(($el) => { + top.postMessage({ host: location.host, text: $el.text() }, '*') + }) + }) }) diff --git a/packages/driver/src/cy/multidomain/index.ts b/packages/driver/src/cy/multidomain/index.ts new file mode 100644 index 000000000000..32cc6d85ca74 --- /dev/null +++ b/packages/driver/src/cy/multidomain/index.ts @@ -0,0 +1,14 @@ +export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State) { + Commands.addAll({ + switchToDomain (domain, fn) { + Cypress.log({ + name: 'switchToDomain', + type: 'parent', + message: domain, + end: true, + }) + + Cypress.action('cy:cross:domain:message', 'run:in:domain', fn.toString()) + }, + }) +} diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index d29cd9f27d16..5c202ea6cdd8 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -476,6 +476,18 @@ class $Cypress { case 'cy:scrolled': return this.emit('scrolled', ...args) + case 'app:cross:domain:window:load': + return this.emit('cross:domain:window:load', args[0]) + + case 'cy:switch:domain': + return this.emit('switch:domain', args[0]) + + case 'runner:cross:domain:driver:ready': + return this.emit('cross:domain:driver:ready') + + case 'cy:cross:domain:message': + return this.emit('cross:domain:message', ...args) + case 'app:uncaught:exception': return this.emitMap('uncaught:exception', ...args) diff --git a/packages/driver/src/cypress/commands.js b/packages/driver/src/cypress/commands.js index 89d6c737bbd8..526183089bd5 100644 --- a/packages/driver/src/cypress/commands.js +++ b/packages/driver/src/cypress/commands.js @@ -38,6 +38,7 @@ const builtInCommands = [ require('../cy/commands/window'), require('../cy/commands/xhr'), require('../cy/net-stubbing').addCommand, + require('../cy/multidomain').addCommands, ] const getTypeByPrevSubject = (prevSubject) => { diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index cd9924054565..c12ad6094427 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -1028,7 +1028,9 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { let onpl; let r try { - setWindowDocumentProps(getContentWindow($autIframe), state) + const autWindow = getContentWindow($autIframe) + + setWindowDocumentProps(autWindow, state) // we may need to update the url now urlNavigationEvent('load') @@ -1037,14 +1039,25 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { // because they would have been automatically applied during // onBeforeAppWindowLoad, but in the case where we visited // about:blank in a visit, we do need these - contentWindowListeners(getContentWindow($autIframe)) + contentWindowListeners(autWindow) Cypress.action('app:window:load', state('window')) - // we are now stable again which is purposefully - // the last event we call here, to give our event - // listeners time to be invoked prior to moving on - return stability.isStable(true, 'load') + // FIXME: temporary hard-coded hack to get multidomain working + if (!autWindow?.location?.pathname?.includes('multidomain-aut')) { + // we are now stable again which is purposefully + // the last event we call here, to give our event + // listeners time to be invoked prior to moving on + return stability.isStable(true, 'load') + } + + Cypress.once('cross:domain:window:load', () => { + Cypress.once('cross:domain:driver:ready', () => { + stability.isStable(true, 'load') + }) + + Cypress.action('cy:switch:domain', 'localhost:3501') + }) } catch (err) { let e = err diff --git a/packages/driver/src/multidomain/index.js b/packages/driver/src/multidomain/index.js index 5737213f923c..03d226350250 100644 --- a/packages/driver/src/multidomain/index.js +++ b/packages/driver/src/multidomain/index.js @@ -40,7 +40,19 @@ export const initialize = (autWindow) => { timeout () {}, }) - $Commands.create(Cypress, cy, Cypress.state) + $Commands.create(Cypress, cy, Cypress.state, Cypress.config) + + window.addEventListener('message', (event) => { + if (event.data && event.data.message === 'run:in:domain') { + const stringifiedTestFn = event.data.data + + autWindow.eval(`(${stringifiedTestFn})()`) + } + }, false) + + top.postMessage('cross:domain:driver:ready', '*') + + autWindow.cy = cy return { cy, diff --git a/packages/runner/multidomain/index.js b/packages/runner/multidomain/index.js index e72284fc337e..f77fe8efd366 100644 --- a/packages/runner/multidomain/index.js +++ b/packages/runner/multidomain/index.js @@ -1,15 +1,6 @@ import { initialize } from '@packages/driver/src/multidomain' -const autWindow = window.parent.frames[0] - -const { cy } = initialize(autWindow) - -autWindow.onReady = () => { - cy.now('get', 'p').then(($el) => { - // eslint-disable-next-line no-console - console.log('got the paragaph with text:', $el.text()) - }) -} +initialize(window.parent.frames[0]) /* diff --git a/packages/runner/src/iframe/iframes.jsx b/packages/runner/src/iframe/iframes.jsx index e6a97bf74cac..73dc90b42069 100644 --- a/packages/runner/src/iframe/iframes.jsx +++ b/packages/runner/src/iframe/iframes.jsx @@ -90,6 +90,8 @@ export default class Iframes extends Component { this.props.eventManager.on('print:selector:elements:to:console', this._printSelectorElementsToConsole) + this.props.eventManager.on('switch:domain', this._addCrossDomainIframe) + this._disposers.push(autorun(() => { this.autIframe.toggleSelectorPlayground(selectorPlaygroundModel.isEnabled) })) @@ -148,14 +150,30 @@ export default class Iframes extends Component { this.autIframe.showBlankContents() + this._addIframe({ + $container, + id: `Your Spec: ${specSrc}`, + src: specSrc, + }) + + return $autIframe + } + + _addCrossDomainIframe = (domain) => { + this._addIframe({ + $container: $(this.refs.container), + id: `Cypress (${domain})`, + src: `http://${domain}/${this.props.config.namespace}/multidomain-iframes/${encodeURIComponent(domain)}`, + }) + } + + _addIframe ({ $container, id, src }) { const $specIframe = $('