From 6b827dd95dc3e0a27d4e495bedb0f2cb2844a6ce Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Wed, 26 May 2021 15:11:35 -0400 Subject: [PATCH 001/177] 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/177] 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 = $('`) - - $iframe.appendTo($iframeContainer) - iframes.push($iframe) - }) - - let onPostMessage - - const successOrigins = [] as string[] - - return new Bluebird((resolve) => { - onPostMessage = (event) => { - const data = event.data - - if (data.type === 'set:storage:load') { - if (!event.source) { - throw new Error('failed to get localStorage') - } - - const opts = _.find(originOptions, { origin: event.origin })! - - event.source.postMessage({ type: 'set:storage:data', data: opts }, '*') - } else if (data.type === 'set:storage:complete') { - successOrigins.push(event.origin) - if (successOrigins.length === origins.length) { - resolve() - } - } - } - - specWindow.addEventListener('message', onPostMessage) - }) - // timeout just in case something goes wrong and the iframe never loads in - .timeout(2000) - .finally(() => { - specWindow.removeEventListener('message', onPostMessage) - $iframeContainer.remove() - }) - .catch(() => { - Cypress.log({ - name: 'warning', - message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, - }) - }) -} - -const getConsoleProps = (sessState: SessionData) => { - const sessionDetails = getSessionDetailsForTable(sessState) - - const tables = _.flatMap(sessionDetails, (val, domain) => { - const cookiesTable = () => { - return { - name: `🍪 Cookies - ${domain} (${val.cookies.length})`, - data: val.cookies, - } - } - - const localStorageTable = () => { - return { - name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`, - data: _.map(val.localStorage.value, (value, key) => { - return { - key, - value, - } - }), - } - } - - return [ - val.cookies && cookiesTable, - val.localStorage && localStorageTable, - ] - }) - - return { - id: sessState.id, - table: _.compact(tables), - } -} - -const getPostMessageLocalStorage = (specWindow, origins): Promise => { - const results = [] as any[] - const iframes: JQuery[] = [] - let onPostMessage - const successOrigins = [] as string[] - - const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) - - _.each(origins, (u) => { - const $iframe = $(``) - - $iframe.appendTo($iframeContainer) - iframes.push($iframe) - }) - - return new Bluebird((resolve) => { - // when the cross-domain iframe for each domain is loaded - // we can only communicate through postmessage - onPostMessage = ((event) => { - const data = event.data - - if (data.type !== 'localStorage') return - - const value = data.value - - results.push([event.origin, value]) - - successOrigins.push(event.origin) - if (successOrigins.length === origins.length) { - resolve(results) - } - }) - - specWindow.addEventListener('message', onPostMessage) - }) - // timeout just in case something goes wrong and the iframe never loads in - .timeout(2000) - .finally(() => { - specWindow.removeEventListener('message', onPostMessage) - $iframeContainer.remove() - }) - .catch((err) => { - Cypress.log({ - name: 'warning', - message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, - }) - - return [] - }) -} - export default function (Commands, Cypress, cy) { const { Promise } = Cypress @@ -867,9 +686,3 @@ export default function (Commands, Cypress, cy) { Cypress.session = sessions } - -function navigateAboutBlank (session = true) { - Cypress.action('cy:url:changed', '') - - return Cypress.action('cy:visit:blank', { type: session ? 'session' : 'session-lifecycle' }) as unknown as Promise -} diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts new file mode 100644 index 000000000000..9f20ebea5021 --- /dev/null +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -0,0 +1,208 @@ +import _ from 'lodash' +import $ from 'jquery' +import { $Location } from '../../../cypress/location' +import Bluebird from 'bluebird' + +type SessionData = Cypress.Commands.Session.SessionData + +const getSessionDetails = (sessState: SessionData) => { + return { + id: sessState.id, + data: _.merge( + _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v.length })), + ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: Object.keys(v.value).length } })), + ) } +} + +const getSessionDetailsForTable = (sessState: SessionData) => { + return _.merge( + _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v })), + ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: v } })), + ) +} + +const isSecureContext = (url: string) => url.startsWith('https:') + +const getCurrentOriginStorage = () => { + // localStorage.length property is not always accurate, we must stringify to check for entries + // for ex) try setting localStorage.key = 'val' and reading localStorage.length, may be 0. + const _localStorageStr = JSON.stringify(window.localStorage) + const _localStorage = _localStorageStr.length > 2 && JSON.parse(_localStorageStr) + const _sessionStorageStr = JSON.stringify(window.sessionStorage) + const _sessionStorage = _sessionStorageStr.length > 2 && JSON.parse(JSON.stringify(window.sessionStorage)) + + const value = {} as any + + if (_localStorage) { + value.localStorage = _localStorage + } + + if (_sessionStorage) { + value.sessionStorage = _sessionStorage + } + + return value +} + +const setPostMessageLocalStorage = async (specWindow, originOptions) => { + const origins = originOptions.map((v) => v.origin) as string[] + + const iframes: JQuery[] = [] + + const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) + + // if we're on an https domain, there is no way for the secure context to access insecure origins from iframes + // since there is no way for the app to access localStorage on insecure contexts, we don't have to clear any localStorage on http domains. + if (isSecureContext(specWindow.location.href)) { + _.remove(origins, (v) => !isSecureContext(v)) + } + + if (!origins.length) return [] + + _.each(origins, (u) => { + const $iframe = $(``) + + $iframe.appendTo($iframeContainer) + iframes.push($iframe) + }) + + let onPostMessage + + const successOrigins = [] as string[] + + return new Bluebird((resolve) => { + onPostMessage = (event) => { + const data = event.data + + if (data.type === 'set:storage:load') { + if (!event.source) { + throw new Error('failed to get localStorage') + } + + const opts = _.find(originOptions, { origin: event.origin })! + + event.source.postMessage({ type: 'set:storage:data', data: opts }, '*') + } else if (data.type === 'set:storage:complete') { + successOrigins.push(event.origin) + if (successOrigins.length === origins.length) { + resolve() + } + } + } + + specWindow.addEventListener('message', onPostMessage) + }) + // timeout just in case something goes wrong and the iframe never loads in + .timeout(2000) + .finally(() => { + specWindow.removeEventListener('message', onPostMessage) + $iframeContainer.remove() + }) + .catch(() => { + Cypress.log({ + name: 'warning', + message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, + }) + }) +} + +const getConsoleProps = (sessState: SessionData) => { + const sessionDetails = getSessionDetailsForTable(sessState) + + const tables = _.flatMap(sessionDetails, (val, domain) => { + const cookiesTable = () => { + return { + name: `🍪 Cookies - ${domain} (${val.cookies.length})`, + data: val.cookies, + } + } + + const localStorageTable = () => { + return { + name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`, + data: _.map(val.localStorage.value, (value, key) => { + return { + key, + value, + } + }), + } + } + + return [ + val.cookies && cookiesTable, + val.localStorage && localStorageTable, + ] + }) + + return { + id: sessState.id, + table: _.compact(tables), + } +} + +const getPostMessageLocalStorage = (specWindow, origins): Promise => { + const results = [] as any[] + const iframes: JQuery[] = [] + let onPostMessage + const successOrigins = [] as string[] + + const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) + + _.each(origins, (u) => { + const $iframe = $(``) + + $iframe.appendTo($iframeContainer) + iframes.push($iframe) + }) + + return new Bluebird((resolve) => { + // when the cross-domain iframe for each domain is loaded + // we can only communicate through postmessage + onPostMessage = ((event) => { + const data = event.data + + if (data.type !== 'localStorage') return + + const value = data.value + + results.push([event.origin, value]) + + successOrigins.push(event.origin) + if (successOrigins.length === origins.length) { + resolve(results) + } + }) + + specWindow.addEventListener('message', onPostMessage) + }) + // timeout just in case something goes wrong and the iframe never loads in + .timeout(2000) + .finally(() => { + specWindow.removeEventListener('message', onPostMessage) + $iframeContainer.remove() + }) + .catch((err) => { + Cypress.log({ + name: 'warning', + message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, + }) + + return [] + }) +} + +function navigateAboutBlank (session: boolean = true) { + Cypress.action('cy:url:changed', '') + + return Cypress.action('cy:visit:blank', { type: session ? 'session' : 'session-lifecycle' }) as unknown as Promise +} + +export { + getSessionDetails, + getCurrentOriginStorage, + setPostMessageLocalStorage, + getConsoleProps, + getPostMessageLocalStorage, + navigateAboutBlank, +} From 68173be4aa3a9d6c64cc447aa68d8f374bc3c066 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Apr 2022 12:39:48 +1000 Subject: [PATCH 107/177] chore: bump minimist from 1.2.5 to 1.2.6 (#21134) Co-authored-by: Lachlan Miller --- .../visual-testing-with-applitools/yarn.lock | 161 ++++++++---------- 1 file changed, 74 insertions(+), 87 deletions(-) diff --git a/npm/react/examples/visual-testing-with-applitools/yarn.lock b/npm/react/examples/visual-testing-with-applitools/yarn.lock index 118dc77203bf..806deba572e6 100644 --- a/npm/react/examples/visual-testing-with-applitools/yarn.lock +++ b/npm/react/examples/visual-testing-with-applitools/yarn.lock @@ -175,40 +175,6 @@ "@cypress/react@file:../../dist": version "0.0.0" -"@oozcitak/dom@1.15.8": - version "1.15.8" - resolved "https://registry.yarnpkg.com/@oozcitak/dom/-/dom-1.15.8.tgz#0c0c7bb54cfdaadc07fd637913e706101721d15d" - integrity sha512-MoOnLBNsF+ok0HjpAvxYxR4piUhRDCEWK0ot3upwOOHYudJd30j6M+LNcE8RKpwfnclAX9T66nXXzkytd29XSw== - dependencies: - "@oozcitak/infra" "1.0.8" - "@oozcitak/url" "1.0.4" - "@oozcitak/util" "8.3.8" - -"@oozcitak/infra@1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@oozcitak/infra/-/infra-1.0.8.tgz#b0b089421f7d0f6878687608301fbaba837a7d17" - integrity sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg== - dependencies: - "@oozcitak/util" "8.3.8" - -"@oozcitak/url@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@oozcitak/url/-/url-1.0.4.tgz#ca8b1c876319cf5a648dfa1123600a6aa5cda6ba" - integrity sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw== - dependencies: - "@oozcitak/infra" "1.0.8" - "@oozcitak/util" "8.3.8" - -"@oozcitak/util@8.3.8": - version "8.3.8" - resolved "https://registry.yarnpkg.com/@oozcitak/util/-/util-8.3.8.tgz#10f65fe1891fd8cde4957360835e78fd1936bfdd" - integrity sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ== - -"@types/node@14.6.2": - version "14.6.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f" - integrity sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A== - "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -269,10 +235,10 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" @@ -288,13 +254,6 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - array-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" @@ -399,6 +358,11 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -476,6 +440,11 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + css-tree@^1.0.0-alpha.39: version "1.0.0-alpha.39" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" @@ -501,14 +470,6 @@ cssstyle@^2.0.0: dependencies: cssom "~0.3.6" -cypress-circleci-reporter@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/cypress-circleci-reporter/-/cypress-circleci-reporter-0.2.0.tgz#a3e1571694f4e21649a6af4d508e68948d23622d" - integrity sha512-uhqcJwvtKJ7Bw3RHVBTqUH9GP2L6jq+qLp/+/Jh3/OSe5Af6H7RxIARhvawsvbPrg9lMWdW/jCezjeUcXrl9uA== - dependencies: - strip-ansi "^6.0.0" - xmlbuilder2 "^2.1.1" - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -530,7 +491,7 @@ dateformat@^3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -debug@2.6.9: +debug@2.6.9, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -630,7 +591,7 @@ escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -esprima@^4.0.0, esprima@^4.0.1: +esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -880,6 +841,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -895,14 +861,6 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -971,11 +929,25 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + lodash@^4.17.19: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +md5@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + mdn-data@2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" @@ -1013,10 +985,10 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.5, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^0.5.1: version "0.5.5" @@ -1025,6 +997,32 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha-junit-reporter@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.0.2.tgz#d521689b651dc52f52044739f8ffb368be415731" + integrity sha512-vYwWq5hh3v1lG0gdQCBxwNipBfvDiAM1PHroQRNp96+2l72e9wEUTw+mzoK+O0SudgfQ7WvTQZ9Nh3qkAYAjfg== + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^6.0.1" + xml "^1.0.0" + +mocha-multi-reporters@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz#c73486bed5519e1d59c9ce39ac7a9792600e5676" + integrity sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg== + dependencies: + debug "^4.1.1" + lodash "^4.17.15" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -1297,11 +1295,6 @@ source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -1332,12 +1325,12 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" supports-color@^5.3.0: version "5.5.0" @@ -1522,16 +1515,10 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xmlbuilder2@^2.1.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/xmlbuilder2/-/xmlbuilder2-2.4.0.tgz#fb6c5171bef1bcb984c88cfef5210e17b7b841cd" - integrity sha512-KrOVUGD65xTQ7ZA+GMQGdBSpe1Ufu5ylCQSYVk6QostySDkxPmAQ0WWIu7dR3JjLfVbF22RFQX7KyrZ6VTLcQg== - dependencies: - "@oozcitak/dom" "1.15.8" - "@oozcitak/infra" "1.0.8" - "@oozcitak/util" "8.3.8" - "@types/node" "14.6.2" - js-yaml "3.14.0" +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= xmlchars@^2.1.1: version "2.2.0" From d7ce86541db67561a51bc20d165144e27b5b1423 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Wed, 20 Apr 2022 23:03:08 -0600 Subject: [PATCH 108/177] chore: (cross-origin) add support for redirecting back to primary (#21144) --- .../integration/commands/navigation_spec.js | 57 ++---- .../commands/multi_domain_navigation.spec.ts | 83 +++++++- packages/driver/src/cy/commands/navigation.ts | 61 +++--- packages/driver/src/cy/multi-domain/index.ts | 16 +- packages/driver/src/cypress/error_messages.ts | 24 ++- packages/driver/types/internal-types.d.ts | 3 +- .../proxy/lib/http/response-middleware.ts | 6 +- .../unit/http/response-middleware.spec.ts | 44 +---- packages/server/lib/experiments.ts | 2 +- packages/server/lib/remote_states.ts | 10 +- packages/server/lib/server-base.ts | 4 +- packages/server/lib/server-e2e.ts | 13 +- packages/server/lib/socket-base.ts | 6 +- .../test/integration/http_requests_spec.js | 2 +- .../server/test/integration/server_spec.js | 181 +++++++++++++----- .../server/test/unit/remote_states.spec.ts | 49 ++--- packages/server/test/unit/socket_spec.js | 21 +- .../runnable_execution_spec.ts.js | 8 +- 18 files changed, 371 insertions(+), 219 deletions(-) diff --git a/packages/driver/cypress/integration/commands/navigation_spec.js b/packages/driver/cypress/integration/commands/navigation_spec.js index b4bda839fbc1..b6b4f0155e3b 100644 --- a/packages/driver/cypress/integration/commands/navigation_spec.js +++ b/packages/driver/cypress/integration/commands/navigation_spec.js @@ -1414,10 +1414,10 @@ describe('src/cy/commands/navigation', () => { \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n You likely forgot to use \`cy.origin()\`:\n \`cy.visit('http://localhost:3500/fixtures/generic.html')\` - \`\`\n + \`\`\n \`cy.origin('http://localhost:3501', () => {\` \` cy.visit('http://localhost:3501/fixtures/generic.html')\` - \` \` + \` \` \`})\`\n The new URL is considered a different origin because the following parts of the URL are different:\n > port\n @@ -1446,18 +1446,18 @@ describe('src/cy/commands/navigation', () => { \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n You likely forgot to use \`cy.origin()\`:\n \`cy.visit('http://localhost:3500/fixtures/generic.html')\` - \`\`\n - \`cy.origin('https://localhost:3500', () => {\` - \` cy.visit('https://localhost:3500/fixtures/generic.html')\` - \` \` + \`\`\n + \`cy.origin('https://localhost:3502', () => {\` + \` cy.visit('https://localhost:3502/fixtures/generic.html')\` + \` \` \`})\`\n The new URL is considered a different origin because the following parts of the URL are different:\n - > protocol\n + > protocol, port\n You may only \`cy.visit()\` same-origin URLs within a single test.\n The previous URL you visited was:\n > 'http://localhost:3500'\n You're attempting to visit this URL:\n - > 'https://localhost:3500'`) + > 'https://localhost:3502'`) expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain') assertLogLength(this.logs, 2) @@ -1467,7 +1467,7 @@ describe('src/cy/commands/navigation', () => { }) cy.visit('http://localhost:3500/fixtures/generic.html') - cy.visit('https://localhost:3500/fixtures/generic.html') + cy.visit('https://localhost:3502/fixtures/generic.html') }) it('throws when attempting to visit a 2nd domain on different superdomain', function (done) { @@ -1478,10 +1478,10 @@ describe('src/cy/commands/navigation', () => { \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n You likely forgot to use \`cy.origin()\`:\n \`cy.visit('http://localhost:3500/fixtures/generic.html')\` - \`\`\n - \`cy.origin('http://google.com:3500', () => {\` - \` cy.visit('http://google.com:3500/fixtures/generic.html')\` - \` \` + \`\`\n + \`cy.origin('http://foobar.com:3500', () => {\` + \` cy.visit('http://www.foobar.com:3500/fixtures/generic.html')\` + \` \` \`})\`\n The new URL is considered a different origin because the following parts of the URL are different:\n > superdomain\n @@ -1489,7 +1489,7 @@ describe('src/cy/commands/navigation', () => { The previous URL you visited was:\n > 'http://localhost:3500'\n You're attempting to visit this URL:\n - > 'http://google.com:3500'`) + > 'http://www.foobar.com:3500'`) expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain') assertLogLength(this.logs, 2) @@ -1499,7 +1499,7 @@ describe('src/cy/commands/navigation', () => { }) cy.visit('http://localhost:3500/fixtures/generic.html') - cy.visit('http://google.com:3500/fixtures/generic.html') + cy.visit('http://www.foobar.com:3500/fixtures/generic.html') }) it('throws attempting to visit 2 unique ip addresses', function (done) { @@ -1510,10 +1510,10 @@ describe('src/cy/commands/navigation', () => { \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n You likely forgot to use \`cy.origin()\`:\n \`cy.visit('http://127.0.0.1:3500/fixtures/generic.html')\` - \`\`\n - \`cy.origin('http://126.0.0.1:3500', () => {\` - \` cy.visit('http://126.0.0.1:3500/fixtures/generic.html')\` - \` \` + \`\`\n + \`cy.origin('http://0.0.0.0:3500', () => {\` + \` cy.visit('http://0.0.0.0:3500/fixtures/generic.html')\` + \` \` \`})\`\n The new URL is considered a different origin because the following parts of the URL are different:\n > superdomain\n @@ -1521,7 +1521,7 @@ describe('src/cy/commands/navigation', () => { The previous URL you visited was:\n > 'http://127.0.0.1:3500'\n You're attempting to visit this URL:\n - > 'http://126.0.0.1:3500'`) + > 'http://0.0.0.0:3500'`) expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain') assertLogLength(this.logs, 2) @@ -1532,22 +1532,7 @@ describe('src/cy/commands/navigation', () => { cy .visit('http://127.0.0.1:3500/fixtures/generic.html') - .visit('http://126.0.0.1:3500/fixtures/generic.html') - }) - - it('does not call resolve:url when throws attempting to visit a 2nd domain', (done) => { - const backend = cy.spy(Cypress, 'backend') - - cy.on('fail', (err) => { - expect(backend).to.be.calledWithMatch('resolve:url', 'http://localhost:3500/fixtures/generic.html') - expect(backend).not.to.be.calledWithMatch('resolve:url', 'http://google.com:3500/fixtures/generic.html') - - done() - }) - - cy - .visit('http://localhost:3500/fixtures/generic.html') - .visit('http://google.com:3500/fixtures/generic.html') + .visit('http://0.0.0.0:3500/fixtures/generic.html') }) it('displays loading_network_failed when _resolveUrl throws', function (done) { diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts index febb6e19e1af..e6bc322cdb45 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts @@ -178,11 +178,11 @@ context('cy.origin navigation', () => { You likely forgot to use \`cy.origin()\`:\n \`cy.origin('http://foobar.com:3500', () => {\` \` cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')\` - \` \` + \` \` \`})\`\n \`cy.origin('http://idp.com:3500', () => {\` \` cy.visit('http://www.idp.com:3500/fixtures/dom.html')\` - \` \` + \` \` \`})\`\n The new URL is considered a different origin because the following parts of the URL are different:\n > superdomain\n @@ -211,10 +211,10 @@ context('cy.origin navigation', () => { \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n \`cy.visit('http://localhost:3500/fixtures/multi-domain.html')\` - \`\`\n + \`\`\n \`cy.origin('http://foobar.com:3500', () => {\` \` cy.visit('http://www.foobar.com:3500/fixtures/dom.html')\` - \` \` + \` \` \`})\`\n The new URL is considered a different origin because the following parts of the URL are different:\n > superdomain\n @@ -312,7 +312,7 @@ context('cy.origin navigation', () => { }) }) - it('supports visit redirects', () => { + it('supports redirecting from primary to secondary in cy.origin', () => { cy.visit('/fixtures/multi-domain.html') cy.origin('http://www.foobar.com:3500', () => { @@ -321,6 +321,79 @@ context('cy.origin navigation', () => { }) }) + it('supports redirecting from secondary to primary outside of cy.origin', () => { + cy.visit('/fixtures/multi-domain.html') + cy.visit('http://www.foobar.com:3500/redirect?href=http://localhost:3500/fixtures/generic.html') + }) + + it('errors when trying to redirect from secondary to primary in cy.origin', (done) => { + cy.on('fail', (e) => { + expect(e.message).to.equal(stripIndent` + \`cy.visit()\` failed because you are attempting to visit a URL from a previous origin inside of \`cy.origin()\`.\n + Instead of placing the \`cy.visit()\` inside of \`cy.origin()\`, the \`cy.visit()\` should be placed outside of the \`cy.origin()\` block.\n + \`\`\n + \`cy.origin('http://foobar.com:3500', () => {\` + \` \` + \`})\`\n + \`cy.visit('http://www.foobar.com:3500/redirect?href=http://localhost:3500/fixtures/generic.html')\``) + + done() + }) + + cy.visit('http://localhost:3500/fixtures/multi-domain.html') + + cy.origin('http://www.foobar.com:3500', () => { + cy.visit('/redirect?href=http://localhost:3500/fixtures/generic.html') + }) + }) + + it('errors when trying to visit primary in cy.origin', (done) => { + cy.on('fail', (e) => { + expect(e.message).to.equal(stripIndent` + \`cy.visit()\` failed because you are attempting to visit a URL from a previous origin inside of \`cy.origin()\`.\n + Instead of placing the \`cy.visit()\` inside of \`cy.origin()\`, the \`cy.visit()\` should be placed outside of the \`cy.origin()\` block.\n + \`\`\n + \`cy.origin('http://foobar.com:3500', () => {\` + \` \` + \`})\`\n + \`cy.visit('http://localhost:3500/fixtures/generic.html')\``) + + done() + }) + + cy.visit('http://localhost:3500/fixtures/multi-domain.html') + + cy.origin('http://www.foobar.com:3500', () => { + cy.visit('http://localhost:3500/fixtures/generic.html') + }) + }) + + it('errors when trying to redirect from primary to secondary outside of cy.origin', (done) => { + cy.on('fail', (e) => { + expect(e.message).to.equal(stripIndent`\ + \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n + You likely forgot to use \`cy.origin()\`:\n + \`cy.visit('http://localhost:3500/fixtures/multi-domain.html')\` + \`\`\n + \`cy.origin('http://foobar.com:3500', () => {\` + \` cy.visit('http://localhost:3500/redirect?href=http://www.foobar.com:3500/fixtures/generic.html')\` + \` \` + \`})\`\n + The new URL is considered a different origin because the following parts of the URL are different:\n + > superdomain\n + You may only \`cy.visit()\` same-origin URLs within a single test.\n + The previous URL you visited was:\n + > 'http://localhost:3500'\n + You're attempting to visit this URL:\n + > 'http://www.foobar.com:3500'`) + + done() + }) + + cy.visit('/fixtures/multi-domain.html') + cy.visit('http://localhost:3500/redirect?href=http://www.foobar.com:3500/fixtures/generic.html') + }) + it('supports auth options and adding auth to subsequent requests', () => { cy.origin('http://foobar.com:3500', () => { cy.visit('http://www.foobar.com:3500/basic_auth', { diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index 8e6f2d4a64ef..99c64ea9f369 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -90,7 +90,7 @@ const timedOutWaitingForPageLoad = (ms, log) => { } } -const cannotVisitDifferentOrigin = ({ remote, existing, previousUrlVisited, log, isCrossOriginSpecBridge = false }) => { +const cannotVisitDifferentOrigin = ({ remote, existing, originalUrl, previousUrlVisited, log, isCrossOriginSpecBridge = false }) => { const differences: string[] = [] if (remote.protocol !== existing.protocol) { @@ -111,6 +111,7 @@ const cannotVisitDifferentOrigin = ({ remote, existing, previousUrlVisited, log, differences: differences.join(', '), previousUrl: previousUrlVisited, attemptedUrl: remote, + originalUrl, isCrossOriginSpecBridge, experimentalSessionAndOrigin: Cypress.config('experimentalSessionAndOrigin'), }, @@ -122,6 +123,22 @@ const cannotVisitDifferentOrigin = ({ remote, existing, previousUrlVisited, log, $errUtils.throwErrByPath('visit.cannot_visit_different_origin', errOpts) } +const cannotVisitPreviousOrigin = ({ remote, originalUrl, previousUrlVisited, log }) => { + const errOpts = { + onFail: log, + args: { + attemptedUrl: remote, + previousUrl: previousUrlVisited, + originalUrl, + }, + errProps: { + isCrossOrigin: true, + }, + } + + $errUtils.throwErrByPath('origin.cannot_visit_previous_origin', errOpts) +} + const specifyFileByRelativePath = (url, log) => { $errUtils.throwErrByPath('visit.specify_file_by_relative_path', { onFail: log, @@ -494,6 +511,7 @@ const normalizeOptions = (options) => { .extend({ timeout: options.responseTimeout, isCrossOrigin: Cypress.isCrossOriginSpecBridge, + hasAlreadyVisitedUrl: options.hasAlreadyVisitedUrl, }) .value() } @@ -833,6 +851,8 @@ export default (Commands, Cypress, cy, state, config) => { onLoad () {}, }) + options.hasAlreadyVisitedUrl = !!previousUrlVisited + if (!_.isUndefined(options.qs) && !_.isObject(options.qs)) { $errUtils.throwErrByPath('visit.invalid_qs', { args: { qs: String(options.qs) } }) } @@ -1026,17 +1046,6 @@ export default (Commands, Cypress, cy, state, config) => { const existingHash = remote.hash || '' const existingAuth = remote.auth || '' - if (previousUrlVisited && (remote.originPolicy !== existing.originPolicy)) { - // if we've already visited a new superDomain - // then die else we'd be in a terrible endless loop - // we also need to disable retries to prevent the endless loop - $utils.getTestFromRunnable(state('runnable'))._retries = 0 - - const params = { remote, existing, previousUrlVisited, log: options._log } - - return cannotVisitDifferentOrigin(params) - } - // in a cross origin spec bridge, the window may not have been set yet if nothing has been loaded in the secondary origin, // it's also possible for a new test to start and for a cross-origin failure to occur if the win is set but // the AUT hasn't yet navigated to the secondary origin @@ -1082,7 +1091,7 @@ export default (Commands, Cypress, cy, state, config) => { return requestUrl(url, options) .then((resp: any = {}) => { - let { url, originalUrl, cookies, redirects, filePath } = resp + let { url, originalUrl, cookies, redirects, filePath, isPrimaryOrigin } = resp // reapply the existing hash url += existingHash @@ -1114,7 +1123,6 @@ export default (Commands, Cypress, cy, state, config) => { // if the origin currently matches // then go ahead and change the iframe's src - // and we're good to go if (remote.originPolicy === existing.originPolicy) { previousUrlVisited = remote @@ -1126,22 +1134,25 @@ export default (Commands, Cypress, cy, state, config) => { }) } - // if we are in a cross origin spec bridge and the origin policies weren't the same, - // we need to throw an error since the user tried to visit a new - // origin which isn't allowed within a cy.origin block - if (Cypress.isCrossOriginSpecBridge && win) { - const existingAutOrigin = $Location.create(win.location.href) - const params = { remote, existing, previousUrlVisited: existingAutOrigin, log: options._log, isCrossOriginSpecBridge: true } + // if we've already cy.visit'ed in the test and we are visiting a new origin, + // throw an error, else we'd be in a endless loop, + // we also need to disable retries to prevent the endless loop + if (previousUrlVisited) { + $utils.getTestFromRunnable(state('runnable'))._retries = 0 + + const params = { remote, existing, originalUrl, previousUrlVisited, log: options._log } return cannotVisitDifferentOrigin(params) } - // if we've already visited a new origin - // then die else we'd be in a terrible endless loop - if (previousUrlVisited) { - const params = { remote, existing, previousUrlVisited, log: options._log } + // if we are in a cross origin spec bridge and the origin policies weren't the same, + // we need to throw an error since the user tried to visit a new + // origin which isn't allowed within a cy.origin block + if (Cypress.isCrossOriginSpecBridge) { + const existingAutOrigin = win ? $Location.create(win.location.href) : $Location.create(Cypress.state('currentActiveOriginPolicy')) + const params = { remote, existing, originalUrl, previousUrlVisited: existingAutOrigin, log: options._log, isCrossOriginSpecBridge: true, isPrimaryOrigin } - return cannotVisitDifferentOrigin(params) + return isPrimaryOrigin ? cannotVisitPreviousOrigin(params) : cannotVisitDifferentOrigin(params) } // tell our backend we're changing origins diff --git a/packages/driver/src/cy/multi-domain/index.ts b/packages/driver/src/cy/multi-domain/index.ts index 44299336918b..70ce6fd5df35 100644 --- a/packages/driver/src/cy/multi-domain/index.ts +++ b/packages/driver/src/cy/multi-domain/index.ts @@ -43,11 +43,11 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, } // If we haven't seen a cy.origin and cleared the timeout within 300ms, - // go ahead and inform the server 'ready:for:origin' failed and to release the - // response. This typically happens during a redirect where the user does + // go ahead and inform the server to release the response. + // This typically happens during a redirect where the user does // not have a cy.origin for the intermediary origin. timeoutId = setTimeout(() => { - Cypress.backend('ready:for:origin', { failed: true }) + Cypress.backend('cross:origin:release:html') }, 300) }) @@ -94,7 +94,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, const validator = new Validator({ log, onFailure: () => { - Cypress.backend('ready:for:origin', { failed: true }) + Cypress.backend('cross:origin:release:html') }, }) @@ -163,7 +163,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // lets the proxy know to allow the response for the secondary // origin html through, so the page will finish loading - Cypress.backend('ready:for:origin', { originPolicy: location.originPolicy }) + Cypress.backend('cross:origin:release:html') if (err) { if (err?.name === 'ReferenceError') { @@ -202,7 +202,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, } // fired once the spec bridge is set up and ready to receive messages - communicator.once('bridge:ready', (_data, specBridgeOriginPolicy) => { + communicator.once('bridge:ready', async (_data, specBridgeOriginPolicy) => { if (specBridgeOriginPolicy === originPolicy) { // now that the spec bridge is ready, instantiate Cypress with the current app config and environment variables for initial sync when creating the instance communicator.toSpecBridge(originPolicy, 'initialize:cypress', { @@ -210,6 +210,8 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, env: preprocessEnv(Cypress.env()), }) + await Cypress.backend('cross:origin:bridge:ready', { originPolicy }) + // once the secondary origin page loads, send along the // user-specified callback to run in that origin try { @@ -237,7 +239,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, }) } catch (err: any) { // Release the request if 'run:origin:fn' fails - Cypress.backend('ready:for:origin', { failed: true }) + Cypress.backend('cross:origin:release:html') const wrappedErr = $errUtils.errByPath('origin.run_origin_fn_errored', { error: err.message, diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index f7f41d972415..937a349e2a17 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1237,6 +1237,22 @@ export default { docsUrl: 'https://on.cypress.io/session-api', }, }, + cannot_visit_previous_origin (args) { + return { + message: stripIndent`\ + ${cmd('visit')} failed because you are attempting to visit a URL from a previous origin inside of ${cmd('origin')}. + + Instead of placing the ${cmd('visit')} inside of ${cmd('origin')}, the ${cmd('visit')} should be placed outside of the ${cmd('origin')} block. + + \`\` + + \`cy.origin('${args.previousUrl.originPolicy}', () => {\` + \` \` + \`})\` + + \`cy.visit('${args.originalUrl}')\``, + } + }, }, proxy: { @@ -2105,15 +2121,15 @@ export default { ${args.isCrossOriginSpecBridge ? `\`cy.origin('${args.previousUrl.originPolicy}', () => {\` \` cy.visit('${args.previousUrl}')\` - \` \` + \` \` \`})\`` : `\`cy.visit('${args.previousUrl}')\` - \`\`` + \`\`` } \`cy.origin('${args.attemptedUrl.originPolicy}', () => {\` - \` cy.visit('${args.attemptedUrl}')\` - \` \` + \` cy.visit('${args.originalUrl}')\` + \` \` \`})\` The new URL is considered a different origin because the following parts of the URL are different: diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index 6f4697214b9d..82a0754ded3e 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -23,7 +23,8 @@ declare namespace Cypress { } interface Backend { - (task: 'ready:for:origin', args: { originPolicy?: string , failed?: boolean}): boolean + (task: 'cross:origin:release:html'): boolean + (task: 'cross:origin:bridge:ready', args: { originPolicy?: string }): boolean (task: 'cross:origin:finished', originPolicy: string): boolean } diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 5768bc4dc3df..82697b51c37c 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -238,10 +238,10 @@ const MaybeDelayForCrossOrigin: ResponseMiddleware = function () { // delay the response if this is a cross-origin (and not returning to a previous origin) html request from the AUT iframe if (this.config.experimentalSessionAndOrigin && isCrossOrigin && !isPreviousOrigin && isAUTFrame && (isHTML || isRenderedHTML)) { - this.debug('is cross-origin, delay until ready:for:origin event') + this.debug('is cross-origin, delay until cross:origin:release:html event') - this.serverBus.once('ready:for:origin', ({ failed }) => { - this.debug(`received ready:for:origin${failed ? ' failed' : ''}, let the response proceed`) + this.serverBus.once('cross:origin:release:html', () => { + this.debug(`received cross:origin:release:html, let the response proceed`) this.next() }) diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts index 5c9f215cb956..a3faa680f77c 100644 --- a/packages/proxy/test/unit/http/response-middleware.spec.ts +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -196,7 +196,7 @@ describe('http/response-middleware', function () { }) }) - it('waits for server signal if req is not of a previous origin, letting it continue after receiving ready:for:origin', function () { + it('waits for server signal if req is not of a previous origin, letting it continue after receiving cross:origin:release:html', function () { prepareContext({ req: { isAUTFrame: true, @@ -217,12 +217,12 @@ describe('http/response-middleware', function () { expect(ctx.serverBus.emit).to.be.calledWith('cross:origin:delaying:html', { href: 'http://www.idp.com/test' }) - ctx.serverBus.once.withArgs('ready:for:origin').args[0][1]({ originPolicy: 'http://idp.com' }) + ctx.serverBus.once.withArgs('cross:origin:release:html').args[0][1]() return promise }) - it('waits for server signal if res is html, letting it continue after receiving ready:for:origin', function () { + it('waits for server signal if res is html, letting it continue after receiving cross:origin:release:html', function () { prepareContext({ incomingRes: { headers: { @@ -242,12 +242,12 @@ describe('http/response-middleware', function () { expect(ctx.serverBus.emit).to.be.calledWith('cross:origin:delaying:html', { href: 'http://www.foobar.com/test' }) - ctx.serverBus.once.withArgs('ready:for:origin').args[0][1]({ originPolicy: 'http://foobar.com' }) + ctx.serverBus.once.withArgs('cross:origin:release:html').args[0][1]() return promise }) - it('waits for server signal if incomingRes is rendered html, letting it continue after receiving ready:for:origin', function () { + it('waits for server signal if incomingRes is rendered html, letting it continue after receiving cross:origin:release:html', function () { prepareContext({ req: { headers: { @@ -268,33 +268,7 @@ describe('http/response-middleware', function () { expect(ctx.serverBus.emit).to.be.calledWith('cross:origin:delaying:html', { href: 'http://www.foobar.com/test' }) - ctx.serverBus.once.withArgs('ready:for:origin').args[0][1]({ originPolicy: 'http://foobar.com' }) - - return promise - }) - - it('waits for server signal, letting it continue after receiving ready:for:origin failed', function () { - prepareContext({ - req: { - isAUTFrame: true, - proxiedUrl: 'http://www.idp.com/test', - }, - incomingRes: { - headers: { - 'content-type': 'text/html', - }, - }, - secondaryOrigins: ['http://foobar.com', 'http://example.com'], - config: { - experimentalSessionAndOrigin: true, - }, - }) - - const promise = testMiddleware([MaybeDelayForCrossOrigin], ctx) - - expect(ctx.serverBus.emit).to.be.calledWith('cross:origin:delaying:html', { href: 'http://www.idp.com/test' }) - - ctx.serverBus.once.withArgs('ready:for:origin').args[0][1]({ failed: true }) + ctx.serverBus.once.withArgs('cross:origin:release:html').args[0][1]() return promise }) @@ -309,7 +283,7 @@ describe('http/response-middleware', function () { // set the secondary remote states remoteStates.addEventListeners(eventEmitter) props.secondaryOrigins?.forEach((originPolicy) => { - eventEmitter.emit('ready:for:origin', { originPolicy }) + eventEmitter.emit('cross:origin:bridge:ready', { originPolicy }) }) ctx = { @@ -592,7 +566,7 @@ describe('http/response-middleware', function () { // set the secondary remote states remoteStates.addEventListeners(eventEmitter) props.secondaryOrigins?.forEach((originPolicy) => { - eventEmitter.emit('ready:for:origin', { originPolicy }) + eventEmitter.emit('cross:origin:bridge:ready', { originPolicy }) }) ctx = { @@ -914,7 +888,7 @@ describe('http/response-middleware', function () { // set the secondary remote states remoteStates.addEventListeners(eventEmitter) props.secondaryOrigins?.forEach((originPolicy) => { - eventEmitter.emit('ready:for:origin', { originPolicy }) + eventEmitter.emit('cross:origin:bridge:ready', { originPolicy }) }) return { diff --git a/packages/server/lib/experiments.ts b/packages/server/lib/experiments.ts index b962cbdb8b2e..0a0e876c76f0 100644 --- a/packages/server/lib/experiments.ts +++ b/packages/server/lib/experiments.ts @@ -71,7 +71,7 @@ const _summaries: StringValues = { const _names: StringValues = { experimentalFetchPolyfill: 'Fetch Polyfill', experimentalInteractiveRunEvents: 'Interactive Mode Run Events', - experimentalSessionAndOrigin: 'Login Flows', + experimentalSessionAndOrigin: 'Cross-origin and Session', experimentalSourceRewriting: 'Improved Source Rewriting', experimentalStudio: 'Studio', } diff --git a/packages/server/lib/remote_states.ts b/packages/server/lib/remote_states.ts index 7049e78067c1..f9ceb3e6a056 100644 --- a/packages/server/lib/remote_states.ts +++ b/packages/server/lib/remote_states.ts @@ -139,14 +139,8 @@ export class RemoteStates { } addEventListeners (eventEmitter: EventEmitter) { - eventEmitter.on('ready:for:origin', ({ originPolicy, failed }) => { - if (failed) { - debug('received ready:for:origin failed, don\'t add origin to remote states') - - return - } - - debug(`received ready:for:origin, add origin ${originPolicy} to remote states`) + eventEmitter.on('cross:origin:bridge:ready', ({ originPolicy }) => { + debug(`received cross:origin:bridge:ready, add origin ${originPolicy} to remote states`) const existingOrigin = this.remoteStates.get(originPolicy) diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index cf87e986f852..edad746878e2 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -166,8 +166,8 @@ export abstract class ServerBase { setupCrossOriginRequestHandling () { this._eventBus.on('cross:origin:delaying:html', (request) => { - this.socket.localBus.once('ready:for:origin', (args) => { - this._eventBus.emit('ready:for:origin', args) + this.socket.localBus.once('cross:origin:release:html', () => { + this._eventBus.emit('cross:origin:release:html') }) this.socket.toDriver('cross:origin:delaying:html', request) diff --git a/packages/server/lib/server-e2e.ts b/packages/server/lib/server-e2e.ts index a87a8cc37210..8dadffa18768 100644 --- a/packages/server/lib/server-e2e.ts +++ b/packages/server/lib/server-e2e.ts @@ -291,11 +291,12 @@ export class ServerE2E extends ServerBase { details.totalTime = Date.now() - startTime - // TODO: think about moving this logic back into the - // frontend so that the driver can be in control of - // when the server should cache the request buffer - // and set the domain vs not - if (isOk && details.isHtml) { + // buffer the response and set the remote state if this is a successful html response that is for the same + // origin if the user has already visited an origin or if this is a request from within cy.origin + // TODO: think about moving this logic back into the frontend so that the driver can be in control + // of when to buffer and set the remote state + if (isOk && details.isHtml && + !((options.hasAlreadyVisitedUrl || options.isCrossOrigin) && !cors.urlOriginsMatch(previousRemoteState.origin, newUrl))) { // if we're not handling a local file set the remote state if (!handlingLocalFile) { this.remoteStates.set(newUrl as string, options) @@ -321,6 +322,8 @@ export class ServerE2E extends ServerBase { restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary) } + details.isPrimaryOrigin = this.remoteStates.isPrimaryOrigin(newUrl!) + return resolve(details) }) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 5e5c563d2395..96a14a6b3a98 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -414,8 +414,10 @@ export class SocketBase { return } - case 'ready:for:origin': - return this.localBus.emit('ready:for:origin', args[0]) + case 'cross:origin:bridge:ready': + return this.localBus.emit('cross:origin:bridge:ready', args[0]) + case 'cross:origin:release:html': + return this.localBus.emit('cross:origin:release:html') case 'cross:origin:finished': return this.localBus.emit('cross:origin:finished', args[0]) default: diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 047656c7f88e..d1ef6e02e7e3 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -3028,7 +3028,7 @@ describe('Routes', () => { }) this.server._eventBus.on('cross:origin:delaying:html', () => { - this.server._eventBus.emit('ready:for:origin', { originPolicy: 'http://foobar.com' }) + this.server._eventBus.emit('cross:origin:release:html') }) return this.rp({ diff --git a/packages/server/test/integration/server_spec.js b/packages/server/test/integration/server_spec.js index d51fc8537bf6..b6f9196349fc 100644 --- a/packages/server/test/integration/server_spec.js +++ b/packages/server/test/integration/server_spec.js @@ -145,6 +145,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -176,6 +177,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: false, contentType: 'application/json', url: 'http://localhost:2000/assets/foo.json', @@ -196,6 +198,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -213,6 +216,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -244,6 +248,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/sub/', @@ -276,6 +281,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: false, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/does-not-exist', @@ -303,6 +309,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -384,6 +391,7 @@ describe('Server', () => { }).then((obj) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: `http://localhost:${this.httpPort}/${path}/100`, @@ -430,6 +438,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://getbootstrap.com/', @@ -464,6 +473,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: false, isHtml: false, contentType: 'application/json', url: 'http://getbootstrap.com/user.json', @@ -508,6 +518,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: undefined, url: 'http://example.com/', @@ -529,6 +540,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: false, isHtml: false, contentType: undefined, url: 'http://example.com/', @@ -562,6 +574,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://espn.go.com/', @@ -625,6 +638,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://espn.go.com/', @@ -644,6 +658,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://espn.go.com/', @@ -698,6 +713,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: false, + isPrimaryOrigin: false, isHtml: false, contentType: undefined, url: 'http://espn.com/', @@ -712,6 +728,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://espn.go.com/', @@ -747,6 +764,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: false, + isPrimaryOrigin: false, isHtml: true, contentType: 'text/html', url: 'http://mlb.mlb.com/', @@ -780,6 +798,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://getbootstrap.com/', @@ -820,6 +839,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://google.com/foo', @@ -876,6 +896,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://google.com/index', @@ -907,7 +928,7 @@ describe('Server', () => { }) context('cross-origin', () => { - it('adds a secondary remote state', function () { + it('adds a remote state and buffers the response when the request is from within cy.origin and the origins match', function () { nock('http://www.cypress.io/') .get('/') .reply(200, 'content', { @@ -923,7 +944,7 @@ describe('Server', () => { fileServer: this.fileServer, }) - this.server.socket.localBus.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + this.server.socket.localBus.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, @@ -944,6 +965,7 @@ describe('Server', () => { .then((obj = {}) => { expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: false, isHtml: true, contentType: 'text/html', url: 'http://www.cypress.io/', @@ -990,34 +1012,54 @@ describe('Server', () => { }) }) - it('doesn\'t override existing remote state on ready:for:origin', function () { - nock('http://www.cypress.io/') + it('adds a remote state and buffers the response when a url has already been visited and the origins match', function () { + nock('http://localhost:3500/') .get('/') .reply(200, 'content', { 'Content-Type': 'text/html', }) - this.server.socket.localBus.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + // this will be the current origin + this.server.remoteStates.set('http://localhost:3500/') + + return this.server._onResolveUrl('http://localhost:3500/', {}, this.automationRequest, { hasAlreadyVisitedUrl: true }) + .then((obj = {}) => { + // Verify the cross origin request was buffered + const buffer = this.buffers.take('http://localhost:3500/') + + expect(buffer).to.not.be.empty - return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest, { isCrossOrigin: true }) - .then(() => { // Verify the secondary remote state is returned expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, props: { - domain: 'cypress', - port: '80', - tld: 'io', + domain: '', + port: '3500', + tld: 'localhost', }, - origin: 'http://www.cypress.io', + origin: 'http://localhost:3500', strategy: 'http', - domainName: 'cypress.io', + domainName: 'localhost', fileServer: null, }) + }) + }) + + it('doesn\'t set a remote state or buffer the response when a url has already been visited and the origins don\'t match', function () { + nock('http://localhost:3500/') + .get('/') + .reply(200, 'content', { + 'Content-Type': 'text/html', + }) - this.server.socket.localBus.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + this.server.remoteStates.set('http://localhost:3500/') - // Verify the existing secondary remote state is not overridden + // this will be the current origin + this.server.socket.localBus.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) + + return this.server._onResolveUrl('http://localhost:3500/', {}, this.automationRequest, { hasAlreadyVisitedUrl: true }) + .then(() => { + // Verify the remote state was not updated expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, props: { @@ -1025,65 +1067,100 @@ describe('Server', () => { port: '80', tld: 'io', }, - origin: 'http://www.cypress.io', + origin: 'http://cypress.io', strategy: 'http', domainName: 'cypress.io', fileServer: null, }) - }) - }) - context('#get()', () => { - it('returns undefined for not found remote state', function () { - this.server.remoteStates.set('http://www.cypress.io/') + // Verify the cross origin request was not buffered + const buffer = this.buffers.take('http://localhost:3500/') - expect(this.server.remoteStates.get('http://notfound.com/')).to.be.undefined + expect(buffer).to.be.empty }) + }) - it('returns primary remote state', function () { - this.server.remoteStates.set('http://www.cypress.io/', { isCrossOrigin: true }) - - expect(this.server.remoteStates.get('http://localhost:2000')).to.deep.eq({ - auth: undefined, - props: null, - origin: 'http://localhost:2000', - strategy: 'file', - domainName: 'localhost', - fileServer: this.fileServer, - }) + it('doesn\'t set a remote state or buffer the response when the request is from within cy.origin and the origins don\'t match', function () { + nock('http://localhost:3500/') + .get('/') + .reply(200, 'content', { + 'Content-Type': 'text/html', }) - it('returns secondary remote state', function () { - this.server.remoteStates.set('http://www.cypress.io/', { isCrossOrigin: true }) + this.server.remoteStates.set('http://localhost:3500/') - expect(this.server.remoteStates.get('http://cypress.io')).to.deep.eq({ + // this will be the current origin + this.server.socket.localBus.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) + + return this.server._onResolveUrl('http://localhost:3500/', {}, this.automationRequest, { isCrossOrigin: true }) + .then(() => { + // Verify the remote state was not updated + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, props: { domain: 'cypress', port: '80', tld: 'io', }, - origin: 'http://www.cypress.io', + origin: 'http://cypress.io', strategy: 'http', domainName: 'cypress.io', fileServer: null, }) + + // Verify the cross origin request was not buffered + const buffer = this.buffers.take('http://localhost:3500/') + + expect(buffer).to.be.empty }) }) - context('#reset()', () => { - it('returns undefined for not found remote state', function () { - this.server.socket.localBus.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + it('doesn\'t override existing remote state on cross:origin:bridge:ready', function () { + nock('http://www.cypress.io/') + .get('/') + .reply(200, 'content', { + 'Content-Type': 'text/html', + }) + + this.server.socket.localBus.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) - expect(this.server.remoteStates.isSecondaryOrigin('http://cypress.io')).to.be.true - expect(this.server.remoteStates.get('http://cypress.io')).to.not.be.undefined + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest, { isCrossOrigin: true, auth: { username: 'u', password: 'p' } }) + .then(() => { + // Verify the secondary remote state is returned + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: { + username: 'u', + password: 'p', + }, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://www.cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) - this.server.remoteStates.reset() + this.server.socket.localBus.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) - expect(this.server.remoteStates.isSecondaryOrigin('http://cypress.io')).to.be.false - expect(this.server.remoteStates.get('http://cypress.io')).to.be.undefined - expect(this.server.remoteStates.isPrimaryOrigin('http://localhost:2000')).to.be.true - expect(this.server.remoteStates.get('http://localhost:2000')).to.not.be.undefined + // Verify the existing secondary remote state is not overridden + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: { + username: 'u', + password: 'p', + }, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://www.cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) }) }) }) @@ -1113,6 +1190,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -1133,6 +1211,7 @@ describe('Server', () => { }).then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://www.google.com/', @@ -1165,6 +1244,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -1208,6 +1288,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://www.google.com/', @@ -1245,6 +1326,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -1279,6 +1361,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://www.google.com/', @@ -1322,6 +1405,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'https://www.foobar.com:8443/', @@ -1359,6 +1443,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -1393,6 +1478,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'https://www.foobar.com:8443/', @@ -1436,6 +1522,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: s3StaticHtmlUrl, @@ -1481,6 +1568,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: 'http://localhost:2000/index.html', @@ -1515,6 +1603,7 @@ describe('Server', () => { .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, + isPrimaryOrigin: true, isHtml: true, contentType: 'text/html', url: s3StaticHtmlUrl, diff --git a/packages/server/test/unit/remote_states.spec.ts b/packages/server/test/unit/remote_states.spec.ts index 021f74c37d7c..6ad429044c4d 100644 --- a/packages/server/test/unit/remote_states.spec.ts +++ b/packages/server/test/unit/remote_states.spec.ts @@ -94,21 +94,21 @@ describe('remote states', () => { context('#isSecondaryOrigin', () => { it('returns true when the requested url is a secondary origin', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin('https://staging.google.com') expect(isSecondaryOrigin).to.be.true }) it('returns false when the requested url is the primary origin', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin('http://localhost:3500') expect(isSecondaryOrigin).to.be.false }) it('returns false when the requested url is not in the origin stack', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin('https://foobar.com') expect(isSecondaryOrigin).to.be.false @@ -123,7 +123,7 @@ describe('remote states', () => { }) it('returns false when the requested url is not the primary origin', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) const isPrimaryOrigin = this.remoteStates.isPrimaryOrigin('http://google.com') expect(isPrimaryOrigin).to.be.false @@ -132,7 +132,7 @@ describe('remote states', () => { context('#removeCurrentOrigin', () => { it('removes the current origin from the stack', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.true this.remoteStates.removeCurrentOrigin('https://google.com') @@ -141,7 +141,7 @@ describe('remote states', () => { }) it('throws an error when trying to remove the incorrect origin', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.true expect(() => this.remoteStates.removeCurrentOrigin('http://notfound.com')) @@ -151,9 +151,10 @@ describe('remote states', () => { context('#reset', () => { it('resets the origin stack and remote states to the primary', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.true + expect(this.remoteStates.get('https://google.com')).to.not.be.undefined this.remoteStates.reset() @@ -164,7 +165,7 @@ describe('remote states', () => { context('#current', () => { it('returns the remote state for the current origin in the stack', function () { - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'https://google.com' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'https://google.com' }) this.remoteStates.set('https://staging.google.com/foo/bar', { isCrossOrigin: true }) const state = this.remoteStates.current() @@ -203,6 +204,8 @@ describe('remote states', () => { }, }) + expect(this.remoteStates.get('https://staging.google.com')).to.deep.equal(state) + expect(this.remoteStates.isPrimaryOrigin('https://staging.google.com')).to.be.true }) @@ -226,6 +229,8 @@ describe('remote states', () => { }, }) + expect(this.remoteStates.get('https://staging.google.com')).to.deep.equal(state) + expect(this.remoteStates.isPrimaryOrigin('http://localhost:3500')).to.be.true expect(this.remoteStates.isPrimaryOrigin('https://staging.google.com')).to.be.false }) @@ -366,36 +371,24 @@ describe('remote states', () => { }) context('events', () => { - it('can add a secondary remote state on ready:for:origin', function () { + it('can add a secondary remote state on cross:origin:bridge:ready', function () { let currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://localhost:3500') - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://cypress.io') expect(this.remoteStates.isSecondaryOrigin(currentState.origin)).to.be.true }) - it('doesn\'t do anything if ready:for:origin failed', function () { - let currentState = this.remoteStates.current() - - expect(currentState.origin).to.equal('http://localhost:3500') - - this.eventEmitter.emit('ready:for:origin', { failed: true }) - - currentState = this.remoteStates.current() - expect(currentState.origin).to.equal('http://localhost:3500') - expect(this.remoteStates.isSecondaryOrigin(currentState.origin)).to.be.false - }) - it('removes the current origin when cross:origin:finished is received', function () { let currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://localhost:3500') - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://cypress.io') @@ -406,22 +399,22 @@ describe('remote states', () => { expect(currentState.origin).to.equal('http://localhost:3500') }) - it('doesn\'t override an existing secondary remote state on ready:for:origin', function () { + it('doesn\'t override an existing secondary remote state on cross:origin:bridge:ready', function () { let currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://localhost:3500') - // simulate a cy.origin by calling ready:for:origin followed by setting + // simulate a cy.origin by calling cross:origin:bridge:ready followed by setting // the origin with specific auth options and finally calling cross:origin:finished - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) this.remoteStates.set('http://cypress.io', { auth: { username: 'u', password: 'p' }, isCrossOrigin: true }) currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://cypress.io') expect(currentState.auth).to.deep.equal({ username: 'u', password: 'p' }) this.eventEmitter.emit('cross:origin:finished', 'http://cypress.io') - // verify calling ready:for:origin doesn't reset the previous state - this.eventEmitter.emit('ready:for:origin', { originPolicy: 'http://cypress.io' }) + // verify calling cross:origin:bridge:ready doesn't reset the previous state + this.eventEmitter.emit('cross:origin:bridge:ready', { originPolicy: 'http://cypress.io' }) currentState = this.remoteStates.current() expect(currentState.origin).to.equal('http://cypress.io') diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index 8a4d659bfaa0..93e4666beca2 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -563,16 +563,25 @@ describe('lib/socket', () => { }) }) - context('on(ready:for:origin)', () => { - it('emits ready:for:origin on local bus', function (done) { - this.server.socket.localBus.once('ready:for:origin', ({ originPolicy, failed }) => { + context('on(cross:origin:bridge:ready)', () => { + it('emits cross:origin:bridge:ready on local bus', function (done) { + this.server.socket.localBus.once('cross:origin:bridge:ready', ({ originPolicy }) => { expect(originPolicy).to.equal('http://foobar.com') - expect(failed).to.be.false done() }) - this.client.emit('backend:request', 'ready:for:origin', { originPolicy: 'http://foobar.com', failed: false }, () => {}) + this.client.emit('backend:request', 'cross:origin:bridge:ready', { originPolicy: 'http://foobar.com' }, () => {}) + }) + }) + + context('on(cross:origin:release:html)', () => { + it('emits cross:origin:release:html on local bus', function (done) { + this.server.socket.localBus.once('cross:origin:release:html', () => { + done() + }) + + this.client.emit('backend:request', 'cross:origin:release:html', () => {}) }) }) @@ -585,7 +594,7 @@ describe('lib/socket', () => { }) // add the origin before calling cross:origin:finished (otherwise we'll fail trying to remove the origin) - this.client.emit('backend:request', 'ready:for:origin', { originPolicy: 'http://foobar.com' }, () => {}) + this.client.emit('backend:request', 'cross:origin:bridge:ready', { originPolicy: 'http://foobar.com' }, () => {}) this.client.emit('backend:request', 'cross:origin:finished', 'http://foobar.com', () => {}) }) diff --git a/system-tests/__snapshots__/runnable_execution_spec.ts.js b/system-tests/__snapshots__/runnable_execution_spec.ts.js index d8e37324a4fd..0d6074bf3faf 100644 --- a/system-tests/__snapshots__/runnable_execution_spec.ts.js +++ b/system-tests/__snapshots__/runnable_execution_spec.ts.js @@ -38,11 +38,11 @@ exports['e2e runnable execution / cannot navigate in before hook and test'] = ` In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`: \`cy.visit('http://localhost:4545/')\` -\`\` +\`\` \`cy.origin('http://localhost:5656', () => {\` \` cy.visit('http://localhost:5656/')\` -\` \` +\` \` \`})\` The new URL is considered a different origin because the following parts of the URL are different: @@ -69,11 +69,11 @@ https://on.cypress.io/cannot-visit-different-origin-domain In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`: \`cy.visit('http://localhost:4545/')\` -\`\` +\`\` \`cy.origin('http://localhost:5656', () => {\` \` cy.visit('http://localhost:5656/')\` -\` \` +\` \` \`})\` The new URL is considered a different origin because the following parts of the URL are different: From 81f8a85821cdb697d7877d1416c50242d7628bff Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Thu, 21 Apr 2022 14:49:16 +0900 Subject: [PATCH 109/177] Fix typo. --- packages/driver/src/cy/chai.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/chai.ts b/packages/driver/src/cy/chai.ts index 2cc8399e5fef..49fdd466e851 100644 --- a/packages/driver/src/cy/chai.ts +++ b/packages/driver/src/cy/chai.ts @@ -31,7 +31,7 @@ const whitespace = /\s/g const valueHasLeadingOrTrailingWhitespaces = /\*\*'\s+|\s+'\*\*/g const imageMarkdown = /!\[.*?\]\(.*?\)/g const doubleslashRe = /\\\\/g -const escapedDoubleslashRe = /__doulbe_slash__/g +const escapedDoubleslashRe = /__double_slash__/g type CreateFunc = ((specWindow, state, assertFn) => ({ chai: Chai.ChaiStatic @@ -106,7 +106,7 @@ chai.use((chai, u) => { return }) - const escapeDoubleSlash = (str: string) => str.replace(doubleslashRe, '__doulbe_slash__') + const escapeDoubleSlash = (str: string) => str.replace(doubleslashRe, '__double_slash__') const restoreDoubleSlash = (str: string) => str.replace(escapedDoubleslashRe, '\\\\') // remove any single quotes between our **, From 55e7f8a2310eded9ebbdf363b0be6d19c3d3df30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 03:25:54 -0500 Subject: [PATCH 110/177] chore: Update Chrome (beta) to 101.0.4951.41 (#21161) Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> --- browser-versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-versions.json b/browser-versions.json index 7cda3cd22949..f304171fae27 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { - "chrome:beta": "101.0.4951.34", + "chrome:beta": "101.0.4951.41", "chrome:stable": "100.0.4896.127" } From e3161a0b37c5895b060c9a2883906409395f59b5 Mon Sep 17 00:00:00 2001 From: Matt Henkes Date: Thu, 21 Apr 2022 09:32:47 -0500 Subject: [PATCH 111/177] chore: [Multi-domain] Break out separate CI tasks to test the driver with experimentalSessionAndOrigin on (#21148) * Attempt to run tests without the feature flag and the multi-domain folder * do it for real this time dummy * trying circleci changes * valid file??? * moar changes! * i am a master of bash scripting * try another exclude pattern * one more time with feeling * lets do it again * maybe this? * different parallel group * lets try this * updating tests phase 1 * updating tests phase 2 * Apply suggestions from code review Co-authored-by: Matt Schile * Update tests * Run more tests * Re-locate system test * Flex message for firefox Co-authored-by: Matt Schile --- circle.yml | 77 ++++++++++++- packages/driver/cypress.json | 3 +- .../integration/commands/navigation_spec.js | 104 ++++++++++++------ .../e2e/multi-domain/stability_spec.ts | 34 ++++++ .../cypress/integration/e2e/stability_spec.js | 32 ------ packages/driver/package.json | 4 +- .../__snapshots__/navigation_spec.ts.js | 95 ---------------- .../navigation_cross_origin_errors.ts | 10 -- system-tests/test/navigation_spec.ts | 48 -------- 9 files changed, 183 insertions(+), 224 deletions(-) create mode 100644 packages/driver/cypress/integration/e2e/multi-domain/stability_spec.ts delete mode 100644 packages/driver/cypress/integration/e2e/stability_spec.js delete mode 100644 system-tests/__snapshots__/navigation_spec.ts.js delete mode 100644 system-tests/projects/e2e/cypress/integration/navigation_cross_origin_errors.ts delete mode 100644 system-tests/test/navigation_spec.ts diff --git a/circle.yml b/circle.yml index 24c0fb2c3f9b..49a982791e36 100644 --- a/circle.yml +++ b/circle.yml @@ -406,6 +406,10 @@ commands: description: chrome channel to install type: string default: '' + experimentalSessionAndOrigin: + description: experimental flag to apply + type: boolean + default: false steps: - restore_cached_workspace - when: @@ -423,8 +427,13 @@ commands: if [[ -v MAIN_RECORD_KEY ]]; then # internal PR - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - yarn cypress:run --record --parallel --group 5x-driver-<> --browser <> + if <>; then + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + yarn cypress:run-experimentalSessionAndOrigin --record --parallel --group 5x-driver-<>-experimentalSessionAndOrigin --browser <> + else + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + yarn cypress:run --record --parallel --group 5x-driver-<> --browser <> + fi else # external PR TESTFILES=$(circleci tests glob "cypress/integration/**/*spec.*" | circleci tests split --total=$CIRCLE_NODE_TOTAL) @@ -433,7 +442,11 @@ commands: if [[ -z "$TESTFILES" ]]; then echo "Empty list of test files" fi - yarn cypress:run --browser <> --spec $TESTFILES + if <>; then + yarn cypress:run-experimentalSessionAndOrigin --browser <> --spec $TESTFILES + else + yarn cypress:run --browser <> --spec $TESTFILES + fi fi working_directory: packages/driver - verify-mocha-results @@ -1294,6 +1307,44 @@ jobs: - run-driver-integration-tests: browser: electron + driver-integration-tests-chrome-experimentalSessionAndOrigin: + <<: *defaults + resource_class: medium + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: chrome + install-chrome-channel: stable + experimentalSessionAndOrigin: true + + driver-integration-tests-chrome-beta-experimentalSessionAndOrigin: + <<: *defaults + resource_class: medium + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: chrome:beta + install-chrome-channel: beta + experimentalSessionAndOrigin: true + + driver-integration-tests-firefox-experimentalSessionAndOrigin: + <<: *defaults + resource_class: medium + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: firefox + experimentalSessionAndOrigin: true + + driver-integration-tests-electron-experimentalSessionAndOrigin: + <<: *defaults + resource_class: medium + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: electron + experimentalSessionAndOrigin: true + desktop-gui-integration-tests-7x: <<: *defaults parallelism: 7 @@ -2084,6 +2135,22 @@ linux-workflow: &linux-workflow context: test-runner:cypress-record-key requires: - build + - driver-integration-tests-chrome-experimentalSessionAndOrigin: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-firefox-experimentalSessionAndOrigin: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-electron-experimentalSessionAndOrigin: + context: test-runner:cypress-record-key + requires: + - build - runner-integration-tests-chrome: context: [test-runner:cypress-record-key, test-runner:percy] requires: @@ -2180,6 +2247,10 @@ linux-workflow: &linux-workflow - driver-integration-tests-chrome - driver-integration-tests-chrome-beta - driver-integration-tests-electron + - driver-integration-tests-firefox-experimentalSessionAndOrigin + - driver-integration-tests-chrome-experimentalSessionAndOrigin + - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin + - driver-integration-tests-electron-experimentalSessionAndOrigin - system-tests-non-root - system-tests-firefox - system-tests-electron diff --git a/packages/driver/cypress.json b/packages/driver/cypress.json index 911238c1181a..f6d381383ca1 100644 --- a/packages/driver/cypress.json +++ b/packages/driver/cypress.json @@ -13,6 +13,5 @@ "retries": { "runMode": 2, "openMode": 0 - }, - "experimentalSessionAndOrigin": true + } } diff --git a/packages/driver/cypress/integration/commands/navigation_spec.js b/packages/driver/cypress/integration/commands/navigation_spec.js index b6b4f0155e3b..cf3a9b5c94ef 100644 --- a/packages/driver/cypress/integration/commands/navigation_spec.js +++ b/packages/driver/cypress/integration/commands/navigation_spec.js @@ -1409,10 +1409,11 @@ describe('src/cy/commands/navigation', () => { it('throws when attempting to visit a 2nd domain on different port', function (done) { cy.on('fail', (err) => { const { lastLog } = this + const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n` expect(err.message).to.equal(stripIndent`\ \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n - You likely forgot to use \`cy.origin()\`:\n + ${experimentalMessage} \`cy.visit('http://localhost:3500/fixtures/generic.html')\` \`\`\n \`cy.origin('http://localhost:3501', () => {\` @@ -1441,10 +1442,11 @@ describe('src/cy/commands/navigation', () => { it('throws when attempting to visit a 2nd domain on different protocol', function (done) { cy.on('fail', (err) => { const { lastLog } = this + const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n` expect(err.message).to.equal(stripIndent`\ \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n - You likely forgot to use \`cy.origin()\`:\n + ${experimentalMessage} \`cy.visit('http://localhost:3500/fixtures/generic.html')\` \`\`\n \`cy.origin('https://localhost:3502', () => {\` @@ -1473,10 +1475,11 @@ describe('src/cy/commands/navigation', () => { it('throws when attempting to visit a 2nd domain on different superdomain', function (done) { cy.on('fail', (err) => { const { lastLog } = this + const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n` expect(err.message).to.equal(stripIndent`\ \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n - You likely forgot to use \`cy.origin()\`:\n + ${experimentalMessage} \`cy.visit('http://localhost:3500/fixtures/generic.html')\` \`\`\n \`cy.origin('http://foobar.com:3500', () => {\` @@ -1505,10 +1508,11 @@ describe('src/cy/commands/navigation', () => { it('throws attempting to visit 2 unique ip addresses', function (done) { cy.on('fail', (err) => { const { lastLog } = this + const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n` expect(err.message).to.equal(stripIndent`\ \`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n - You likely forgot to use \`cy.origin()\`:\n + ${experimentalMessage} \`cy.visit('http://127.0.0.1:3500/fixtures/generic.html')\` \`\`\n \`cy.origin('http://0.0.0.0:3500', () => {\` @@ -2145,6 +2149,53 @@ describe('src/cy/commands/navigation', () => { .get('#does-not-exist', { timeout: 200 }).should('have.class', 'foo') }) + it('displays cross origin failures when navigating to a cross origin', { pageLoadTimeout: 3000 }, function (done) { + cy.on('fail', (err) => { + const { lastLog } = this + + if (Cypress.config('experimentalSessionAndOrigin')) { + // When the experimentalSessionAndOrigin feature is enabled, we will timeout and display this message. + expect(err.message).to.equal(stripIndent`\ + Timed out after waiting \`3000ms\` for your remote page to load on origin(s):\n + - \`http://localhost:3500\`\n + A cross-origin request for \`http://www.foobar.com:3500/fixtures/multi-domain-secondary.html\` was detected.\n + A command that triggers cross-origin navigation must be immediately followed by a \`cy.origin()\` command:\n + \`cy.origin(\'http://foobar.com:3500\', () => {\` + \` \` + \`})\`\n + If the cross-origin request was an intermediary state, you can try increasing the \`pageLoadTimeout\` value in \`cypress.json\` to wait longer.\n + Browsers will not fire the \`load\` event until all stylesheets and scripts are done downloading.\n + When this \`load\` event occurs, Cypress will continue running commands.`) + + expect(err.docsUrl).to.eq('https://on.cypress.io/origin') + } else { + const error = Cypress.isBrowser('firefox') ? 'Permission denied to access property "document" on cross-origin object' : 'Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.' + + // When the experimentalSessionAndOrigin feature is disabled, we will immediately and display this message. + expect(err.message).to.equal(stripIndent`\ + Cypress detected a cross origin error happened on page load:\n + > ${error}\n + Before the page load, you were bound to the origin policy:\n + > http://localhost:3500\n + A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.\n + A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different.\n + Cypress does not allow you to navigate to a different origin URL within a single test.\n + You may need to restructure some of your test code to avoid this problem.\n + Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in \`cypress.json\`.`) + + expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation') + } + + assertLogLength(this.logs, 6) + expect(lastLog.get('error')).to.eq(err) + + done() + }) + + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="cross-origin-secondary-link"]').click() + }) + return null }) }) @@ -2269,41 +2320,28 @@ describe('src/cy/commands/navigation', () => { }) }) - it('waits for stability at the end of the command queue when not stable', { experimentalSessionAndOrigin: false }, (done) => { + it('tests waiting on stability at the end of the command queue', (done) => { cy .visit('/fixtures/generic.html') .then((win) => { - cy.on('window:load', () => { + // We do not wait if the experimentalSessionAndOrigin feature is enabled + if (Cypress.config('experimentalSessionAndOrigin')) { + const onLoad = cy.spy() + + cy.on('window:load', onLoad) + cy.on('command:queue:end', () => { + expect(onLoad).not.have.been.called done() }) - }) - - cy.on('command:queue:before:end', () => { - // force us to become unstable immediately - // else the beforeunload event fires at the end - // of the tick which is too late - cy.isStable(false, 'testing') - - win.location.href = '/timeout?ms=100' - }) - - return null - }) - }) - - it('does not wait for stability at the end of the command queue when not stable with experimentalSessionAndOrigin', (done) => { - const onLoad = cy.spy() - - cy - .visit('/fixtures/generic.html') - .then((win) => { - cy.on('window:load', onLoad) - - cy.on('command:queue:end', () => { - expect(onLoad).not.have.been.called - done() - }) + } else { + // We do wait if the experimentalSessionAndOrigin feature is not enabled + cy.on('window:load', () => { + cy.on('command:queue:end', () => { + done() + }) + }) + } cy.on('command:queue:before:end', () => { // force us to become unstable immediately diff --git a/packages/driver/cypress/integration/e2e/multi-domain/stability_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/stability_spec.ts new file mode 100644 index 000000000000..26486e41c651 --- /dev/null +++ b/packages/driver/cypress/integration/e2e/multi-domain/stability_spec.ts @@ -0,0 +1,34 @@ +describe('stability', () => { + describe('before each transitions', () => { + describe('transitioning from a before block to an it block while unstable', () => { + beforeEach(() => { + cy.visit('/fixtures/auth/index.html') + cy.window().then((win) => { + win.location.href = 'http://localhost:3500/timeout?ms=1000' + }) + }) + + it('fails if the page does not load within the page load timeout', { defaultCommandTimeout: 50, pageLoadTimeout: 500 }, (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include(`Timed out after waiting \`500ms\` for your remote page to load.`) + done() + }) + + cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com + }) + + it('waits for the page to load before running the command', { defaultCommandTimeout: 50 }, () => { + cy.get('body').invoke('text').should('equal', 'timeout') + }) + + it('will retry and fail the command after the page loads', { defaultCommandTimeout: 50 }, (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include(`Timed out retrying after 50ms: expected 'timeout' to equal 'not timeout'`) + done() + }) + + cy.get('body').invoke('text').should('equal', 'not timeout') + }) + }) + }) +}) diff --git a/packages/driver/cypress/integration/e2e/stability_spec.js b/packages/driver/cypress/integration/e2e/stability_spec.js deleted file mode 100644 index b6d541845f79..000000000000 --- a/packages/driver/cypress/integration/e2e/stability_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -describe('stability', () => { - describe('transitioning from a before block to an it block while unstable', () => { - beforeEach(() => { - cy.visit('/fixtures/auth/index.html') - cy.window().then((win) => { - win.location.href = 'http://localhost:3500/timeout?ms=1000' - }) - }) - - it('fails if the page does not load within the page load timeout', { defaultCommandTimeout: 50, pageLoadTimeout: 500 }, (done) => { - cy.on('fail', (err) => { - expect(err.message).to.include(`Timed out after waiting \`500ms\` for your remote page to load.`) - done() - }) - - cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com - }) - - it('waits for the page to load before running the command', { defaultCommandTimeout: 50 }, () => { - cy.get('body').invoke('text').should('equal', 'timeout') - }) - - it('will retry and fail the command after the page loads', { defaultCommandTimeout: 50 }, (done) => { - cy.on('fail', (err) => { - expect(err.message).to.include(`Timed out retrying after 50ms: expected 'timeout' to equal 'not timeout'`) - done() - }) - - cy.get('body').invoke('text').should('equal', 'not timeout') - }) - }) -}) diff --git a/packages/driver/package.json b/packages/driver/package.json index aca090231a17..6641bc4c0d00 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -5,7 +5,9 @@ "scripts": { "clean-deps": "rm -rf node_modules", "cypress:open": "node ../../scripts/cypress open", - "cypress:run": "node ../../scripts/cypress run", + "cypress:run": "node ../../scripts/cypress run --spec \"cypress/integration/*/*\",\"cypress/integration/*/!(multi-domain)/**/*\"", + "cypress:open-experimentalSessionAndOrigin": "node ../../scripts/cypress open --config experimentalSessionAndOrigin=true", + "cypress:run-experimentalSessionAndOrigin": "node ../../scripts/cypress run --config experimentalSessionAndOrigin=true", "postinstall": "patch-package", "start": "node -e 'console.log(require(`chalk`).red(`\nError:\n\tRunning \\`yarn start\\` is no longer needed for driver/cypress tests.\n\tWe now automatically spawn the server in the pluginsFile.\n\tChanges to the server will be watched and reloaded automatically.`))'" }, diff --git a/system-tests/__snapshots__/navigation_spec.ts.js b/system-tests/__snapshots__/navigation_spec.ts.js deleted file mode 100644 index 38df39d3a9c0..000000000000 --- a/system-tests/__snapshots__/navigation_spec.ts.js +++ /dev/null @@ -1,95 +0,0 @@ -exports['e2e cross origin navigation / captures cross origin failures when "experimentalSessionAndOrigin" config value is falsy'] = ` - -==================================================================================================== - - (Run Starting) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Cypress: 1.2.3 │ - │ Browser: FooBrowser 88 │ - │ Specs: 1 found (navigation_cross_origin_errors.ts) │ - │ Searched: cypress/integration/navigation_cross_origin_errors.ts │ - │ Experiments: experimentalSessionAndOrigin=false │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - -──────────────────────────────────────────────────────────────────────────────────────────────────── - - Running: navigation_cross_origin_errors.ts (1 of 1) - - - navigation cross origin errors - 1) displays cross origin failures when "experimentalSessionAndOrigin" is turned off - - - 0 passing - 1 failing - - 1) navigation cross origin errors - displays cross origin failures when "experimentalSessionAndOrigin" is turned off: - CypressError: Cypress detected a cross origin error happened on page load: - - > [Cross origin error message] - -Before the page load, you were bound to the origin policy: - - > http://localhost:13370 - -A cross origin error happens when your application navigates to a new URL which does not match the origin policy above. - -A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different. - -Cypress does not allow you to navigate to a different origin URL within a single test. - -You may need to restructure some of your test code to avoid this problem. - -Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in \`cypress.json\`. - -https://on.cypress.io/cross-origin-violation - [stack trace lines] - - - - - (Results) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 1 │ - │ Passing: 0 │ - │ Failing: 1 │ - │ Pending: 0 │ - │ Skipped: 0 │ - │ Screenshots: 1 │ - │ Video: true │ - │ Duration: X seconds │ - │ Spec Ran: navigation_cross_origin_errors.ts │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - - (Screenshots) - - - /XXX/XXX/XXX/cypress/screenshots/navigation_cross_origin_errors.ts/navigation cr (1280x720) - oss origin errors -- displays cross origin failures when experimentalSessionAndO - rigin is turned off (failed).png - - - (Video) - - - Started processing: Compressing to 32 CRF - - Finished processing: /XXX/XXX/XXX/cypress/videos/navigation_cross_origin_errors. (X second) - ts.mp4 - - -==================================================================================================== - - (Run Finished) - - - Spec Tests Passing Failing Pending Skipped - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✖ navigation_cross_origin_errors.ts XX:XX 1 - 1 - - │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✖ 1 of 1 failed (100%) XX:XX 1 - 1 - - - - -` diff --git a/system-tests/projects/e2e/cypress/integration/navigation_cross_origin_errors.ts b/system-tests/projects/e2e/cypress/integration/navigation_cross_origin_errors.ts deleted file mode 100644 index 7e1654adc89a..000000000000 --- a/system-tests/projects/e2e/cypress/integration/navigation_cross_origin_errors.ts +++ /dev/null @@ -1,10 +0,0 @@ -describe('navigation cross origin errors', () => { - it('displays cross origin failures when "experimentalSessionAndOrigin" is turned off', function () { - // @ts-ignore - cy.visit('/jquery.html').window().then((win) => { - const constructedCrossOriginAnchor = win.$(`cross origin`).appendTo(win.document.body) - - constructedCrossOriginAnchor.get(0).click() - }) - }) -}) diff --git a/system-tests/test/navigation_spec.ts b/system-tests/test/navigation_spec.ts deleted file mode 100644 index b4f83ade06e0..000000000000 --- a/system-tests/test/navigation_spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import systemTests, { expect } from '../lib/system-tests' - -const PORT = 13370 -const onServer = function (app) { - app.get('/cross_origin.html', (req, res) => { - res.send('

cross origin

') - }) -} - -describe('e2e cross origin navigation', () => { - systemTests.setup({ - servers: [{ - port: 4466, - onServer, - }], - settings: { - hosts: { - '*.foobar.com': '127.0.0.1', - }, - }, - }) - - // TODO: convert to cypress-in-cypress test if possible - // https://github.com/cypress-io/cypress/issues/20973 - systemTests.it('captures cross origin failures when "experimentalSessionAndOrigin" config value is falsy', { - // keep the port the same to prevent issues with the snapshot - port: PORT, - spec: 'navigation_cross_origin_errors.ts', - browser: ['chrome', 'electron'], - snapshot: true, - expectedExitCode: 1, - config: { - experimentalSessionAndOrigin: false, - }, - async onRun (exec) { - const res = await exec() - - expect(res.stdout).to.contain('Cypress detected a cross origin error happened on page load') - expect(res.stdout).to.contain('Before the page load, you were bound to the origin policy:') - expect(res.stdout).to.contain('A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.') - expect(res.stdout).to.contain('A new URL does not match the origin policy if the \'protocol\', \'port\' (if specified), and/or \'host\' (unless of the same superdomain) are different.') - expect(res.stdout).to.contain('Cypress does not allow you to navigate to a different origin URL within a single test.') - expect(res.stdout).to.contain('You may need to restructure some of your test code to avoid this problem.') - expect(res.stdout).to.contain('Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in `cypress.json`.') - expect(res.stdout).to.contain('https://on.cypress.io/cross-origin-violation') - }, - }) -}) From 342bc06ae3f7d2bb3b6eca28183d30391330f16a Mon Sep 17 00:00:00 2001 From: Ahmed Tarek Date: Thu, 21 Apr 2022 22:54:50 +0200 Subject: [PATCH 112/177] fix: prevObject types (#21106) Co-authored-by: Emily Rohrbough Co-authored-by: Rachel Co-authored-by: Tyler Biethman --- cli/types/cypress.d.ts | 11 ++++++++--- cli/types/tests/cypress-tests.ts | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index bfb7c84822fd..2e576727860b 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -10,10 +10,13 @@ declare namespace Cypress { type PrevSubject = keyof PrevSubjectMap type TestingType = 'e2e' | 'component' type PluginConfig = (on: PluginEvents, config: PluginConfigOptions) => void | ConfigOptions | Promise + interface JQueryWithSelector extends JQuery { + selector?: string | null + } interface PrevSubjectMap { optional: O - element: JQuery + element: JQueryWithSelector document: Document window: Window } @@ -467,16 +470,18 @@ declare namespace Cypress { Commands: { add(name: T, fn: CommandFn): void add(name: T, options: CommandOptions & {prevSubject: false}, fn: CommandFn): void + add(name: T, options: CommandOptions & {prevSubject: true}, fn: CommandFnWithSubject): void add( - name: T, options: CommandOptions & { prevSubject: true | S | ['optional'] }, fn: CommandFnWithSubject, + name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject, ): void add( name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject[S]>, ): void addAll(fns: CommandFns): void addAll(options: CommandOptions & {prevSubject: false}, fns: CommandFns): void + addAll(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject): void addAll( - options: CommandOptions & { prevSubject: true | S | ['optional'] }, fns: CommandFnsWithSubject, + options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject, ): void addAll( options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject[S]>, diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index eecb36f65909..d1992f829f6e 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -83,7 +83,7 @@ namespace CypressCommandsTests { arg // $ExpectType string }) Cypress.Commands.add('newCommand', { prevSubject: true }, (subject, arg) => { - subject // $ExpectType unknown + subject // $ExpectType any arg // $ExpectType string return }) @@ -113,11 +113,11 @@ namespace CypressCommandsTests { arg // $ExpectType string }) Cypress.Commands.add('newCommand', { prevSubject: 'element' }, (subject, arg) => { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector arg // $ExpectType string }) Cypress.Commands.add('newCommand', { prevSubject: ['element'] }, (subject, arg) => { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector arg // $ExpectType string }) Cypress.Commands.add('newCommand', { prevSubject: ['element', 'document', 'window'] }, (subject, arg) => { @@ -126,7 +126,7 @@ namespace CypressCommandsTests { } else if (subject instanceof Document) { subject // $ExpectType Document } else { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector } arg // $ExpectType string }) @@ -136,7 +136,7 @@ namespace CypressCommandsTests { } else if (subject instanceof Document) { subject // $ExpectType Document } else if (subject) { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector } else { subject // $ExpectType void } @@ -173,7 +173,7 @@ namespace CypressCommandsTests { }) Cypress.Commands.addAll({ prevSubject: true }, { newCommand: (subject, arg) => { - subject // $ExpectType unknown + subject // $ExpectType any arg // $ExpectType any return }, @@ -215,13 +215,13 @@ namespace CypressCommandsTests { }) Cypress.Commands.addAll({ prevSubject: 'element' }, { newCommand: (subject, arg) => { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector arg // $ExpectType any } }) Cypress.Commands.addAll({ prevSubject: ['element'] }, { newCommand: (subject, arg) => { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector arg // $ExpectType any } }) @@ -232,7 +232,7 @@ namespace CypressCommandsTests { } else if (subject instanceof Document) { subject // $ExpectType Document } else { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector } arg // $ExpectType any } @@ -244,7 +244,7 @@ namespace CypressCommandsTests { } else if (subject instanceof Document) { subject // $ExpectType Document } else if (subject) { - subject // $ExpectType JQuery + subject // $ExpectType JQueryWithSelector } else { subject // $ExpectType void } @@ -271,7 +271,7 @@ namespace CypressCommandsTests { originalFn.apply(this, [arg]) // $ExpectType Chainable }) Cypress.Commands.overwrite<'type', 'element'>('type', (originalFn, element, text, options?: Partial) => { - element // $ExpectType JQuery + element // $ExpectType JQueryWithSelector text // $ExpectType string if (options && options.sensitive) { From 885541ea5e736dfd4dac1d3254025b55b367fa3e Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Fri, 22 Apr 2022 09:41:35 -0400 Subject: [PATCH 113/177] improve error messages for unsupported APIs/commands --- .../commands/multi_domain_unsupported_commands.spec.ts | 4 ++-- .../e2e/multi-domain/multi_domain_cypress_api.spec.ts | 2 +- packages/driver/src/cypress/error_messages.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_unsupported_commands.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_unsupported_commands.spec.ts index 5f63679a56e9..b6b862da5d72 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_unsupported_commands.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_unsupported_commands.spec.ts @@ -6,7 +6,7 @@ context('cy.origin unsupported commands', () => { it('cy.route() method is deprecated', (done) => { cy.on('fail', (err) => { - expect(err.message).to.equal('`cy.route()` has been deprecated and use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.') + expect(err.message).to.equal('`cy.route()` has been deprecated and its use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.') expect(err.docsUrl).to.equal('https://on.cypress.io/intercept') done() }) @@ -18,7 +18,7 @@ context('cy.origin unsupported commands', () => { it('cy.server() method is deprecated', (done) => { cy.on('fail', (err) => { - expect(err.message).to.equal('`cy.server()` has been deprecated and use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.') + expect(err.message).to.equal('`cy.server()` has been deprecated and its use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.') expect(err.docsUrl).to.equal('https://on.cypress.io/intercept') done() }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts index 0e67507ea61e..927ec1d5e673 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts @@ -206,7 +206,7 @@ describe('cy.origin Cypress API', () => { context('not supported', () => { it('throws an error when a user attempts to configure Cypress.Server.defaults() inside of cy.origin', (done) => { cy.on('fail', (err) => { - expect(err.message).to.equal('`Cypress.Server.*` has been deprecated and use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.') + expect(err.message).to.equal('`Cypress.Server.*` has been deprecated and its use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.') expect(err.docsUrl).to.equal('https://on.cypress.io/intercept') done() }) diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 937a349e2a17..28adaa64859e 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1205,15 +1205,15 @@ export default { }, unsupported: { route: { - message: `${cmd('route')} has been deprecated and use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, + message: `${cmd('route')} has been deprecated and its use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, docsUrl: 'https://on.cypress.io/intercept', }, server: { - message: `${cmd('server')} has been deprecated and use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, + message: `${cmd('server')} has been deprecated and its use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, docsUrl: 'https://on.cypress.io/intercept', }, Server: { - message: `\`Cypress.Server.*\` has been deprecated and use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, + message: `\`Cypress.Server.*\` has been deprecated and its use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`, docsUrl: 'https://on.cypress.io/intercept', }, Cookies_preserveOnce: { From 0bb655e3795e894610357b205b0d3b3d5bd1c819 Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Fri, 22 Apr 2022 23:28:13 +0900 Subject: [PATCH 114/177] chore: remove command type todos (#20601) Co-authored-by: Zach Bloomquist Co-authored-by: Jennifer Shehane Co-authored-by: Rachel Co-authored-by: Tyler Biethman --- cli/types/cypress.d.ts | 2 +- .../integration/commands/screenshot_spec.js | 6 +- .../driver/src/cy/commands/actions/focus.ts | 30 ++++++--- .../driver/src/cy/commands/actions/scroll.ts | 39 ++++++----- .../driver/src/cy/commands/actions/select.ts | 16 +++-- .../driver/src/cy/commands/actions/submit.ts | 15 +++-- .../driver/src/cy/commands/actions/type.ts | 33 ++++++---- packages/driver/src/cy/commands/angular.ts | 12 ++-- packages/driver/src/cy/commands/clock.ts | 3 +- packages/driver/src/cy/commands/cookies.ts | 64 +++++++++++-------- packages/driver/src/cy/commands/debugging.ts | 27 ++++---- packages/driver/src/cy/commands/exec.ts | 13 ++-- packages/driver/src/cy/commands/files.ts | 24 ++++--- packages/driver/src/cy/commands/location.ts | 23 ++++--- packages/driver/src/cy/commands/misc.ts | 15 +++-- packages/driver/src/cy/commands/navigation.ts | 23 +++---- .../src/cy/commands/querying/focused.ts | 15 +++-- .../src/cy/commands/querying/querying.ts | 40 +++++++----- .../driver/src/cy/commands/querying/root.ts | 16 +++-- packages/driver/src/cy/commands/screenshot.ts | 14 ++-- packages/driver/src/cy/commands/task.ts | 12 ++-- packages/driver/src/cy/commands/window.ts | 53 +++++++-------- packages/driver/src/cy/keyboard.ts | 3 +- packages/driver/src/cy/video-recorder.ts | 2 - packages/driver/src/cypress/browser.ts | 3 +- packages/driver/src/cypress/log.ts | 2 +- .../@types+jquery.scrollto+1.4.29.dev.patch | 58 +++++++++++++++++ 27 files changed, 349 insertions(+), 214 deletions(-) create mode 100644 patches/@types+jquery.scrollto+1.4.29.dev.patch diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 2e576727860b..b76af718c3c5 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -2519,7 +2519,7 @@ declare namespace Cypress { action: 'select' | 'drag-drop' } - interface BlurOptions extends Loggable, Forceable { } + interface BlurOptions extends Loggable, Timeoutable, Forceable { } interface CheckOptions extends Loggable, Timeoutable, ActionableOptions { interval: number diff --git a/packages/driver/cypress/integration/commands/screenshot_spec.js b/packages/driver/cypress/integration/commands/screenshot_spec.js index 0045c2a7b24a..acb4837802e8 100644 --- a/packages/driver/cypress/integration/commands/screenshot_spec.js +++ b/packages/driver/cypress/integration/commands/screenshot_spec.js @@ -761,9 +761,9 @@ describe('src/cy/commands/screenshot', () => { cy.get('.short-element').within(() => { cy.screenshot({ capture: 'runner' }) }).then(() => { - // the runner was captured - expect(Cypress.action.withArgs('cy:before:screenshot').args[0][1].appOnly).to.be.true - expect(Cypress.automation.withArgs('take:screenshot').args[0][1].capture).to.equal('viewport') + // the runner was captured ("appOnly === true" means to hide the runner UI) + expect(Cypress.action.withArgs('cy:before:screenshot').args[0][1].appOnly).to.be.false + expect(Cypress.automation.withArgs('take:screenshot').args[0][1].capture).to.equal('runner') }) }) diff --git a/packages/driver/src/cy/commands/actions/focus.ts b/packages/driver/src/cy/commands/actions/focus.ts index 5f5bc457a762..e866def4c133 100644 --- a/packages/driver/src/cy/commands/actions/focus.ts +++ b/packages/driver/src/cy/commands/actions/focus.ts @@ -4,16 +4,29 @@ import $dom from '../../../dom' import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' import $elements from '../../../dom/elements' +import type { Log } from '../../../cypress/log' + +interface InternalFocusOptions extends Partial { + _log?: Log + $el: JQuery + error: boolean + verify: boolean +} + +interface InternalBlurOptions extends Partial { + _log?: Log + $el: JQuery + $focused: JQuery + error: boolean + verify: boolean +} export default (Commands, Cypress, cy) => { return Commands.addAll({ prevSubject: ['element', 'window'] }, { - // TODO: any -> Partial - focus (subject, options: any = {}) { - const userOptions = options - + focus (subject, userOptions: Partial = {}) { // we should throw errors by default! // but allow them to be silenced - options = _.defaults({}, userOptions, { + const options: InternalFocusOptions = _.defaults({}, userOptions, { $el: subject, error: true, log: true, @@ -85,13 +98,10 @@ export default (Commands, Cypress, cy) => { return verifyAssertions() }, - // TODO: any -> Partial - blur (subject, options: any = {}) { - const userOptions = options - + blur (subject, userOptions: Partial = {}) { // we should throw errors by default! // but allow them to be silenced - options = _.defaults({}, userOptions, { + const options: InternalBlurOptions = _.defaults({}, userOptions, { $el: subject, $focused: cy.getFocused(), error: true, diff --git a/packages/driver/src/cy/commands/actions/scroll.ts b/packages/driver/src/cy/commands/actions/scroll.ts index 48077d99b2eb..f918f0f459a4 100644 --- a/packages/driver/src/cy/commands/actions/scroll.ts +++ b/packages/driver/src/cy/commands/actions/scroll.ts @@ -5,6 +5,7 @@ import Promise from 'bluebird' import $dom from '../../../dom' import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' +import type { Log } from '../../../cypress/log' const findScrollableParent = ($el, win) => { const $parent = $dom.getParent($el) @@ -28,12 +29,26 @@ const isNaNOrInfinity = (item) => { return _.isNaN(num) || !_.isFinite(num) } +interface InternalScrollIntoViewOptions extends Partial { + _log?: Log + $el: JQuery + $parent: any + axis: string + offset?: object +} + +interface InternalScrollToOptions extends Partial { + _log?: Log + $el: any + x: number + y: number + error?: any + axis: string +} + export default (Commands, Cypress, cy, state) => { Commands.addAll({ prevSubject: 'element' }, { - // TODO: any -> Partial - scrollIntoView (subject, options: any = {}) { - const userOptions = options - + scrollIntoView (subject, userOptions: Partial = {}) { if (!_.isObject(userOptions)) { $errUtils.throwErrByPath('scrollIntoView.invalid_argument', { args: { arg: userOptions } }) } @@ -49,7 +64,7 @@ export default (Commands, Cypress, cy, state) => { $errUtils.throwErrByPath('scrollIntoView.multiple_elements', { args: { num: subject.length } }) } - options = _.defaults({}, userOptions, { + const options: InternalScrollIntoViewOptions = _.defaults({}, userOptions, { $el: subject, $parent: state('window'), log: true, @@ -115,9 +130,6 @@ export default (Commands, Cypress, cy, state) => { const scrollIntoView = () => { return new Promise((resolve, reject) => { // scroll our axes - // TODO: done() came from jQuery animate(), specifically, EffectsOptions at misc.d.ts - // The type definition should be fixed at @types/jquery.scrollto. - // @ts-ignore return $(options.$parent).scrollTo(options.$el, { axis: options.axis, easing: options.easing, @@ -157,10 +169,8 @@ export default (Commands, Cypress, cy, state) => { }) Commands.addAll({ prevSubject: ['optional', 'element', 'window'] }, { - // TODO: any -> Partial - scrollTo (subject, xOrPosition, yOrOptions, options: any = {}) { + scrollTo (subject, xOrPosition, yOrOptions, userOptions: Partial = {}) { let x; let y - let userOptions = options // check for undefined or null values if (xOrPosition === undefined || xOrPosition === null) { @@ -261,7 +271,7 @@ export default (Commands, Cypress, cy, state) => { $errUtils.throwErrByPath('scrollTo.multiple_containers', { args: { num: $container.length } }) } - options = _.defaults({}, userOptions, { + const options: InternalScrollToOptions = _.defaults({}, userOptions, { $el: $container, log: true, duration: 0, @@ -361,10 +371,7 @@ export default (Commands, Cypress, cy, state) => { const scrollTo = () => { return new Promise((resolve, reject) => { - // scroll our axis' - // TODO: done() came from jQuery animate(), specifically, EffectsOptions at misc.d.ts - // The type definition should be fixed at @types/jquery.scrollto. - // @ts-ignore + // scroll our axis $(options.$el).scrollTo({ left: x, top: y }, { axis: options.axis, easing: options.easing, diff --git a/packages/driver/src/cy/commands/actions/select.ts b/packages/driver/src/cy/commands/actions/select.ts index 12b3a28ea7c7..e21e2e31d806 100644 --- a/packages/driver/src/cy/commands/actions/select.ts +++ b/packages/driver/src/cy/commands/actions/select.ts @@ -5,13 +5,19 @@ import $dom from '../../../dom' import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' import $elements from '../../../dom/elements' +import type { Log } from '../../../cypress/log' const newLineRe = /\n/g +interface InternalSelectOptions extends Partial { + _log?: Log + $el: JQuery + error?: any +} + export default (Commands, Cypress, cy) => { Commands.addAll({ prevSubject: 'element' }, { - // TODO: any -> Partial - select (subject, valueOrTextOrIndex, options: any = {}) { + select (subject, valueOrTextOrIndex, userOptions: Partial = {}) { if ( !_.isNumber(valueOrTextOrIndex) && !_.isString(valueOrTextOrIndex) @@ -28,9 +34,7 @@ export default (Commands, Cypress, cy) => { $errUtils.throwErrByPath('select.invalid_array_argument', { args: { value: JSON.stringify(valueOrTextOrIndex) } }) } - const userOptions = options - - options = _.defaults({}, userOptions, { + const options: InternalSelectOptions = _.defaults({}, userOptions, { $el: subject, log: true, force: false, @@ -55,7 +59,7 @@ export default (Commands, Cypress, cy) => { }, }) - options._log.snapshot('before', { next: 'after' }) + options._log!.snapshot('before', { next: 'after' }) } let node diff --git a/packages/driver/src/cy/commands/actions/submit.ts b/packages/driver/src/cy/commands/actions/submit.ts index cbd61919c931..de2b6e10474a 100644 --- a/packages/driver/src/cy/commands/actions/submit.ts +++ b/packages/driver/src/cy/commands/actions/submit.ts @@ -5,14 +5,17 @@ import $dom from '../../../dom' import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' import $actionability from '../../actionability' +import type { Log } from '../../../cypress/log' + +interface InternalSubmitOptions extends Partial{ + _log?: Log + $el: JQuery +} export default (Commands, Cypress, cy) => { Commands.addAll({ prevSubject: 'element' }, { - // TODO: any -> Partial - submit (subject, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + submit (subject: JQuery, userOptions: Partial = {}) { + const options: InternalSubmitOptions = _.defaults({}, userOptions, { log: true, $el: subject, }) @@ -35,7 +38,7 @@ export default (Commands, Cypress, cy) => { }, }) - options._log.snapshot('before', { next: 'after' }) + options._log!.snapshot('before', { next: 'after' }) } if (!options.$el.is('form')) { diff --git a/packages/driver/src/cy/commands/actions/type.ts b/packages/driver/src/cy/commands/actions/type.ts index e131970b38f8..a168ebf88160 100644 --- a/packages/driver/src/cy/commands/actions/type.ts +++ b/packages/driver/src/cy/commands/actions/type.ts @@ -8,23 +8,33 @@ import $utils from '../../../cypress/utils' import $errUtils from '../../../cypress/error_utils' import $actionability from '../../actionability' import $Keyboard from '../../../cy/keyboard' +import type { Log } from '../../../cypress/log' + import debugFn from 'debug' const debug = debugFn('cypress:driver:command:type') +interface InternalTypeOptions extends Partial { + _log?: Log + $el: JQuery + ensure?: object + verify: boolean + interval?: number +} + +interface InternalClearOptions extends Partial { + _log?: Log + ensure?: object +} + export default function (Commands, Cypress, cy, state, config) { const { keyboard } = cy.devices - // Note: These "change type of `any` to X" comments are written instead of changing them directly - // because Cypress extends user-given options with Cypress internal options. - // These comments will be removed after removing `// @ts-nocheck` comments in `packages/driver`. - // TODO: change the type of `any` to `Partial` - function type (subject, chars, options: any = {}) { - const userOptions = options + function type (subject, chars, userOptions: Partial = {}) { let updateTable // allow the el we're typing into to be // changed by options -- used by cy.clear() - options = _.defaults({}, userOptions, { + const options: InternalTypeOptions = _.defaults({}, userOptions, { $el: subject, log: true, verify: true, @@ -110,7 +120,7 @@ export default function (Commands, Cypress, cy, state, config) { }, }) - options._log.snapshot('before', { next: 'after' }) + options._log!.snapshot('before', { next: 'after' }) } if (options.$el.length > 1) { @@ -572,11 +582,8 @@ export default function (Commands, Cypress, cy, state, config) { }) } - // TODO: change the type of `any` to `Partial` - function clear (subject, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + function clear (subject, userOptions: Partial = {}) { + const options: InternalClearOptions = _.defaults({}, userOptions, { log: true, force: false, waitForAnimations: config('waitForAnimations'), diff --git a/packages/driver/src/cy/commands/angular.ts b/packages/driver/src/cy/commands/angular.ts index 48661b05d8a2..f6b4bf374979 100644 --- a/packages/driver/src/cy/commands/angular.ts +++ b/packages/driver/src/cy/commands/angular.ts @@ -3,9 +3,14 @@ import $ from 'jquery' import Promise from 'bluebird' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' const ngPrefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-'] +interface InternalNgOptions extends Partial { + _log?: Log +} + export default (Commands, Cypress, cy, state) => { const findByNgBinding = (binding, options) => { const selector = '.ng-binding' @@ -89,10 +94,7 @@ export default (Commands, Cypress, cy, state) => { } Commands.addAll({ - // TODO: Change the options type from `any` to `Partial`. - ng (type, selector, options: any = {}) { - const userOptions = options - + ng (type: string, selector: string, userOptions: Partial = {}) { // what about requirejs / browserify? // we need to intelligently check to see if we're using those // and if angular is available through them. throw a very specific @@ -102,7 +104,7 @@ export default (Commands, Cypress, cy, state) => { $errUtils.throwErrByPath('ng.no_global') } - options = _.defaults({}, userOptions, { log: true }) + const options: InternalNgOptions = _.defaults({}, userOptions, { log: true }) if (options.log) { options._log = Cypress.log({ diff --git a/packages/driver/src/cy/commands/clock.ts b/packages/driver/src/cy/commands/clock.ts index aa202fbeff46..6ac0a36bb5a1 100644 --- a/packages/driver/src/cy/commands/clock.ts +++ b/packages/driver/src/cy/commands/clock.ts @@ -42,8 +42,7 @@ export default function (Commands, Cypress, cy, state) { }) return Commands.addAll({ type: 'utility' }, { - // TODO: change the options type from `any` to Partial. - clock (subject, now, methods, options: any = {}) { + clock (subject, now, methods, options: Partial = {}) { let userOptions = options const ctx = state('ctx') diff --git a/packages/driver/src/cy/commands/cookies.ts b/packages/driver/src/cy/commands/cookies.ts index 9002763dc0db..69075312e083 100644 --- a/packages/driver/src/cy/commands/cookies.ts +++ b/packages/driver/src/cy/commands/cookies.ts @@ -3,6 +3,7 @@ import Promise from 'bluebird' import $utils from '../../cypress/utils' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' import { $Location } from '../../cypress/location' // TODO: add hostOnly to COOKIE_PROPS @@ -78,6 +79,30 @@ function cookieValidatesSecurePrefix (options) { return options.secure === false } +interface InternalGetCookieOptions extends Partial { + _log?: Log + cookie?: Cypress.Cookie +} + +interface InternalGetCookiesOptions extends Partial { + _log?: Log + cookies?: Cypress.Cookie[] +} + +interface InternalSetCookieOptions extends Partial { + _log?: Log + name: string + cookie?: Cypress.Cookie +} + +type InternalClearCookieOptions = InternalGetCookieOptions + +interface InternalClearCookiesOptions extends Partial { + _log?: Log + cookies?: Cypress.Cookie[] + domain?: any +} + export default function (Commands, Cypress, cy, state, config) { const automateCookies = function (event, obj = {}, log, timeout) { const automate = () => { @@ -164,11 +189,8 @@ export default function (Commands, Cypress, cy, state, config) { }) return Commands.addAll({ - // TODO: change the type of `any` to `Partial` - getCookie (name, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + getCookie (name, userOptions: Partial = {}) { + const options: InternalGetCookieOptions = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) @@ -211,11 +233,8 @@ export default function (Commands, Cypress, cy, state, config) { .catch(handleBackendError('getCookie', 'reading the requested cookie from', onFail)) }, - // TODO: change the type of `any` to `Partial` - getCookies (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + getCookies (userOptions: Partial = {}) { + const options: InternalGetCookiesOptions = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) @@ -250,11 +269,8 @@ export default function (Commands, Cypress, cy, state, config) { .catch(handleBackendError('getCookies', 'reading cookies from', options._log)) }, - // TODO: change the type of `any` to `Partial` - setCookie (name, value, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + setCookie (name, value, userOptions: Partial = {}) { + const options: InternalSetCookieOptions = _.defaults({}, userOptions, { name, value, path: '/', @@ -332,11 +348,8 @@ export default function (Commands, Cypress, cy, state, config) { }).catch(handleBackendError('setCookie', 'setting the requested cookie in', onFail)) }, - // TODO: change the type of `any` to `Partial` - clearCookie (name, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + clearCookie (name, userOptions: Partial = {}) { + const options: InternalClearCookieOptions = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) @@ -382,11 +395,8 @@ export default function (Commands, Cypress, cy, state, config) { .catch(handleBackendError('clearCookie', 'clearing the requested cookie in', onFail)) }, - // TODO: change the type of `any` to `Partial` - clearCookies (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + clearCookies (userOptions: Partial = {}) { + const options: InternalClearCookiesOptions = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) @@ -396,12 +406,12 @@ export default function (Commands, Cypress, cy, state, config) { message: '', timeout: options.timeout, consoleProps () { - let c + const c = options.cookies const obj = {} obj['Yielded'] = 'null' - if ((c = options.cookies) && c.length) { + if (c && c.length) { obj['Cleared Cookies'] = c obj['Num Cookies'] = c.length } else { diff --git a/packages/driver/src/cy/commands/debugging.ts b/packages/driver/src/cy/commands/debugging.ts index 6318e904b9ef..8ac152cf0604 100644 --- a/packages/driver/src/cy/commands/debugging.ts +++ b/packages/driver/src/cy/commands/debugging.ts @@ -1,6 +1,7 @@ import _ from 'lodash' import $utils from '../../cypress/utils' +import type { Log } from '../../cypress/log' const resume = (state, resumeAll = true) => { const onResume = state('onResume') @@ -32,6 +33,14 @@ const getNextQueuedCommand = (state, queue) => { return search(state('index')) } +interface InternalPauseOptions extends Partial { + _log?: Log +} + +interface InternalDebugOptions extends Partial { + _log?: Log +} + export default (Commands, Cypress, cy, state, config) => { Cypress.on('resume:next', () => { return resume(state, false) @@ -42,18 +51,15 @@ export default (Commands, Cypress, cy, state, config) => { }) Commands.addAll({ type: 'utility', prevSubject: 'optional' }, { - // TODO: change the options type from `any` to `Loggable`. // pause should indefinitely pause until the user // presses a key or clicks in the UI to continue - pause (subject, options: any = {}) { + pause (subject, userOptions: Partial = {}) { // bail if we're in run mode, unless --headed and --no-exit flags are passed if (!config('isInteractive') && (!config('browser').isHeaded || config('exit'))) { return subject } - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + const options: InternalPauseOptions = _.defaults({}, userOptions, { log: true }) if (options.log) { options._log = Cypress.log({ @@ -72,8 +78,8 @@ export default (Commands, Cypress, cy, state, config) => { // pause on the very next one state('onPaused', null) - if (options.log) { - options._log.end() + if (options._log) { + options._log!.end() } } @@ -103,11 +109,8 @@ export default (Commands, Cypress, cy, state, config) => { return subject }, - // TODO: change `any` to Loggable - debug (subject, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + debug (subject, userOptions: Partial = {}) { + const options: InternalDebugOptions = _.defaults({}, userOptions, { log: true, }) diff --git a/packages/driver/src/cy/commands/exec.ts b/packages/driver/src/cy/commands/exec.ts index d6114590579f..a0131c604ef1 100644 --- a/packages/driver/src/cy/commands/exec.ts +++ b/packages/driver/src/cy/commands/exec.ts @@ -2,14 +2,17 @@ import _ from 'lodash' import Promise from 'bluebird' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' + +interface InternalExecOptions extends Partial { + _log?: Log + cmd?: string +} export default (Commands, Cypress, cy) => { Commands.addAll({ - // TODO: change the type of `any` to `Partical` - exec (cmd, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + exec (cmd: string, userOptions: Partial = {}) { + const options: InternalExecOptions = _.defaults({}, userOptions, { log: true, timeout: Cypress.config('execTimeout'), failOnNonZeroExit: true, diff --git a/packages/driver/src/cy/commands/files.ts b/packages/driver/src/cy/commands/files.ts index 473bc7b02d80..02512836a209 100644 --- a/packages/driver/src/cy/commands/files.ts +++ b/packages/driver/src/cy/commands/files.ts @@ -2,19 +2,26 @@ import _ from 'lodash' import { basename } from 'path' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' + +interface InternalReadFileOptions extends Partial { + _log?: Log + encoding: Cypress.Encodings +} + +interface InternalWriteFileOptions extends Partial { + _log?: Log +} export default (Commands, Cypress, cy, state) => { Commands.addAll({ - // TODO: change the type of `any` to `Partial` - readFile (file, encoding, options: any = {}) { - let userOptions = options - + readFile (file, encoding, userOptions: Partial = {}) { if (_.isObject(encoding)) { userOptions = encoding encoding = undefined } - options = _.defaults({}, userOptions, { + const options: InternalReadFileOptions = _.defaults({}, userOptions, { // https://github.com/cypress-io/cypress/issues/1558 // If no encoding is specified, then Cypress has historically defaulted // to `utf8`, because of it's focus on text files. This is in contrast to @@ -109,16 +116,13 @@ export default (Commands, Cypress, cy, state) => { return verifyAssertions() }, - // TODO: change the type of `any` to `Partial` - writeFile (fileName, contents, encoding, options: any = {}) { - let userOptions = options - + writeFile (fileName, contents, encoding, userOptions: Partial = {}) { if (_.isObject(encoding)) { userOptions = encoding encoding = undefined } - options = _.defaults({}, userOptions, { + const options: InternalWriteFileOptions = _.defaults({}, userOptions, { // https://github.com/cypress-io/cypress/issues/1558 // If no encoding is specified, then Cypress has historically defaulted // to `utf8`, because of it's focus on text files. This is in contrast to diff --git a/packages/driver/src/cy/commands/location.ts b/packages/driver/src/cy/commands/location.ts index 837d39b8a3d3..900d12cabca1 100644 --- a/packages/driver/src/cy/commands/location.ts +++ b/packages/driver/src/cy/commands/location.ts @@ -2,15 +2,21 @@ import _ from 'lodash' import Promise from 'bluebird' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' const { throwErrByPath } = $errUtils +interface InternalUrlOptions extends Partial { + _log?: Log +} + +interface InternalHashOptions extends Partial { + _log?: Log +} + export default (Commands, Cypress, cy) => { Commands.addAll({ - // TODO: change the type of `any` to `Partial` - url (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + url (userOptions: Partial = {}) { + const options: InternalUrlOptions = _.defaults({}, userOptions, { log: true }) if (options.log !== false) { options._log = Cypress.log({ @@ -38,11 +44,8 @@ export default (Commands, Cypress, cy) => { return resolveHref() }, - // TODO: change the type of `any` to `Partial` - hash (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + hash (userOptions: Partial = {}) { + const options: InternalHashOptions = _.defaults({}, userOptions, { log: true }) if (options.log !== false) { options._log = Cypress.log({ diff --git a/packages/driver/src/cy/commands/misc.ts b/packages/driver/src/cy/commands/misc.ts index 5a0db7a0f094..54d862663cc6 100644 --- a/packages/driver/src/cy/commands/misc.ts +++ b/packages/driver/src/cy/commands/misc.ts @@ -4,6 +4,12 @@ import Promise from 'bluebird' import $Command from '../../cypress/command' import $dom from '../../dom' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' + +interface InternalWrapOptions extends Partial { + _log?: Log + timeout: number +} export default (Commands, Cypress, cy, state) => { Commands.addAll({ prevSubject: 'optional' }, { @@ -49,11 +55,8 @@ export default (Commands, Cypress, cy, state) => { return null }, - // TODO: change the type of `any` to `Partial` - wrap (arg, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + wrap (arg, userOptions: Partial = {}) { + const options: InternalWrapOptions = _.defaults({}, userOptions, { log: true, timeout: Cypress.config('defaultCommandTimeout'), }) @@ -68,7 +71,7 @@ export default (Commands, Cypress, cy, state) => { }) if ($dom.isElement(arg)) { - options._log.set({ $el: arg }) + options._log!.set({ $el: arg }) } } diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index 97bbb11359e7..da2a73e2b2d7 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -5,7 +5,7 @@ import Promise from 'bluebird' import $utils from '../../cypress/utils' import $errUtils from '../../cypress/error_utils' -import { LogUtils } from '../../cypress/log' +import { LogUtils, Log } from '../../cypress/log' import { bothUrlsMatchAndOneHasHash } from '../navigation' import { $Location } from '../../cypress/location' @@ -410,6 +410,10 @@ type InvalidContentTypeError = Error & { invalidContentType: boolean } +interface InternalVisitOptions extends Partial { + _log?: Log +} + export default (Commands, Cypress, cy, state, config) => { reset() @@ -693,14 +697,11 @@ export default (Commands, Cypress, cy, state, config) => { return $errUtils.throwErrByPath('go.invalid_argument', { onFail: options._log }) }, - // TODO: Change the type of `any` to `Partial`. - visit (url, options: any = {}) { - if (options.url && url) { - $errUtils.throwErrByPath('visit.no_duplicate_url', { args: { optionsUrl: options.url, url } }) + visit (url, userOptions: Partial = {}) { + if (userOptions.url && url) { + $errUtils.throwErrByPath('visit.no_duplicate_url', { args: { optionsUrl: userOptions.url, url } }) } - let userOptions = options - if (_.isObject(url) && _.isEqual(userOptions, {})) { // options specified as only argument userOptions = url @@ -717,7 +718,7 @@ export default (Commands, Cypress, cy, state, config) => { consoleProps['Options'] = _.pick(userOptions, VISIT_OPTS) } - options = _.defaults({}, userOptions, { + const options: InternalVisitOptions = _.defaults({}, userOptions, { auth: null, failOnStatusCode: true, retryOnNetworkFailure: true, @@ -972,14 +973,14 @@ export default (Commands, Cypress, cy, state, config) => { } } - if (options.log) { - let message = options._log.get('message') + if (options._log) { + let message = options._log!.get('message') if (redirects && redirects.length) { message = [message].concat(redirects).join(' -> ') } - options._log.set({ message }) + options._log!.set({ message }) } consoleProps['Resolved Url'] = url diff --git a/packages/driver/src/cy/commands/querying/focused.ts b/packages/driver/src/cy/commands/querying/focused.ts index 45f67366ae20..c96acf77cc4c 100644 --- a/packages/driver/src/cy/commands/querying/focused.ts +++ b/packages/driver/src/cy/commands/querying/focused.ts @@ -2,14 +2,17 @@ import _ from 'lodash' import Promise from 'bluebird' import $dom from '../../../dom' +import type { Log } from '../../../cypress/log' + +interface InternalFocusedOptions extends Partial{ + _log?: Log + verify: boolean +} export default (Commands, Cypress, cy, state) => { Commands.addAll({ - // TODO: any -> Partial - focused (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + focused (userOptions: Partial = {}) { + const options: InternalFocusedOptions = _.defaults({}, userOptions, { verify: true, log: true, }) @@ -23,7 +26,7 @@ export default (Commands, Cypress, cy, state) => { return } - options._log.set({ + options._log!.set({ $el, consoleProps () { const ret = $el ? $dom.getElements($el) : '--nothing--' diff --git a/packages/driver/src/cy/commands/querying/querying.ts b/packages/driver/src/cy/commands/querying/querying.ts index aa80eeb3c268..0c4ffb9f1d93 100644 --- a/packages/driver/src/cy/commands/querying/querying.ts +++ b/packages/driver/src/cy/commands/querying/querying.ts @@ -4,14 +4,25 @@ import Promise from 'bluebird' import $dom from '../../../dom' import $elements from '../../../dom/elements' import $errUtils from '../../../cypress/error_utils' +import type { Log } from '../../../cypress/log' import { resolveShadowDomInclusion } from '../../../cypress/shadow_dom_utils' import { getAliasedRequests, isDynamicAliasingPossible } from '../../net-stubbing/aliasing' +interface InternalGetOptions extends Partial { + _log?: Log + _retries?: number + filter?: any + onRetry?: Function + verify?: boolean +} + +interface InternalContainsOptions extends Partial { + _log?: Log +} + export default (Commands, Cypress, cy, state) => { Commands.addAll({ - // TODO: any -> Partial - get (selector, options: any = {}) { - const userOptions = options + get (selector, userOptions: Partial = {}) { const ctx = this if ((userOptions === null) || _.isArray(userOptions) || !_.isPlainObject(userOptions)) { @@ -20,7 +31,7 @@ export default (Commands, Cypress, cy, state) => { }) } - options = _.defaults({}, userOptions, { + const options: InternalGetOptions = _.defaults({}, userOptions, { retry: true, withinSubject: state('withinSubject'), log: true, @@ -28,7 +39,7 @@ export default (Commands, Cypress, cy, state) => { verify: true, }) - options.includeShadowDom = resolveShadowDomInclusion(Cypress, userOptions.includeShadowDom) + options.includeShadowDom = resolveShadowDomInclusion(Cypress, options.includeShadowDom) let aliasObj const consoleProps: Record = {} @@ -100,7 +111,7 @@ export default (Commands, Cypress, cy, state) => { return consoleProps } - options._log.set(obj) + options._log!.set(obj) } let allParts @@ -263,14 +274,14 @@ export default (Commands, Cypress, cy, state) => { consoleProps.Yielded = $dom.getElements($el) consoleProps.Elements = $el != null ? $el.length : undefined - options._log.set({ $el }) + options._log!.set({ $el }) } const getElements = () => { let $el try { - let scope = options.withinSubject + let scope: (typeof options.withinSubject) | Node[] = options.withinSubject if (options.includeShadowDom) { const root = options.withinSubject ? options.withinSubject[0] : cy.state('document') @@ -295,7 +306,7 @@ export default (Commands, Cypress, cy, state) => { return err } - options._log.error(err) + options._log!.error(err) } throw err @@ -305,7 +316,7 @@ export default (Commands, Cypress, cy, state) => { // and we have been explictly told to filter // then just attempt to filter out elements from our within subject if (!$el.length && options.withinSubject && options.filter) { - const filtered = options.withinSubject.filter(selector) + const filtered = (options.withinSubject as JQuery).filter(selector) // reset $el if this found anything if (filtered.length) { @@ -350,10 +361,7 @@ export default (Commands, Cypress, cy, state) => { }) Commands.addAll({ prevSubject: ['optional', 'window', 'document', 'element'] }, { - // TODO: any -> Partial - contains (subject, filter, text, options: any = {}) { - let userOptions = options - + contains (subject, filter, text, userOptions: Partial = {}) { // nuke our subject if its present but not an element. // in these cases its either window or document but // we dont care. @@ -389,7 +397,7 @@ export default (Commands, Cypress, cy, state) => { $errUtils.throwErrByPath('contains.regex_conflict') } - options = _.defaults({}, userOptions, { log: true, matchCase: true }) + const options: InternalContainsOptions = _.defaults({}, userOptions, { log: true, matchCase: true }) if (!(_.isString(text) || _.isFinite(text) || _.isRegExp(text))) { $errUtils.throwErrByPath('contains.invalid_argument') @@ -459,7 +467,7 @@ export default (Commands, Cypress, cy, state) => { consoleProps.Yielded = $dom.getElements($el) consoleProps.Elements = $el != null ? $el.length : undefined - options._log.set({ $el }) + options._log!.set({ $el }) } // find elements by the :cy-contains psuedo selector diff --git a/packages/driver/src/cy/commands/querying/root.ts b/packages/driver/src/cy/commands/querying/root.ts index 579bc461d31f..6847c9b955e3 100644 --- a/packages/driver/src/cy/commands/querying/root.ts +++ b/packages/driver/src/cy/commands/querying/root.ts @@ -1,12 +1,14 @@ import _ from 'lodash' +import type { Log } from '../../../cypress/log' + +interface InternalRootOptions extends Partial { + _log?: Log +} export default (Commands, Cypress, cy, state) => { Commands.addAll({ - // TODO: any -> Partial - root (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + root (userOptions: Partial = {}) { + const options: InternalRootOptions = _.defaults({}, userOptions, { log: true }) if (options.log !== false) { options._log = Cypress.log({ @@ -16,8 +18,8 @@ export default (Commands, Cypress, cy, state) => { } const log = ($el) => { - if (options.log) { - options._log.set({ $el }) + if (options._log) { + options._log!.set({ $el }) } return $el diff --git a/packages/driver/src/cy/commands/screenshot.ts b/packages/driver/src/cy/commands/screenshot.ts index 53987c587c0b..585cd08639e8 100644 --- a/packages/driver/src/cy/commands/screenshot.ts +++ b/packages/driver/src/cy/commands/screenshot.ts @@ -8,6 +8,7 @@ import $Screenshot from '../../cypress/screenshot' import $dom from '../../dom' import $errUtils from '../../cypress/error_utils' import $utils from '../../cypress/utils' +import type { Log } from '../../cypress/log' const getViewportHeight = (state) => { // TODO this doesn't seem correct @@ -410,6 +411,10 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options: TakeScreensho .finally(after) } +interface InternalScreenshotOptions extends Partial { + _log?: Log +} + export default function (Commands, Cypress, cy, state, config) { // failure screenshot when not interactive Cypress.on('runnable:after:run:async', (test, runnable) => { @@ -440,10 +445,7 @@ export default function (Commands, Cypress, cy, state, config) { }) Commands.addAll({ prevSubject: ['optional', 'element', 'window', 'document'] }, { - // TODO: any -> Partial - screenshot (subject, name, options: any = {}) { - let userOptions = options - + screenshot (subject, name, userOptions: Partial = {}) { if (_.isObject(name)) { userOptions = name name = null @@ -452,7 +454,7 @@ export default function (Commands, Cypress, cy, state, config) { // make sure when we capture the entire test runner // we are not limited to "within" subject // https://github.com/cypress-io/cypress/issues/14253 - if (options.capture !== 'runner') { + if (userOptions.capture !== 'runner') { const withinSubject = state('withinSubject') if (withinSubject && $dom.isElement(withinSubject)) { @@ -463,7 +465,7 @@ export default function (Commands, Cypress, cy, state, config) { // TODO: handle hook titles const runnable = state('runnable') - options = _.defaults({}, userOptions, { + const options: InternalScreenshotOptions = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) diff --git a/packages/driver/src/cy/commands/task.ts b/packages/driver/src/cy/commands/task.ts index ff07b7bf8d7e..7223b95084e7 100644 --- a/packages/driver/src/cy/commands/task.ts +++ b/packages/driver/src/cy/commands/task.ts @@ -4,14 +4,16 @@ import Promise from 'bluebird' import $utils from '../../cypress/utils' import $errUtils from '../../cypress/error_utils' import $stackUtils from '../../cypress/stack_utils' +import type { Log } from '../../cypress/log' + +interface InternalTaskOptions extends Partial { + _log?: Log +} export default (Commands, Cypress, cy) => { Commands.addAll({ - // TODO: any -> Partial - task (task, arg, options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + task (task, arg, userOptions: Partial = {}) { + const options: InternalTaskOptions = _.defaults({}, userOptions, { log: true, timeout: Cypress.config('taskTimeout'), }) diff --git a/packages/driver/src/cy/commands/window.ts b/packages/driver/src/cy/commands/window.ts index a4005d888443..908e81189a25 100644 --- a/packages/driver/src/cy/commands/window.ts +++ b/packages/driver/src/cy/commands/window.ts @@ -2,6 +2,7 @@ import _ from 'lodash' import Promise from 'bluebird' import $errUtils from '../../cypress/error_utils' +import type { Log } from '../../cypress/log' const viewports = { 'macbook-16': '1536x960', @@ -35,6 +36,24 @@ type CurrentViewport = Pick // refresh would cause viewport to hang let currentViewport: CurrentViewport | null = null +interface InternalTitleOptions extends Partial { + _log?: Log +} + +interface InternalWindowOptions extends Partial { + _log?: Log + error?: any +} + +interface InternalDocumentOptions extends Partial { + _log?: Log + error?: any +} + +interface InternalViewportOptions extends Partial { + _log?: Log +} + export default (Commands, Cypress, cy, state) => { const defaultViewport: CurrentViewport = _.pick(Cypress.config() as Cypress.Config, 'viewportWidth', 'viewportHeight') @@ -78,11 +97,8 @@ export default (Commands, Cypress, cy, state) => { } Commands.addAll({ - // TODO: any -> Partial - title (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + title (userOptions: Partial = {}) { + const options: InternalTitleOptions = _.defaults({}, userOptions, { log: true }) if (options.log) { options._log = Cypress.log({ timeout: options.timeout }) @@ -101,11 +117,8 @@ export default (Commands, Cypress, cy, state) => { return resolveTitle() }, - // TODO: any -> Partial - window (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + window (userOptions: Partial = {}) { + const options: InternalWindowOptions = _.defaults({}, userOptions, { log: true }) if (options.log) { options._log = Cypress.log({ timeout: options.timeout }) @@ -144,11 +157,8 @@ export default (Commands, Cypress, cy, state) => { return verifyAssertions() }, - // TODO: any -> Partial - document (options: any = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { log: true }) + document (userOptions: Partial = {}) { + const options: InternalDocumentOptions = _.defaults({}, userOptions, { log: true }) if (options.log) { options._log = Cypress.log({ timeout: options.timeout }) @@ -188,15 +198,12 @@ export default (Commands, Cypress, cy, state) => { return verifyAssertions() }, - // TODO: any -> Partial - viewport (presetOrWidth, heightOrOrientation, options: any = {}) { - const userOptions = options - + viewport (presetOrWidth, heightOrOrientation, userOptions: Partial = {}) { if (_.isObject(heightOrOrientation)) { - options = heightOrOrientation + userOptions = heightOrOrientation } - options = _.defaults({}, userOptions, { log: true }) + const options: InternalViewportOptions = _.defaults({}, userOptions, { log: true }) let height let width @@ -208,10 +215,6 @@ export default (Commands, Cypress, cy, state) => { const isPreset = typeof presetOrWidth === 'string' options._log = Cypress.log({ - // TODO: timeout below should be removed - // because cy.viewport option doesn't support `timeout` - // @see https://docs.cypress.io/api/commands/viewport#Arguments - timeout: options.timeout, consoleProps () { const obj: Record = {} diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index f03306b6fa21..c44791ce2fc3 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -12,6 +12,7 @@ import type { HTMLTextLikeElement } from '../dom/elements' import $selection from '../dom/selection' import $utils from '../cypress/utils' import $window from '../dom/window' +import type { Log } from '../cypress/log' const debug = Debug('cypress:driver:keyboard') @@ -680,7 +681,7 @@ export interface typeOptions { force?: boolean simulated?: boolean release?: boolean - _log?: any + _log?: Log delay?: number onError?: Function onEvent?: Function diff --git a/packages/driver/src/cy/video-recorder.ts b/packages/driver/src/cy/video-recorder.ts index 980d4aa75e7b..008d4cb6c8d4 100644 --- a/packages/driver/src/cy/video-recorder.ts +++ b/packages/driver/src/cy/video-recorder.ts @@ -24,8 +24,6 @@ export const initVideoRecorder = (Cypress) => { mimeType: 'video/webm', } - // TODO: update TypeScript to 4.4+. - // @ts-ignore const mediaRecorder = new window.MediaRecorder(stream, options) mediaRecorder.start(200) diff --git a/packages/driver/src/cypress/browser.ts b/packages/driver/src/cypress/browser.ts index 559d74843496..a3d188955c06 100644 --- a/packages/driver/src/cypress/browser.ts +++ b/packages/driver/src/cypress/browser.ts @@ -35,8 +35,7 @@ const _isBrowser = (browser, matcher, errPrefix) => { } } -// TODO: change the type of `any` to `IsBrowserMatcher` -const isBrowser = (config, obj: any = '', errPrefix: string = '`Cypress.isBrowser()`') => { +const isBrowser = (config, obj: Cypress.IsBrowserMatcher = '', errPrefix: string = '`Cypress.isBrowser()`') => { return _ .chain(obj) .concat([]) diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 88ec76a1a60a..9418a200bb6a 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -219,7 +219,7 @@ const defaults = function (state: Cypress.State, config, obj) { return obj } -class Log { +export class Log { cy: any state: Cypress.State config: any diff --git a/patches/@types+jquery.scrollto+1.4.29.dev.patch b/patches/@types+jquery.scrollto+1.4.29.dev.patch new file mode 100644 index 000000000000..8c79239f7a1c --- /dev/null +++ b/patches/@types+jquery.scrollto+1.4.29.dev.patch @@ -0,0 +1,58 @@ +diff --git a/node_modules/@types/jquery.scrollto/index.d.ts b/node_modules/@types/jquery.scrollto/index.d.ts +index 0a00c69..126a3d0 100755 +--- a/node_modules/@types/jquery.scrollto/index.d.ts ++++ b/node_modules/@types/jquery.scrollto/index.d.ts +@@ -6,7 +6,7 @@ + + /// + +-interface ScrollToOptions { ++interface JQueryScrollToOptions extends JQuery.EffectsOptions { + /** + * Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. + */ +@@ -48,7 +48,7 @@ interface ScrollToOptions { + onAfterFirst?: (() => void) | undefined; + } + +-interface JQuery { ++interface JQuery { + /** + * Scroll the matched elements + */ +@@ -60,7 +60,7 @@ interface JQuery { + * @param duration The OVERALL length of the animation + * @param settings Set of settings. + */ +- (target: any, duration?: number, settings?: ScrollToOptions): JQuery; ++ (target: any, duration?: number, settings?: JQueryScrollToOptions): JQuery; + /** + * Scroll the matched elements + * +@@ -76,7 +76,7 @@ interface JQuery { + * @param settings Set of settings. + * @param onAfter The onAfter callback. + */ +- (target: any, settings: ScrollToOptions, onAfter?: Function): JQuery; ++ (target: any, settings: JQueryScrollToOptions, onAfter?: Function): JQuery; + + }; + +@@ -94,7 +94,7 @@ interface JQueryStatic { + * @param duration The OVERALL length of the animation + * @param settings Set of settings. + */ +- (target: any, duration?: number, settings?: ScrollToOptions): JQuery; ++ (target: any, duration?: number, settings?: JQueryScrollToOptions): JQuery; + /** + * Scroll window + * +@@ -110,7 +110,7 @@ interface JQueryStatic { + * @param settings Set of settings. + * @param onAfter The onAfter callback. + */ +- (target: any, settings: ScrollToOptions, onAfter?: Function): JQuery; ++ (target: any, settings: JQueryScrollToOptions, onAfter?: Function): JQuery; + + }; + From a1101e65629db7b39efd9aff5a62b8ad927edebd Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Fri, 22 Apr 2022 10:30:40 -0400 Subject: [PATCH 115/177] feat: support snapshots and console props within multi-domain (#20949) * Empty commit to get a new percy nonce * Implement snapshots and consoleprops within multi origin further progress with getters cleaned up log/snapshot serialization attempt to pass and hydrate value state of serialized dom elements temp commit traversal dom by some stretch of a miracle this is working... still somehow working after massive performance issues with full tree serialization, fix here is to just attach values to inputs for reifying on primary now we are cookin test WIP tests WIP working multi-domain actions snapshots tests added more tests to verify snapshots add tests and refactor certain tests to make simpler added misc snapshot tests add navigation snapshot placeholder add network request snapshot tests add shadow querying snapshot tests update test names added snapshot querying spec added screenshot snapshot test add spies,clocks, and stubs tests implement snapshot tests for traversal commands rename local storeage snapshot tests to fit convention add viewport snapshot tests rename snapshot traversal to fit naming convention add snapshot waiting tests added window snapshot tests implement navigation snapshot tests now that sinon proxy issues internal to log are now fixed refactor multi-domain snapshot tests to leverage utility method over redefining in each spec * fix lint types issues on serializationKeys * rename switchToDomain to origin (might help with failing tests... ya know?) * rename snapshot files to fit origin paradigm and fix misname on primaryDomainCommunicator * fix .tick() snapshot/consoleProps test (figure out the deal with consoleProps sometimes being a function) * rename multiDomainCommunicator to origin primaryDomainCommunicator * don't invoke functions with arguments (we need to be more explicit about the functions we are invoking * opt for my explicit serialization behavior with functions, never attempt to serialize bluebird promises * move serialization to folder and change name to index * refactor log serialization to own file, clean up code and add comments to what is going on in this 'here be dragons' code * make sure to serialize functions for snapshots * fix pause snapshot test for multi origin * refactor postprocess snapshot into own method to handle in final state snapshot processing for cross origin * update snapshot comments to be more accurate * fix renamings within tests * fix path in log.ts serialization * revert about:blank changes in aut-iframe which was breaking session * move all log/snapshot serialization magic invokations into the communicator * update typos and fix namings of preprocess and reify * further name changes * fix snapshot generator to always reify snapshot () over attempting to match in the DOM * unskip test that was fixed with explicit function serialization for logs * fix flaky screenshot test that was screensize dependent * rename a few items in the log serialization file * clean up snapshot style reification to be more straightforward and remove redundancies * refactor snapshots code to be more readable * update reifyDomElement docs to actually explain what hte method does * fix typos within the log serialization file pertaining to comments * use Cypress._ over lodash import to reduce spec bundle size * remove snapshots test folder and migrate tests into commands directory with #consoleProps context blocks for each * change removeSrcAttributeFromAUTIframe name to removeSrcAttribute as it is implied on the AUT * update log consoleProps comment to better reflect cross origin nature * remove skipped consoleProps tests that do not have a command log to test against * add createSnapshot to internal types (might need more specifics on this) * refactor multi-domain consoleProp tests to use shouldWithTimeout custom command to avoid setTimeouts on command queue event to make test implementation cleaner * simplify DOM hydration for input based elements * update preprocessedHTMLElement type * clean up some documentation and remove TS ignores. added getStyles to internal-types. * add comment to aut-iframe on src attr removal for posterity * reverse snapshot ternary for readability * add shouldWithTimeout into spec-types and refactor out of internal-types * add getAll type to cypress spec-types * compare originPolicy of top and AUT instead of just origin to make snapshots work in subdomains * add comment to _storeOriginalState for future developers and to add clarity * add some basic log serialization tests that show full pre/reification of log, as well as state hydration for innerHTML. break out object/array methods from log like serialization into own methods * update variables to metasyntactic * add renderProps assertion for cy.request * apply suggestions from code review to clean up log serializer * make snapshot serialization more generic and typesafe * work around firefox 93 issues by unsetting the document in cy state as the document is in a cross origin context in the primary, which means accessing any elements will not work * clean up code and implement suggestions in code review * remove crossOriginLog in favor of nullish coalescing if visible on the log is not set * if get is null, return null for whole snapshot Co-authored-by: Ryan Manuel Co-authored-by: Matt Henkes --- packages/driver/cypress/fixtures/dom.html | 4 +- .../commands/multi_domain_actions.spec.ts | 621 ++++++++++++++++ .../commands/multi_domain_aliasing.spec.ts | 37 + .../commands/multi_domain_assertions.spec.ts | 43 ++ .../commands/multi_domain_connectors.spec.ts | 73 ++ .../commands/multi_domain_cookies.spec.ts | 140 ++++ .../commands/multi_domain_files.spec.ts | 49 ++ .../multi_domain_local_storage.spec.ts | 32 + .../commands/multi_domain_location.spec.ts | 66 ++ .../commands/multi_domain_misc.spec.ts | 140 +++- .../commands/multi_domain_navigation.spec.ts | 75 ++ .../multi_domain_network_requests.spec.ts | 41 ++ .../commands/multi_domain_querying.spec.ts | 72 ++ .../multi_domain_querying_shadow.spec.ts | 38 + .../commands/multi_domain_screenshot.spec.ts | 43 ++ .../multi_domain_spies_stubs_clocks.spec.ts | 99 +++ .../commands/multi_domain_traversal.spec.ts | 529 ++++++++++++++ .../commands/multi_domain_viewport.spec.ts | 28 + .../commands/multi_domain_waiting.spec.ts | 27 + .../commands/multi_domain_window.spec.ts | 53 ++ .../integration/util/serialization_spec.ts | 688 ++++++++++++++++++ packages/driver/cypress/support/utils.js | 20 + packages/driver/src/cy/snapshots.ts | 193 +++-- packages/driver/src/cypress/log.ts | 7 +- .../driver/src/multi-domain/communicator.ts | 26 +- packages/driver/src/multi-domain/cypress.ts | 14 + .../driver/src/multi-domain/events/logs.ts | 6 +- .../index.ts} | 15 +- packages/driver/src/util/serialization/log.ts | 425 +++++++++++ packages/driver/types/internal-types.d.ts | 2 + packages/driver/types/spec-types.d.ts | 8 + packages/runner-ct/src/iframe/iframes.tsx | 2 + packages/runner-shared/src/event-manager.js | 3 + .../runner-shared/src/iframe/aut-iframe.js | 51 ++ .../runner-shared/src/iframe/iframe-model.js | 29 +- packages/runner/index.d.ts | 1 + packages/runner/src/iframe/iframes.jsx | 2 + 37 files changed, 3621 insertions(+), 81 deletions(-) create mode 100644 packages/driver/cypress/integration/util/serialization_spec.ts rename packages/driver/src/util/{serialization.ts => serialization/index.ts} (91%) create mode 100644 packages/driver/src/util/serialization/log.ts create mode 100644 packages/driver/types/spec-types.d.ts diff --git a/packages/driver/cypress/fixtures/dom.html b/packages/driver/cypress/fixtures/dom.html index 1d3e241f2a46..6347924ceccf 100644 --- a/packages/driver/cypress/fixtures/dom.html +++ b/packages/driver/cypress/fixtures/dom.html @@ -437,8 +437,8 @@ not to be checked') + expect(assertionLogs[1].consoleProps.Message).to.equal('expected not to be disabled') + + assertionLogs.forEach(({ $el, consoleProps }) => { + expect($el.jquery).to.be.ok + + expect(consoleProps.Command).to.equal('assert') + expect(consoleProps.subject[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.subject[0]).to.have.property('value').that.equals('blue') + expect(consoleProps.subject[0].getAttribute('name')).to.equal('colors') + }) + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_connectors.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_connectors.spec.ts index 732ce983827e..6b94fc87c3a8 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_connectors.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_connectors.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin connectors', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -43,4 +45,75 @@ context('cy.origin connectors', () => { }) }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.its()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id>input').its('length') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps, $el } = findCrossOriginLogs('its', logs, 'foobar.com') + + expect($el.jquery).to.be.ok + + expect(consoleProps.Command).to.equal('its') + expect(consoleProps.Property).to.equal('.length') + expect(consoleProps.Yielded).to.equal(3) + + expect(consoleProps.Subject.length).to.equal(3) + + // make sure subject elements are indexed in the correct order + expect(consoleProps.Subject[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Subject[0]).to.have.property('id').that.equals('input') + + expect(consoleProps.Subject[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Subject[1]).to.have.property('id').that.equals('name') + + expect(consoleProps.Subject[2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Subject[2]).to.have.property('id').that.equals('age') + }) + }) + + it('.invoke()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#button').invoke('text') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps, $el } = findCrossOriginLogs('invoke', logs, 'foobar.com') + + expect($el.jquery).to.be.ok + + expect(consoleProps.Command).to.equal('invoke') + expect(consoleProps.Function).to.equal('.text()') + expect(consoleProps.Yielded).to.equal('button') + + expect(consoleProps.Subject).to.have.property('tagName').that.equals('BUTTON') + expect(consoleProps.Subject).to.have.property('id').that.equals('button') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_cookies.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_cookies.spec.ts index b068abb275db..ce088c50c29a 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_cookies.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_cookies.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin cookies', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -34,4 +36,142 @@ context('cy.origin cookies', () => { cy.getCookies().should('be.empty') }) }) + + context('#consoleProps', () => { + const { _ } = Cypress + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.getCookie()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.getCookies().should('be.empty') + cy.setCookie('foo', 'bar') + cy.getCookie('foo') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('getCookie', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('getCookie') + expect(consoleProps.Yielded).to.have.property('domain').that.includes('foobar.com') + expect(consoleProps.Yielded).to.have.property('expiry').that.is.a('number') + expect(consoleProps.Yielded).to.have.property('httpOnly').that.equals(false) + expect(consoleProps.Yielded).to.have.property('secure').that.equals(false) + expect(consoleProps.Yielded).to.have.property('name').that.equals('foo') + expect(consoleProps.Yielded).to.have.property('value').that.equals('bar') + expect(consoleProps.Yielded).to.have.property('path').that.is.a('string') + }) + }) + + it('.getCookies()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.getCookies().should('be.empty') + + cy.setCookie('foo', 'bar') + cy.getCookies() + }) + + cy.shouldWithTimeout(() => { + // get the last 'getCookies' command, which is the one we care about for this test + const allGetCookieLogs = findCrossOriginLogs('getCookies', logs, 'foobar.com') + + const { consoleProps } = allGetCookieLogs.pop() as any + + expect(consoleProps.Command).to.equal('getCookies') + expect(consoleProps['Num Cookies']).to.equal(1) + + // can't exactly assert on length() as this is a array proxy object + expect(consoleProps.Yielded.length).to.equal(1) + expect(consoleProps.Yielded[0]).to.have.property('expiry').that.is.a('number') + expect(consoleProps.Yielded[0]).to.have.property('httpOnly').that.equals(false) + expect(consoleProps.Yielded[0]).to.have.property('secure').that.equals(false) + expect(consoleProps.Yielded[0]).to.have.property('name').that.equals('foo') + expect(consoleProps.Yielded[0]).to.have.property('value').that.equals('bar') + expect(consoleProps.Yielded[0]).to.have.property('path').that.is.a('string') + }) + }) + + it('.setCookie()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.getCookies().should('be.empty') + + cy.setCookie('foo', 'bar') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('setCookie', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('setCookie') + expect(consoleProps.Yielded).to.have.property('domain').that.includes('foobar.com') + expect(consoleProps.Yielded).to.have.property('expiry').that.is.a('number') + expect(consoleProps.Yielded).to.have.property('httpOnly').that.equals(false) + expect(consoleProps.Yielded).to.have.property('secure').that.equals(false) + expect(consoleProps.Yielded).to.have.property('name').that.equals('foo') + expect(consoleProps.Yielded).to.have.property('value').that.equals('bar') + expect(consoleProps.Yielded).to.have.property('path').that.is.a('string') + }) + }) + + it('.clearCookie()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.setCookie('foo', 'bar') + cy.getCookie('foo').should('not.be.null') + cy.clearCookie('foo') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('clearCookie', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('clearCookie') + expect(consoleProps.Yielded).to.equal('null') + expect(consoleProps['Cleared Cookie']).to.have.property('domain').that.includes('foobar.com') + expect(consoleProps['Cleared Cookie']).to.have.property('expiry').that.is.a('number') + expect(consoleProps['Cleared Cookie']).to.have.property('httpOnly').that.equals(false) + expect(consoleProps['Cleared Cookie']).to.have.property('secure').that.equals(false) + expect(consoleProps['Cleared Cookie']).to.have.property('name').that.equals('foo') + expect(consoleProps['Cleared Cookie']).to.have.property('value').that.equals('bar') + expect(consoleProps['Cleared Cookie']).to.have.property('path').that.is.a('string') + }) + }) + + it('.clearCookies()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.setCookie('foo', 'bar') + cy.setCookie('faz', 'baz') + + cy.getCookies().should('have.length', 2) + cy.clearCookies() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('clearCookies', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('clearCookies') + expect(consoleProps['Num Cookies']).to.equal(2) + + expect(consoleProps.Yielded).to.equal('null') + + expect(consoleProps['Cleared Cookies'].length).to.equal(2) + + expect(consoleProps['Cleared Cookies'][0]).to.have.property('name').that.equals('foo') + expect(consoleProps['Cleared Cookies'][0]).to.have.property('value').that.equals('bar') + + expect(consoleProps['Cleared Cookies'][1]).to.have.property('name').that.equals('faz') + expect(consoleProps['Cleared Cookies'][1]).to.have.property('value').that.equals('baz') + + _.forEach(consoleProps['Cleared Cookies'], (clearedCookie) => { + expect(clearedCookie).to.have.property('httpOnly').that.equals(false) + expect(clearedCookie).to.have.property('secure').that.equals(false) + expect(clearedCookie).to.have.property('path').that.is.a('string') + }) + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_files.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_files.spec.ts index 611bcf83cdda..b741e55e2ac6 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_files.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_files.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin files', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -44,4 +46,51 @@ context('cy.origin files', () => { }) }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.readFile()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.readFile('cypress/fixtures/example.json') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('readFile', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('readFile') + expect(consoleProps['File Path']).to.include('cypress/fixtures/example.json') + expect(consoleProps.Contents).to.deep.equal({ example: true }) + }) + }) + + it('.writeFile()', () => { + cy.origin('http://foobar.com:3500', () => { + const contents = JSON.stringify({ foo: 'bar' }) + + cy.stub(Cypress, 'backend').resolves({ + contents, + filePath: 'foo.json', + }) + + cy.writeFile('foo.json', contents) + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('writeFile', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('writeFile') + expect(consoleProps['File Path']).to.equal('foo.json') + expect(consoleProps.Contents).to.equal('{"foo":"bar"}') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_local_storage.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_local_storage.spec.ts index 7996d9aed2d9..4ca617ef20d3 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_local_storage.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_local_storage.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin local storage', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -17,4 +19,34 @@ context('cy.origin local storage', () => { }) }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.clearLocalStorage()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.window().then((win) => { + win.localStorage.setItem('foo', 'bar') + expect(win.localStorage.getItem('foo')).to.equal('bar') + }) + + cy.clearLocalStorage() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('clearLocalStorage', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('clearLocalStorage') + expect(consoleProps.Yielded).to.be.null + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_location.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_location.spec.ts index ba4b9a9b5b6e..cb920a1daa55 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_location.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_location.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin location', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -24,4 +26,68 @@ context('cy.origin location', () => { cy.url().should('equal', 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.hash()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.hash() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('hash', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('hash') + }) + }) + + it('.location()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.location() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('location', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('location') + + expect(consoleProps.Yielded).to.have.property('auth').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('authObj').that.is.undefined + expect(consoleProps.Yielded).to.have.property('hash').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('host').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('hostname').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('href').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('origin').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('originPolicy').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('pathname').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('port').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('protocol').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('search').that.is.a('string') + expect(consoleProps.Yielded).to.have.property('superDomain').that.is.a('string') + }) + }) + + it('.url()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.url() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('url', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('url') + + expect(consoleProps.Yielded).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts index beca1cf6cf05..0b5c075c23d8 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin misc', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -56,12 +58,146 @@ context('cy.origin misc', () => { cy.task('return:arg', 'works').should('eq', 'works') }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.exec()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.exec('echo foobar') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('exec', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('exec') + expect(consoleProps['Shell Used']).to.be.undefined + expect(consoleProps.Yielded).to.have.property('code').that.equals(0) + expect(consoleProps.Yielded).to.have.property('stderr').that.equals('') + expect(consoleProps.Yielded).to.have.property('stdout').that.equals('foobar') + }) + }) + + it('.focused()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#button').click().focused() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('focused', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('focused') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('BUTTON') + expect(consoleProps.Yielded).to.have.property('id').that.equals('button') + }) + }) + + it('.wrap()', () => { + cy.origin('http://foobar.com:3500', () => { + const arr = ['foo', 'bar', 'baz'] + + cy.wrap(arr).spread((foo, bar, baz) => { + expect(foo).to.equal('foo') + expect(bar).to.equal('bar') + expect(baz).to.equal('baz') + }) + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('wrap', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('wrap') + expect(consoleProps.Yielded[0]).to.equal('foo') + expect(consoleProps.Yielded[1]).to.equal('bar') + expect(consoleProps.Yielded[2]).to.equal('baz') + }) + }) + + it('.debug()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#button').debug() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('debug', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('debug') + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('BUTTON') + expect(consoleProps.Yielded).to.have.property('id').that.equals('button') + }) + }) + + it('.pause()', () => { + cy.origin('http://foobar.com:3500', () => { + const afterPaused = new Promise((resolve) => { + cy.once('paused', () => { + Cypress.emit('resume:all') + resolve() + }) + }) + + cy.pause().wrap({}).should('deep.eq', {}) + // pause is a noop in run mode, so only wait for it if in open mode + if (Cypress.config('isInteractive')) { + cy.wrap(afterPaused) + } + }) + + cy.shouldWithTimeout(() => { + if (Cypress.config('isInteractive')) { + // if `isInteractive`, the .pause() will NOT show up in the command log in this case. Essentially a no-op. + return + } + + const { consoleProps } = findCrossOriginLogs('pause', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('pause') + expect(consoleProps.Yielded).to.be.undefined + }) + }) + + it('.task()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.task('return:arg', 'works') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('task', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('task') + expect(consoleProps.Yielded).to.equal('works') + expect(consoleProps.arg).to.equal('works') + expect(consoleProps.task).to.equal('return:arg') + }) + }) + }) }) it('verifies number of cy commands', () => { // @ts-ignore - // remove 'getAll' command since it's a custom command we add for our own testing and not an actual cy command - const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => command === 'getAll') + // remove 'getAll' and 'shouldWithTimeout' commands since they are custom commands we added for our own testing and are not actual cy commands + const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => command === 'getAll' || command === 'shouldWithTimeout') const expectedCommands = [ 'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select', 'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then', diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts index e6bc322cdb45..bc95ec846cc6 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts @@ -1,4 +1,5 @@ const { stripIndent } = require('common-tags') +import { findCrossOriginLogs } from '../../../../support/utils' context('cy.origin navigation', () => { it('.go()', () => { @@ -496,4 +497,78 @@ context('cy.origin navigation', () => { cy.location('pathname').should('equal', '/fixtures/dom.html') }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.go()', () => { + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="cross-origin-secondary-link"]').click() + + cy.origin('http://foobar.com:3500', () => { + cy.visit('http://www.foobar.com:3500/fixtures/dom.html') + + cy.go('back') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps, ...attrs } = findCrossOriginLogs('go', logs, 'foobar.com') + + expect(attrs.name).to.equal('go') + expect(attrs.message).to.equal('back') + + expect(consoleProps.Command).to.equal('go') + expect(consoleProps.Yielded).to.be.null + }) + }) + + it('.reload()', () => { + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="dom-link"]').click() + + cy.origin('http://foobar.com:3500', () => { + cy.reload() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps, ...attrs } = findCrossOriginLogs('reload', logs, 'foobar.com') + + expect(attrs.name).to.equal('reload') + expect(attrs.message).to.equal('') + + expect(consoleProps.Command).to.equal('reload') + expect(consoleProps.Yielded).to.be.null + }) + }) + + it('visit()', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.origin('http://foobar.com:3500', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps, ...attrs } = findCrossOriginLogs('visit', logs, 'foobar.com') + + expect(attrs.name).to.equal('visit') + expect(attrs.message).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + expect(consoleProps.Command).to.equal('visit') + expect(consoleProps).to.have.property('Cookies Set').that.is.an('object') + expect(consoleProps).to.have.property('Redirects').that.is.an('object') + expect(consoleProps).to.have.property('Resolved Url').that.equals('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_network_requests.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_network_requests.spec.ts index 1afecedf3cb8..dfe3e7f3df52 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_network_requests.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_network_requests.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin network requests', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -30,4 +32,43 @@ context('cy.origin network requests', () => { }) }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.request()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.request('http://www.foobar.com:3500/fixtures/example.json') + }) + + cy.shouldWithTimeout(() => { + const { consoleProps, renderProps } = findCrossOriginLogs('request', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('request') + + expect(consoleProps.Request).to.have.property('Request Body').that.equals(null) + expect(consoleProps.Request).to.have.property('Request Headers').that.is.a('object') + expect(consoleProps.Request).to.have.property('Request URL').that.equals('http://www.foobar.com:3500/fixtures/example.json') + expect(consoleProps.Request).to.have.property('Response Body').that.is.a('string') + expect(consoleProps.Request).to.have.property('Response Headers').that.is.a('object') + expect(consoleProps.Request).to.have.property('Response Status').that.equals(200) + + expect(consoleProps.Yielded).to.have.property('body').that.deep.equals({ example: true }) + expect(consoleProps.Yielded).to.have.property('duration').that.is.a('number') + expect(consoleProps.Yielded).to.have.property('headers').that.is.a('object') + expect(consoleProps.Yielded).to.have.property('status').that.equals(200) + + expect(renderProps).to.have.property('indicator').that.equals('successful') + expect(renderProps).to.have.property('message').that.equals('GET 200 http://www.foobar.com:3500/fixtures/example.json') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying.spec.ts index e22baad757ec..75fc0859bdb6 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin querying', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -29,4 +31,74 @@ context('cy.origin querying', () => { cy.root().should('match', 'html') }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.contains()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.contains('Nested Find') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('contains', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('contains') + expect(consoleProps['Applied To']).to.be.undefined + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Content).to.equal('Nested Find') + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('DIV') + expect(consoleProps.Yielded).to.have.property('id').that.equals('nested-find') + }) + }) + + it('.within()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').within(() => { + cy.get('#input') + }) + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('within', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('within') + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('FORM') + expect(consoleProps.Yielded).to.have.property('id').that.equals('by-id') + }) + }) + + it('.root()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.root() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('root', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('root') + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('HTML') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying_shadow.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying_shadow.spec.ts index a7ec229ebad2..3171d39ef553 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying_shadow.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_querying_shadow.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin shadow dom', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -10,4 +12,40 @@ context('cy.origin shadow dom', () => { .should('have.text', 'Shadow Content 1') }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.shadow()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#shadow-element-1').shadow() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('shadow', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('shadow') + expect(consoleProps.Elements).to.equal(1) + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('CY-TEST-ELEMENT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('shadow-element-1') + + expect(consoleProps.Yielded).to.be.null + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts index ecbeffff9919..dab3e9136dca 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts @@ -275,4 +275,47 @@ context('cy.origin screenshot', () => { }) }) }) + + context('#consoleProps', () => { + const { findCrossOriginLogs } = require('../../../../support/utils') + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="screenshots-link"]').click() + }) + + it('.screenshot()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.screenshot({ capture: 'fullPage' }) + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('screenshot', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('screenshot') + + expect(consoleProps).to.have.property('blackout') + expect(consoleProps).to.have.property('capture').that.equals('fullPage') + expect(consoleProps).to.have.property('dimensions').that.is.a('string') + expect(consoleProps).to.have.property('disableTimersAndAnimations').that.is.a('boolean') + expect(consoleProps).to.have.property('duration').that.is.a('string') + expect(consoleProps).to.have.property('multipart').that.is.a('boolean') + expect(consoleProps).to.have.property('name').to.be.null + expect(consoleProps).to.have.property('path').that.is.a('string') + expect(consoleProps).to.have.property('pixelRatio').that.is.a('number') + expect(consoleProps).to.have.property('scaled').that.is.a('boolean') + expect(consoleProps).to.have.property('size').that.is.a('string') + expect(consoleProps).to.have.property('specName').that.is.a('string') + expect(consoleProps).to.have.property('takenAt').that.is.a('string') + expect(consoleProps).to.have.property('testAttemptIndex').that.is.a('number') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_spies_stubs_clocks.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_spies_stubs_clocks.spec.ts index 34bfa8b9bf55..51929443d1b8 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_spies_stubs_clocks.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_spies_stubs_clocks.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin spies, stubs, and clock', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -80,4 +82,101 @@ context('cy.origin spies, stubs, and clock', () => { }) }) }) + + context('#consoleProps', () => { + const { _ } = Cypress + let logs: Map + + beforeEach(() => { + logs = new Map() + + // cy.clock only adds a log and does NOT update + cy.on('log:added', (attrs, log) => { + logs.set(attrs.id, log) + }) + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('spy()', () => { + cy.origin('http://foobar.com:3500', () => { + const foo = { bar () { } } + + cy.spy(foo, 'bar') + foo.bar() + expect(foo.bar).to.be.called + }) + + cy.shouldWithTimeout(() => { + const spyLog = findCrossOriginLogs('spy-1', logs, 'foobar.com') + + expect(spyLog.consoleProps.Command).to.equal('spy-1') + expect(spyLog.callCount).to.be.a('number') + expect(spyLog.functionName).to.equal('bar') + }) + }) + + it('.stub()', () => { + cy.origin('http://foobar.com:3500', () => { + const foo = { bar () { } } + + cy.stub(foo, 'bar') + foo.bar() + expect(foo.bar).to.be.called + }) + + cy.shouldWithTimeout(() => { + const stubLog = findCrossOriginLogs('stub-1', logs, 'foobar.com') + + expect(stubLog.consoleProps.Command).to.equal('stub-1') + expect(stubLog.callCount).to.be.a('number') + expect(stubLog.functionName).to.equal('bar') + }) + }) + + it('.clock()', () => { + cy.origin('http://foobar.com:3500', () => { + const now = Date.UTC(2022, 0, 12) + + cy.clock(now) + }) + + cy.shouldWithTimeout(() => { + const clockLog = findCrossOriginLogs('clock', logs, 'foobar.com') + + expect(clockLog.name).to.equal('clock') + + const consoleProps = clockLog.consoleProps() + + expect(consoleProps.Command).to.equal('clock') + expect(consoleProps).to.have.property('Methods replaced').that.is.a('object') + expect(consoleProps).to.have.property('Now').that.is.a('number') + }) + }) + + it('.tick()', () => { + cy.origin('http://foobar.com:3500', () => { + const now = Date.UTC(2022, 0, 12) + + cy.clock(now) + + cy.tick(10000) + }) + + cy.shouldWithTimeout(() => { + const tickLog = findCrossOriginLogs('tick', logs, 'foobar.com') + + expect(tickLog.name).to.equal('tick') + + const consoleProps = _.isFunction(tickLog.consoleProps) ? tickLog.consoleProps() : tickLog.consoleProps + + expect(consoleProps.Command).to.equal('tick') + expect(consoleProps).to.have.property('Methods replaced').that.is.a('object') + expect(consoleProps).to.have.property('Now').that.is.a('number') + expect(consoleProps).to.have.property('Ticked').that.is.a('string') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_traversal.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_traversal.spec.ts index 9e06a6acbd6c..a06101342991 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_traversal.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_traversal.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin traversal', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -112,4 +114,531 @@ context('cy.origin traversal', () => { cy.get('#input').siblings().should('have.length', 2) }) }) + + context('#consoleProps', () => { + const { _ } = Cypress + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.children()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').children() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('children', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id') + expect(consoleProps.Command).to.equal('children') + expect(consoleProps.Elements).to.equal(3) + expect(consoleProps.Selector).to.equal('') + expect(consoleProps.Yielded.length).to.equal(3) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('input') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('name') + expect(consoleProps.Yielded[2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[2]).to.have.property('id').that.equals('age') + }) + }) + + it('.closest()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').closest('form') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('closest', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id') + expect(consoleProps.Command).to.equal('closest') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('form') + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('FORM') + expect(consoleProps.Yielded).to.have.property('id').that.equals('by-id') + expect(consoleProps.Yielded.querySelector('input#input')).to.be.ok + expect(consoleProps.Yielded.querySelector('input#name')).to.be.ok + expect(consoleProps.Yielded.querySelector('input#age')).to.be.ok + }) + }) + + it('.eq()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id>input').eq(1) + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('eq', logs, 'foobar.com') + + expect(consoleProps['Applied To'].length).to.equal(3) + expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input') + expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name') + expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('eq') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('1') + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('name') + }) + }) + + it('.filter()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-name>input') + .filter('[name="dogs"]') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('filter', logs, 'foobar.com') + + expect(consoleProps['Applied To'].length).to.equal(12) + expect(consoleProps.Command).to.equal('filter') + expect(consoleProps.Elements).to.equal(4) + expect(consoleProps.Selector).to.equal('[name="dogs"]') + + expect(consoleProps.Yielded.length).to.equal(4) + + _.forEach(consoleProps.Yielded, (yielded) => { + expect(yielded).to.have.property('tagName').that.equals('INPUT') + expect(yielded).to.have.property('name').that.equals('dogs') + }) + }) + }) + + it('.find()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').find('input') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('find', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id') + + expect(consoleProps.Command).to.equal('find') + expect(consoleProps.Elements).to.equal(3) + expect(consoleProps.Selector).to.equal('input') + + expect(consoleProps.Yielded.length).to.equal(3) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('input') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('name') + expect(consoleProps.Yielded[2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[2]).to.have.property('id').that.equals('age') + }) + }) + + it('.first()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id>input').first() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('first', logs, 'foobar.com') + + expect(consoleProps['Applied To'].length).to.equal(3) + expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input') + expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name') + expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('first') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('input') + }) + }) + + it('.last()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id>input').last() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('last', logs, 'foobar.com') + + expect(consoleProps['Applied To'].length).to.equal(3) + expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input') + expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name') + expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('last') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('age') + }) + }) + + it('.next()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#input').next() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('next', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('input') + + expect(consoleProps.Command).to.equal('next') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('name') + }) + }) + + it('.nextAll()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#input').nextAll() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('nextAll', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('input') + + expect(consoleProps.Command).to.equal('nextAll') + expect(consoleProps.Elements).to.equal(2) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded.length).to.equal(2) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('name') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('age') + }) + }) + + it('.nextUntil()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#input').nextUntil('#age') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('nextUntil', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('input') + + expect(consoleProps.Command).to.equal('nextUntil') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('#age') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('name') + }) + }) + + it('.not()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id>input').not('#age') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('not', logs, 'foobar.com') + + expect(consoleProps['Applied To'].length).to.equal(3) + expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input') + expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name') + expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('not') + expect(consoleProps.Elements).to.equal(2) + expect(consoleProps.Selector).to.equal('#age') + + expect(consoleProps.Yielded.length).to.equal(2) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('input') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('name') + }) + }) + + it('.parent()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').parent() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('parent', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id') + + expect(consoleProps.Command).to.equal('parent') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('DIV') + expect(consoleProps.Yielded).to.have.property('id').that.equals('dom') + }) + }) + + it('.parents()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').parents() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('parents', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id') + + expect(consoleProps.Command).to.equal('parents') + expect(consoleProps.Elements).to.equal(3) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded.length).to.equal(3) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('DIV') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('dom') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('BODY') + expect(consoleProps.Yielded[2]).to.have.property('tagName').that.equals('HTML') + }) + }) + + it('.parentsUntil()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#by-id').parentsUntil('body') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('parentsUntil', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id') + + expect(consoleProps.Command).to.equal('parentsUntil') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('body') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('DIV') + expect(consoleProps.Yielded).to.have.property('id').that.equals('dom') + }) + }) + + it('.prev()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#age').prev() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('prev', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('prev') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('name') + }) + }) + + it('.prevAll()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#age').prevAll() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('prevAll', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('prevAll') + expect(consoleProps.Elements).to.equal(2) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded.length).to.equal(2) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('name') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('input') + }) + }) + + it('.prevUntil()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#age').prevUntil('#input') + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('prevUntil', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('age') + + expect(consoleProps.Command).to.equal('prevUntil') + expect(consoleProps.Elements).to.equal(1) + expect(consoleProps.Selector).to.equal('#input') + + expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded).to.have.property('id').that.equals('name') + }) + }) + + it('.siblings()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get('#input').siblings() + }) + + cy.shouldWithTimeout(() => { + // in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions + // set to context to undefined to run the assertions + if (Cypress.isBrowser('firefox')) { + cy.state('document', undefined) + } + + const { consoleProps } = findCrossOriginLogs('siblings', logs, 'foobar.com') + + expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps['Applied To']).to.have.property('id').that.equals('input') + + expect(consoleProps.Command).to.equal('siblings') + expect(consoleProps.Elements).to.equal(2) + expect(consoleProps.Selector).to.equal('') + + expect(consoleProps.Yielded.length).to.equal(2) + expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('name') + expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT') + expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('age') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_viewport.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_viewport.spec.ts index e49a346b961a..cfe34498fda2 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_viewport.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_viewport.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin viewport', () => { it('syncs the viewport from the primary to secondary', () => { // change the viewport in the primary first @@ -178,5 +180,31 @@ context('cy.origin viewport', () => { }) }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.viewport()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.viewport(320, 480) + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('viewport', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('viewport') + expect(consoleProps.Width).to.equal(320) + expect(consoleProps.Height).to.equal(480) + }) + }) + }) }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts index 54a7f51bcb7c..67f188e603e7 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin waiting', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -9,4 +11,29 @@ context('cy.origin waiting', () => { cy.wait(500) }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.wait()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.wait(200) + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('wait', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('wait') + expect(consoleProps).to.have.property('Waited For').to.equal('200ms before continuing') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_window.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_window.spec.ts index 95b56f279d36..84d7bfc03708 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_window.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_window.spec.ts @@ -1,3 +1,5 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + context('cy.origin window', () => { beforeEach(() => { cy.visit('/fixtures/multi-domain.html') @@ -21,4 +23,55 @@ context('cy.origin window', () => { cy.title().should('include', 'DOM Fixture') }) }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.window()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.window() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('window', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('window') + expect(consoleProps.Yielded).to.be.null + }) + }) + + it('.document()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.document() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('document', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('document') + expect(consoleProps.Yielded).to.be.null + }) + }) + + it('.title()', () => { + cy.origin('http://foobar.com:3500', () => { + cy.title() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('title', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('title') + expect(consoleProps.Yielded).to.equal('DOM Fixture') + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/util/serialization_spec.ts b/packages/driver/cypress/integration/util/serialization_spec.ts new file mode 100644 index 000000000000..b8c74725223b --- /dev/null +++ b/packages/driver/cypress/integration/util/serialization_spec.ts @@ -0,0 +1,688 @@ +import { reifyDomElement, preprocessDomElement, preprocessLogLikeForSerialization, preprocessLogForSerialization, reifyLogFromSerialization } from '../../../src/util/serialization/log' + +describe('Log Serialization', () => { + const buildSnapshot = (innerSnapshotElement) => { + const mockSnapshot = document.createElement('body') + + // populate some items into the mockSnapshot that would mimic what the DOM might actually look like, along with our inner snapshot element + const mockContainer = document.createElement('div') + const mockInnerHeader = document.createElement('h1') + const mockTextNode = document.createTextNode('Mock Snapshot Header') + + mockInnerHeader.appendChild(mockTextNode) + mockContainer.appendChild(mockInnerHeader) + mockContainer.appendChild(innerSnapshotElement) + + mockSnapshot.appendChild(mockContainer) + + return mockSnapshot + } + + it('preprocesses complex log-like data structures by preprocessing log DOM elements and table functions', () => { + const mockSpan = document.createElement('span') + + mockSpan.innerHTML = 'click button' + + const mockButton = document.createElement('button') + + mockButton.appendChild(mockSpan) + + const mockClickedElement = document.createElement('form') + + mockClickedElement.appendChild(mockButton) + mockClickedElement.id = 'button-inside-a' + + const mockSnapshot = buildSnapshot(mockClickedElement) + + const mockSnapshots = ['before', 'after'].map((snapshotName) => { + return { + name: snapshotName, + htmlAttrs: {}, + body: { + get: () => Cypress.$(mockSnapshot), + }, + } + }) + + // mockLogAttrs should look just like log attributes that are emitted from log:changed/log:added events. This example is what a 'click' log may look like + const mockLogAttrs = { + $el: Cypress.$(mockClickedElement), + alias: undefined, + chainerId: 'mock-chainer-id', + consoleProps: { + ['Applied To']: mockClickedElement, + Command: 'click', + Coords: { + x: 100, + y: 50, + }, + Options: undefined, + Yielded: undefined, + table: { + 1: () => { + return { + name: 'Mouse Events', + // NOTE: click data length is truncated for test readability + data: [ + { + 'Active Modifiers': null, + 'Event Type': 'pointerover', + 'Prevented Default': null, + 'Stopped Propagation': null, + 'Target Element': mockClickedElement, + }, + { + 'Active Modifiers': null, + 'Event Type': 'mouseover', + 'Prevented Default': null, + 'Stopped Propagation': null, + 'Target Element': mockClickedElement, + }, + ], + } + }, + }, + }, + coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 }, + ended: true, + err: undefined, + event: false, + highlightAttr: 'data-cypress-el', + hookId: 'r4', + id: 'mock-log-id', + instrument: 'command', + message: '', + name: 'click', + numElements: 1, + referencesAlias: undefined, + renderProps: {}, + snapshots: mockSnapshots, + state: 'passed', + testCurrentRetry: 0, + testId: 'r4', + timeout: 4000, + type: 'child', + url: 'http://www.foobar.com', + viewportHeight: 660, + viewportWidth: 1000, + visible: true, + wallClockStartedAt: '2022-04-18T21:52:37.833Z', + } + + const { consoleProps, snapshots, $el, ...logAttrs } = preprocessLogForSerialization(mockLogAttrs) + + expect(logAttrs).to.deep.equal({ + alias: undefined, + chainerId: 'mock-chainer-id', + coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 }, + ended: true, + err: undefined, + event: false, + highlightAttr: 'data-cypress-el', + hookId: 'r4', + id: 'mock-log-id', + instrument: 'command', + message: '', + name: 'click', + numElements: 1, + referencesAlias: undefined, + renderProps: {}, + state: 'passed', + testCurrentRetry: 0, + testId: 'r4', + timeout: 4000, + type: 'child', + url: 'http://www.foobar.com', + viewportHeight: 660, + viewportWidth: 1000, + visible: true, + wallClockStartedAt: '2022-04-18T21:52:37.833Z', + }) + + expect($el).to.deep.equal([ + { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + ]) + + expect(consoleProps).to.deep.equal({ + ['Applied To']: { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + Command: 'click', + Coords: { + x: 100, + y: 50, + }, + Options: undefined, + Yielded: undefined, + table: { + 1: { + serializationKey: 'function', + value: { + name: 'Mouse Events', + data: [ + { + 'Active Modifiers': null, + 'Event Type': 'pointerover', + 'Prevented Default': null, + 'Stopped Propagation': null, + 'Target Element': { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + }, + { + 'Active Modifiers': null, + 'Event Type': 'mouseover', + 'Prevented Default': null, + 'Stopped Propagation': null, + 'Target Element': { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + }, + ], + }, + }, + }, + }) + + expect(snapshots).to.deep.equal([ + { + name: 'before', + htmlAttrs: {}, + styles: {}, + body: { + get: { + serializationKey: 'function', + value: [{ + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + serializationKey: 'dom', + tagName: 'BODY', + }], + }, + }, + }, + { + name: 'after', + htmlAttrs: {}, + styles: {}, + body: { + get: { + serializationKey: 'function', + value: [{ + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + serializationKey: 'dom', + tagName: 'BODY', + }], + }, + }, + }, + ]) + }) + + it('reifies complex log-like data structures by reifying serialized DOM elements and table functions back into native data types, respectively', () => { + // this should log identical to the test output above from what a preprocessed click log looks like after postMessage() + const mockPreprocessedLogAttrs = { + $el: [ + { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + ], + alias: undefined, + chainerId: 'mock-chainer-id', + consoleProps: { + ['Applied To']: { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + Command: 'click', + Coords: { + x: 100, + y: 50, + }, + Options: undefined, + Yielded: undefined, + table: { + 1: { + serializationKey: 'function', + value: { + name: 'Mouse Events', + // NOTE: click data length is truncated for test readability + data: [ + { + 'Active Modifiers': null, + 'Event Type': 'pointerover', + 'Prevented Default': null, + 'Stopped Propagation': null, + 'Target Element': { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + }, + { + 'Active Modifiers': null, + 'Event Type': 'mouseover', + 'Prevented Default': null, + 'Stopped Propagation': null, + 'Target Element': { + attributes: { + id: 'button-inside-a', + }, + innerHTML: '', + serializationKey: 'dom', + tagName: 'FORM', + }, + }, + ], + }, + }, + }, + }, + coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 }, + ended: true, + err: undefined, + event: false, + highlightAttr: 'data-cypress-el', + hookId: 'r4', + id: 'mock-log-id', + instrument: 'command', + message: '', + name: 'click', + numElements: 1, + referencesAlias: undefined, + renderProps: {}, + snapshots: [ + { + name: 'before', + htmlAttrs: {}, + styles: {}, + body: { + get: { + serializationKey: 'function', + value: [{ + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + serializationKey: 'dom', + tagName: 'BODY', + }], + }, + }, + }, + { + name: 'after', + htmlAttrs: {}, + styles: {}, + body: { + get: { + serializationKey: 'function', + value: [{ + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + serializationKey: 'dom', + tagName: 'BODY', + }], + }, + }, + }, + ], + state: 'passed', + testCurrentRetry: 0, + testId: 'r4', + timeout: 4000, + type: 'child', + url: 'http://www.foobar.com', + viewportHeight: 660, + viewportWidth: 1000, + visible: true, + wallClockStartedAt: '2022-04-18T21:52:37.833Z', + } + + const { consoleProps, snapshots, $el, ...logAttrs } = reifyLogFromSerialization(mockPreprocessedLogAttrs) + + expect(logAttrs).to.deep.equal({ + alias: undefined, + chainerId: 'mock-chainer-id', + coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 }, + ended: true, + err: undefined, + event: false, + highlightAttr: 'data-cypress-el', + hookId: 'r4', + id: 'mock-log-id', + instrument: 'command', + message: '', + name: 'click', + numElements: 1, + referencesAlias: undefined, + renderProps: {}, + state: 'passed', + testCurrentRetry: 0, + testId: 'r4', + timeout: 4000, + type: 'child', + url: 'http://www.foobar.com', + viewportHeight: 660, + viewportWidth: 1000, + visible: true, + wallClockStartedAt: '2022-04-18T21:52:37.833Z', + }) + + expect($el.jquery).to.be.ok + expect($el.length).to.equal(1) + expect($el[0]).to.be.instanceOf(HTMLFormElement) + expect($el[0].id).to.equal('button-inside-a') + expect($el[0].textContent).to.equal('click button') + + // most of the consoleProps logic is tested in the e2e/multi-domain folder. focus in this test will be mostly snapshot serialization + expect(consoleProps['Applied To']).to.be.instanceOf(HTMLFormElement) + expect(consoleProps['Applied To']).to.have.property('id').that.equals('button-inside-a') + expect(consoleProps['Applied To']).to.have.property('textContent').that.equals('click button') + + expect(consoleProps.table).to.have.property('1') + expect(consoleProps.table[1]).to.be.a('function') + + expect(snapshots).to.have.lengthOf(2) + + expect(snapshots[0]).to.have.property('name').that.equals('before') + expect(snapshots[0]).to.have.property('htmlAttrs').that.deep.equals({}) + // styles should now live in the CSS map after a snapshot is processed through createSnapshot and snapshots should exist in document map + expect(snapshots[0]).to.not.have.property('styles') + expect(snapshots[0]).to.have.property('body').that.has.property('get').that.is.a('function') + + const snapshotBodyBefore = snapshots[0].body.get() + + expect(snapshotBodyBefore.length).to.equal(1) + + expect(snapshotBodyBefore[0]).to.be.instanceOf(HTMLBodyElement) + // verify to some degree that the reified elements above can be matched into the snapshot + expect(snapshotBodyBefore[0].querySelector('form#button-inside-a')).to.be.instanceOf(HTMLFormElement) + + expect(snapshots[1]).to.have.property('name').that.equals('after') + expect(snapshots[1]).to.have.property('htmlAttrs').that.deep.equals({}) + expect(snapshots[1]).to.not.have.property('styles') + expect(snapshots[1]).to.have.property('body').that.has.property('get').that.is.a('function') + + const snapshotBodyAfter = snapshots[1].body.get() + + expect(snapshotBodyAfter.length).to.equal(1) + + expect(snapshotBodyAfter[0]).to.be.instanceOf(HTMLBodyElement) + // verify to some degree that the reified elements above can be matched into the snapshot + expect(snapshotBodyAfter[0].querySelector('form#button-inside-a')).to.be.instanceOf(HTMLFormElement) + }) + + // purpose of these 'DOM Elements' tests is to give a very basic understanding of how DOM element serialization works in the log serializer + context('DOM Elements- preprocesses/reifies a given DOM element with stateful', () => { + context('input', () => { + it('preprocess', () => { + const inputElement = document.createElement('input') + + inputElement.type = 'text' + inputElement.value = 'foo' + inputElement.setAttribute('data-cy', 'bar') + + const snapshot = buildSnapshot(inputElement) + + snapshot.setAttribute('foo', 'bar') + + const preprocessedSnapshot = preprocessDomElement(snapshot) + + expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY') + expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom') + expect(preprocessedSnapshot).to.have.property('attributes').that.deep.equals({ + foo: 'bar', + }) + + expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`

Mock Snapshot Header

`) + }) + + it('reifies', () => { + const preprocessedSnapshot = { + tagName: 'BODY', + serializationKey: 'dom', + attributes: { + foo: 'bar', + }, + innerHTML: `

Mock Snapshot Header

`, + } + + const reifiedSnapshot = reifyDomElement(preprocessedSnapshot) + + expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement) + expect(reifiedSnapshot.getAttribute('foo')).to.equal('bar') + expect(reifiedSnapshot.querySelector('input[type="text"][value="foo"][data-cy="bar"]')).to.be.instanceOf(HTMLInputElement) + }) + }) + + context('select', () => { + it('preprocess', () => { + const selectElement = document.createElement('select') + + selectElement.id = 'metasyntactic-variables' + selectElement.name = 'Metasyntactic Variables' + + const options = ['Hank Hill', 'Buck Strickland', 'Donna', 'Old Donna'].map((val) => { + const option = document.createElement('option') + + option.value = val + + return option + }) + + options.forEach((option) => selectElement.appendChild(option)) + + selectElement.selectedIndex = 1 + + const snapshot = buildSnapshot(selectElement) + + const preprocessedSnapshot = preprocessDomElement(snapshot) + + expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY') + expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom') + expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`

Mock Snapshot Header

`) + }) + + it('reifies', () => { + const preprocessedSnapshot = { + tagName: 'BODY', + serializationKey: 'dom', + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + } + + const reifiedSnapshot = reifyDomElement(preprocessedSnapshot) + + expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement) + expect(reifiedSnapshot.querySelector('select#metasyntactic-variables option[selected]')).to.have.property('value').that.equals('Buck Strickland') + }) + }) + + context('textarea', () => { + it('preprocess', () => { + const textAreaElement = document.createElement('textarea') + + textAreaElement.rows = 4 + textAreaElement.cols = 20 + textAreaElement.value = 'Generic variable names that function as placeholders' + + const snapshot = buildSnapshot(textAreaElement) + + const preprocessedSnapshot = preprocessDomElement(snapshot) + + expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY') + expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom') + expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`

Mock Snapshot Header

`) + }) + + it('reifies', () => { + const preprocessedSnapshot = { + tagName: 'BODY', + serializationKey: 'dom', + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + } + + const reifiedSnapshot = reifyDomElement(preprocessedSnapshot) + + expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement) + expect(reifiedSnapshot.querySelector('textarea[rows="4"]')).to.have.property('textContent').that.equals('Generic variable names that function as placeholders') + }) + }) + + context('radio', () => { + it('preprocess', () => { + const formElement = document.createElement('form') + + const radioInputs = ['foo', 'bar', 'baz'].map((val) => { + const radioInput = document.createElement('input') + + radioInput.type = 'radio' + radioInput.value = val + + return radioInput + }) + + radioInputs[1].checked = true + + radioInputs.forEach((radioInput) => formElement.appendChild(radioInput)) + + const snapshot = buildSnapshot(formElement) + + const preprocessedSnapshot = preprocessDomElement(snapshot) + + expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY') + expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom') + expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`

Mock Snapshot Header

`) + }) + + it('reifies', () => { + const preprocessedSnapshot = { + tagName: 'BODY', + serializationKey: 'dom', + attributes: {}, + innerHTML: `

Mock Snapshot Header

`, + } + + const reifiedSnapshot = reifyDomElement(preprocessedSnapshot) + + expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement) + expect(reifiedSnapshot.querySelector('form input[value="bar"]')).to.have.property('checked').that.equals(true) + }) + }) + + context('checkbox', () => { + it('preprocess', () => { + const formElement = document.createElement('form') + + const checkboxInputs = ['foo', 'bar', 'bar'].map((val) => { + const checkboxInput = document.createElement('input') + + checkboxInput.type = 'checkbox' + checkboxInput.value = val + + return checkboxInput + }) + + checkboxInputs[1].checked = true + + checkboxInputs.forEach((checkboxInput) => formElement.appendChild(checkboxInput)) + + const snapshot = buildSnapshot(formElement) + + const preprocessedSnapshot = preprocessDomElement(snapshot) + + expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY') + expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom') + expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`

Mock Snapshot Header

`) + }) + + it('reifies', () => { + const preprocessedSnapshot = { + tagName: 'BODY', + serializationKey: 'dom', + attributes: {}, + innerHTML: `"

Mock Snapshot Header

"`, + } + + const reifiedSnapshot = reifyDomElement(preprocessedSnapshot) + + expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement) + expect(reifiedSnapshot.querySelector('form input[value="bar"]')).to.have.property('checked').that.equals(true) + }) + }) + }) + + // purpose of these 'DOM Elements' tests is to give a very basic understanding of how DOM element serialization works in the log serializer + context('Functions', () => { + it('does NOT try to serialize a function unless `attemptToSerializeFunctions` is set to true', () => { + const serializedFunction = preprocessLogLikeForSerialization(() => 'foo') + + expect(serializedFunction).to.be.null + }) + + it('Tries to serialize EXPLICIT/KNOWN serializable functions by setting `attemptToSerializeFunctions` to true', () => { + const functionContents = [ + 'foo', + { + bar: 'baz', + }, + document.createElement('html'), + ] + + const myKnownSerializableFunction = () => functionContents + + const serializedFunction = preprocessLogLikeForSerialization(myKnownSerializableFunction, true) + + expect(serializedFunction).to.deep.equal({ + serializationKey: 'function', + value: [ + 'foo', + { + bar: 'baz', + }, + { + tagName: 'HTML', + serializationKey: 'dom', + attributes: {}, + innerHTML: '', + }, + ], + }) + }) + }) +}) diff --git a/packages/driver/cypress/support/utils.js b/packages/driver/cypress/support/utils.js index c358c3d657c8..8ae506576458 100644 --- a/packages/driver/cypress/support/utils.js +++ b/packages/driver/cypress/support/utils.js @@ -72,6 +72,20 @@ export const assertLogLength = (logs, expectedLength) => { expect(logs.length).to.eq(expectedLength, `received ${logs.length} logs when we expected ${expectedLength}: [${receivedLogs}]`) } +export const findCrossOriginLogs = (consolePropCommand, logMap, matchingOrigin) => { + const matchedLogs = Array.from(logMap.values()).filter((log) => { + const props = log.get() + + let consoleProps = _.isFunction(props?.consoleProps) ? props.consoleProps() : props?.consoleProps + + return consoleProps.Command === consolePropCommand && props.id.includes(matchingOrigin) + }) + + const logAttrs = matchedLogs.map((log) => log.get()) + + return logAttrs.length === 1 ? logAttrs[0] : logAttrs +} + export const attachListeners = (listenerArr) => { return (els) => { _.each(els, (el, elName) => { @@ -94,6 +108,10 @@ const getAllFn = (...aliases) => { ) } +const shouldWithTimeout = (cb, timeout = 250) => { + cy.wrap({}, { timeout }).should(cb) +} + export const keyEvents = [ 'keydown', 'keyup', @@ -120,6 +138,8 @@ export const expectCaret = (start, end) => { Cypress.Commands.add('getAll', getAllFn) +Cypress.Commands.add('shouldWithTimeout', shouldWithTimeout) + const chaiSubset = require('chai-subset') chai.use(chaiSubset) diff --git a/packages/driver/src/cy/snapshots.ts b/packages/driver/src/cy/snapshots.ts index 6d8edd3a421b..9d0a05328101 100644 --- a/packages/driver/src/cy/snapshots.ts +++ b/packages/driver/src/cy/snapshots.ts @@ -5,6 +5,8 @@ import { create as createSnapshotsCSS } from './snapshots_css' export const HIGHLIGHT_ATTR = 'data-cypress-el' +export const FINAL_SNAPSHOT_NAME = 'final state' + export const create = ($$, state) => { const snapshotsCss = createSnapshotsCSS($$, state) const snapshotsMap = new WeakMap() @@ -99,15 +101,20 @@ export const create = ($$, state) => { } const getStyles = (snapshot) => { - const styleIds = snapshotsMap.get(snapshot) + const { ids, styles } = snapshotsMap.get(snapshot) || {} - if (!styleIds) { + if (!ids && !styles) { return {} } + // If a cross origin processed snapshot, styles are directly added into the CSS map. Simply return them. + if (styles?.headStyles || styles?.bodyStyles) { + return styles + } + return { - headStyles: snapshotsCss.getStylesByIds(styleIds.headStyleIds), - bodyStyles: snapshotsCss.getStylesByIds(styleIds.bodyStyleIds), + headStyles: snapshotsCss.getStylesByIds(ids?.headStyleIds), + bodyStyles: snapshotsCss.getStylesByIds(ids?.bodyStyleIds), } } @@ -119,20 +126,24 @@ export const create = ($$, state) => { $body.find('script,link[rel="stylesheet"],style').remove() const snapshot = { - name: 'final state', + name: FINAL_SNAPSHOT_NAME, htmlAttrs, body: { get: () => $body.detach(), }, } - snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds }) + snapshotsMap.set(snapshot, { + ids: { + headStyleIds, + bodyStyleIds, + }, + }) return snapshot } - const createSnapshot = (name, $elToHighlight) => { - Cypress.action('cy:snapshot', name) + const createSnapshotBody = ($elToHighlight) => { // create a unique selector for this el // but only IF the subject is truly an element. For example // we might be wrapping a primitive like "$([1, 2]).first()" @@ -141,64 +152,89 @@ export const create = ($$, state) => { // jQuery v3 runs in strict mode and throws an error if you attempt to set a property // TODO: in firefox sometimes this throws a cross-origin access error - try { - const isJqueryElement = $dom.isElement($elToHighlight) && $dom.isJquery($elToHighlight) + const isJqueryElement = $dom.isElement($elToHighlight) && $dom.isJquery($elToHighlight) - if (isJqueryElement) { - ($elToHighlight as JQuery).attr(HIGHLIGHT_ATTR, 'true') - } + if (isJqueryElement) { + ($elToHighlight as JQuery).attr(HIGHLIGHT_ATTR, 'true') + } - // TODO: throw error here if cy is undefined! - - // cloneNode can actually trigger functions attached to custom elements - // so we have to use importNode to clone the element - // https://github.com/cypress-io/cypress/issues/7187 - // https://github.com/cypress-io/cypress/issues/1068 - // we import it to a transient document (snapshotDocument) so that there - // are no side effects from cloning it. see below for how we re-attach - // it to the AUT document - // https://github.com/cypress-io/cypress/issues/8679 - // this can fail if snapshotting before the page has fully loaded, - // so we catch this below and return null for the snapshot - // https://github.com/cypress-io/cypress/issues/15816 - const $body = $$(snapshotDocument.importNode($$('body')[0], true)) - - // for the head and body, get an array of all CSS, - // whether it's links or style tags - // if it's same-origin, it will get the actual styles as a string - // it it's cross-origin, it will get a reference to the link's href - const { headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds() - - // replaces iframes with placeholders - replaceIframes($body) - - // remove tags we don't want in body - $body.find('script,link[rel=\'stylesheet\'],style').remove() - - // here we need to figure out if we're in a remote manual environment - // if so we need to stringify the DOM: - // 1. grab all inputs / textareas / options and set their value on the element - // 2. convert DOM to string: body.prop("outerHTML") - // 3. send this string via websocket to our server - // 4. server rebroadcasts this to our client and its stored as a property - - // its also possible for us to store the DOM string completely on the server - // without ever sending it back to the browser (until its requests). - // we could just store it in memory and wipe it out intelligently. - // this would also prevent having to store the DOM structure on the client, - // which would reduce memory, and some CPU operations - - // now remove it after we clone - if (isJqueryElement) { - ($elToHighlight as JQuery).removeAttr(HIGHLIGHT_ATTR) - } + // TODO: throw error here if cy is undefined! + + // cloneNode can actually trigger functions attached to custom elements + // so we have to use importNode to clone the element + // https://github.com/cypress-io/cypress/issues/7187 + // https://github.com/cypress-io/cypress/issues/1068 + // we import it to a transient document (snapshotDocument) so that there + // are no side effects from cloning it. see below for how we re-attach + // it to the AUT document + // https://github.com/cypress-io/cypress/issues/8679 + // this can fail if snapshotting before the page has fully loaded, + // so we catch this below and return null for the snapshot + // https://github.com/cypress-io/cypress/issues/15816 + const $body = $$(snapshotDocument.importNode($$('body')[0], true)) + // for the head and body, get an array of all CSS, + // whether it's links or style tags + // if it's same-origin, it will get the actual styles as a string + // if it's cross-origin, it will get a reference to the link's href + const { headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds() + + // replaces iframes with placeholders + replaceIframes($body) + + // remove tags we don't want in body + $body.find('script,link[rel=\'stylesheet\'],style').remove() + + // here we need to figure out if we're in a remote manual environment + // if so we need to stringify the DOM: + // 1. grab all inputs / textareas / options and set their value on the element + // 2. convert DOM to string: body.prop("outerHTML") + // 3. send this string via websocket to our server + // 4. server rebroadcasts this to our client and its stored as a property + + // its also possible for us to store the DOM string completely on the server + // without ever sending it back to the browser (until its requests). + // we could just store it in memory and wipe it out intelligently. + // this would also prevent having to store the DOM structure on the client, + // which would reduce memory, and some CPU operations + + // now remove it after we clone + if (isJqueryElement) { + ($elToHighlight as JQuery).removeAttr(HIGHLIGHT_ATTR) + } + + const $htmlAttrs = getHtmlAttrs($$('html')[0]) + + return { + $body, + $htmlAttrs, + headStyleIds, + bodyStyleIds, + } + } + + const reifySnapshotBody = (preprocessedSnapshot) => { + const $body = preprocessedSnapshot.body.get() + const $htmlAttrs = preprocessedSnapshot.htmlAttrs + const { headStyles, bodyStyles } = preprocessedSnapshot.styles + + return { + $body, + $htmlAttrs, + headStyles, + bodyStyles, + } + } + + const createSnapshot = (name, $elToHighlight, preprocessedSnapshot) => { + Cypress.action('cy:snapshot', name) + + try { + const { + $body, + $htmlAttrs, + ...styleAttrs + } = preprocessedSnapshot ? reifySnapshotBody(preprocessedSnapshot) : createSnapshotBody($elToHighlight) - // preserve attributes on the tag - const htmlAttrs = getHtmlAttrs($$('html')[0]) - // the body we clone via importNode above is attached to a transient document - // so that there are no side effects from cloning it. we only attach it back - // to the AUT document at the last moment (when restoring the snapshot) - // https://github.com/cypress-io/cypress/issues/8679 let attachedBody const body = { get: () => { @@ -212,11 +248,38 @@ export const create = ($$, state) => { const snapshot = { name, - htmlAttrs, + htmlAttrs: $htmlAttrs, body, } - snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds }) + const { + headStyleIds, + bodyStyleIds, + headStyles, + bodyStyles, + }: { + headStyleIds?: string[] + bodyStyleIds?: string[] + headStyles?: string[] + bodyStyles?: string[] + } = styleAttrs + + if (headStyleIds && bodyStyleIds) { + snapshotsMap.set(snapshot, { + ids: { + headStyleIds, + bodyStyleIds, + }, + }) + } else if (headStyles && bodyStyles) { + // The Snapshot is being reified from cross origin. Get inline styles of reified snapshot. + snapshotsMap.set(snapshot, { + styles: { + headStyles, + bodyStyles, + }, + }) + } return snapshot } catch (e) { diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 66a00e464af0..da408e891170 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -444,7 +444,7 @@ class Log { this.obj = { highlightAttr: HIGHLIGHT_ATTR, numElements: $el.length, - visible: $el.length === $el.filter(':visible').length, + visible: this.get('visible') ?? $el.length === $el.filter(':visible').length, } return this.set(this.obj, { silent: true }) @@ -505,8 +505,11 @@ class Log { consoleObj[key] = _this.get('name') + // in the case a log is being recreated from the cross-origin spec bridge to the primary, consoleProps may be an Object + const consoleObjDefaults = _.isFunction(consoleProps) ? consoleProps.apply(this, args) : consoleProps + // merge in the other properties from consoleProps - _.extend(consoleObj, consoleProps.apply(this, args)) + _.extend(consoleObj, consoleObjDefaults) // TODO: right here we need to automatically // merge in "Yielded + Element" if there is an $el diff --git a/packages/driver/src/multi-domain/communicator.ts b/packages/driver/src/multi-domain/communicator.ts index 3d8df0e4b537..c9f8cfa3e97f 100644 --- a/packages/driver/src/multi-domain/communicator.ts +++ b/packages/driver/src/multi-domain/communicator.ts @@ -3,10 +3,13 @@ import { EventEmitter } from 'events' import { preprocessConfig, preprocessEnv } from '../util/config' import { preprocessForSerialization, reifySerializedError } from '../util/serialization' import { $Location } from '../cypress/location' +import { preprocessLogForSerialization, reifyLogFromSerialization, preprocessSnapshotForSerialization, reifySnapshotFromSerialization } from '../util/serialization/log' const debug = debugFn('cypress:driver:multi-origin') const CROSS_ORIGIN_PREFIX = 'cross:origin:' +const LOG_EVENTS = [`${CROSS_ORIGIN_PREFIX}log:added`, `${CROSS_ORIGIN_PREFIX}log:changed`] +const FINAL_SNAPSHOT_EVENT = `${CROSS_ORIGIN_PREFIX}final:snapshot:generated` /** * Primary Origin communicator. Responsible for sending/receiving events throughout @@ -42,6 +45,16 @@ export class PrimaryOriginCommunicator extends EventEmitter { this.crossOriginDriverWindows[data.originPolicy] = source as Window } + // reify any logs coming back from the cross-origin spec bridges to serialize snapshot/consoleProp DOM elements as well as select functions. + if (LOG_EVENTS.includes(data?.event)) { + data.data = reifyLogFromSerialization(data.data as any) + } + + // reify the final snapshot coming back from the secondary domain if requested by the runner. + if (FINAL_SNAPSHOT_EVENT === data?.event) { + data.data = reifySnapshotFromSerialization(data.data as any) + } + if (data?.data?.err) { data.data.err = reifySerializedError(data.data.err, this.userInvocationStack as string) } @@ -171,13 +184,24 @@ export class SpecBridgeCommunicator extends EventEmitter { */ toPrimary (event: string, data?: Cypress.ObjectLike, options: { syncGlobals: boolean } = { syncGlobals: false }) { const { originPolicy } = $Location.create(window.location.href) + const eventName = `${CROSS_ORIGIN_PREFIX}${event}` + + // Preprocess logs before sending through postMessage() to attempt to serialize some DOM nodes and functions. + if (LOG_EVENTS.includes(eventName)) { + data = preprocessLogForSerialization(data as any) + } + + // If requested by the runner, preprocess the final snapshot before sending through postMessage() to attempt to serialize the DOM body of the snapshot. + if (FINAL_SNAPSHOT_EVENT === eventName) { + data = preprocessSnapshotForSerialization(data as any) + } debug('<= to Primary ', event, data, originPolicy) if (options.syncGlobals) this.syncGlobalsToPrimary() this.handleSubjectAndErr(data, (data: Cypress.ObjectLike) => { window.top?.postMessage({ - event: `${CROSS_ORIGIN_PREFIX}${event}`, + event: eventName, data, originPolicy, }, '*') diff --git a/packages/driver/src/multi-domain/cypress.ts b/packages/driver/src/multi-domain/cypress.ts index ad485f391412..5a8d8cfb9425 100644 --- a/packages/driver/src/multi-domain/cypress.ts +++ b/packages/driver/src/multi-domain/cypress.ts @@ -6,10 +6,12 @@ import '../config/lodash' import $Cypress from '../cypress' import { $Cy } from '../cypress/cy' +import { $Location } from '../cypress/location' import $Commands from '../cypress/commands' import { create as createLog } from '../cypress/log' import { bindToListeners } from '../cy/listeners' import { handleOriginFn } from './domain_fn' +import { FINAL_SNAPSHOT_NAME } from '../cy/snapshots' import { handleLogs } from './events/logs' import { handleSocketEvents } from './events/socket' import { handleSpecWindowEvents } from './events/spec_window' @@ -30,6 +32,18 @@ const createCypress = () => { setup(config, env) }) + Cypress.specBridgeCommunicator.on('generate:final:snapshot', (snapshotUrl: string) => { + const currentAutOriginPolicy = cy.state('autOrigin') + const requestedSnapshotUrlLocation = $Location.create(snapshotUrl) + + if (requestedSnapshotUrlLocation.originPolicy === currentAutOriginPolicy) { + // if true, this is the correct specbridge to take the snapshot and send it back + const finalSnapshot = cy.createSnapshot(FINAL_SNAPSHOT_NAME) + + Cypress.specBridgeCommunicator.toPrimary('final:snapshot:generated', finalSnapshot) + } + }) + Cypress.specBridgeCommunicator.toPrimary('bridge:ready') } diff --git a/packages/driver/src/multi-domain/events/logs.ts b/packages/driver/src/multi-domain/events/logs.ts index 1a7973c375d6..c686c5004020 100644 --- a/packages/driver/src/multi-domain/events/logs.ts +++ b/packages/driver/src/multi-domain/events/logs.ts @@ -1,12 +1,10 @@ -import { LogUtils } from '../../cypress/log' - export const handleLogs = (Cypress: Cypress.Cypress) => { const onLogAdded = (attrs) => { - Cypress.specBridgeCommunicator.toPrimary('log:added', LogUtils.getDisplayProps(attrs)) + Cypress.specBridgeCommunicator.toPrimary('log:added', attrs) } const onLogChanged = (attrs) => { - Cypress.specBridgeCommunicator.toPrimary('log:changed', LogUtils.getDisplayProps(attrs)) + Cypress.specBridgeCommunicator.toPrimary('log:changed', attrs) } Cypress.on('log:added', onLogAdded) diff --git a/packages/driver/src/util/serialization.ts b/packages/driver/src/util/serialization/index.ts similarity index 91% rename from packages/driver/src/util/serialization.ts rename to packages/driver/src/util/serialization/index.ts index 16d82f3e8bf7..32cfc3ef353e 100644 --- a/packages/driver/src/util/serialization.ts +++ b/packages/driver/src/util/serialization/index.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import structuredClonePonyfill from 'core-js-pure/actual/structured-clone' -import $stackUtils from '../cypress/stack_utils' -import $errUtils from '../cypress/error_utils' +import $stackUtils from '../../cypress/stack_utils' +import $errUtils from '../../cypress/error_utils' export const UNSERIALIZABLE = '__cypress_unserializable_value' @@ -10,7 +10,7 @@ export const UNSERIALIZABLE = '__cypress_unserializable_value' // @ts-ignore const structuredCloneRef = window?.structuredClone || structuredClonePonyfill -const isSerializableInCurrentBrowser = (value: any) => { +export const isSerializableInCurrentBrowser = (value: any) => { try { structuredCloneRef(value) @@ -24,6 +24,12 @@ const isSerializableInCurrentBrowser = (value: any) => { return false } + // In some instances of structuredClone, Bluebird promises are considered serializable, but can be very deep objects + // For ours needs, we really do NOT want to serialize these + if (value instanceof Cypress.Promise) { + return false + } + return true } catch (e) { return false @@ -106,7 +112,8 @@ export const preprocessForSerialization = (valueToSanitize: { [key: string]: // Even if native errors can be serialized through postMessage, many properties are omitted on structuredClone(), including prototypical hierarchy // because of this, we preprocess native errors to objects and postprocess them once they come back to the primary origin - if (_.isArray(valueToSanitize)) { + // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays. This is important for commands like .selectFile() using buffer streams + if (_.isArray(valueToSanitize) || _.isTypedArray(valueToSanitize)) { return _.map(valueToSanitize, preprocessForSerialization) as unknown as T } diff --git a/packages/driver/src/util/serialization/log.ts b/packages/driver/src/util/serialization/log.ts new file mode 100644 index 000000000000..d8a19f0823c7 --- /dev/null +++ b/packages/driver/src/util/serialization/log.ts @@ -0,0 +1,425 @@ +import _ from 'lodash' +import { isSerializableInCurrentBrowser, preprocessForSerialization } from './index' +import $dom from '../../dom' + +interface PreprocessedHTMLElement { + tagName: string + attributes: { [key: string]: string } + innerHTML: string + serializationKey: 'dom' +} + +interface PreprocessedFunction { + value: any + serializationKey: 'function' +} + +/** + * Takes an HTMLElement that might be a for a given snapshot or any other element, likely pertaining to log consoleProps, + * on the page that needs to be preprocessed for serialization. The method here is to do a very shallow serialization, + * by trying to make the HTML as stateful as possible before preprocessing. + * + * @param {HTMLElement} props - an HTMLElement + * @returns {PreprocessedHTMLElement} a preprocessed element that can be fed through postMessage() that can be reified in the primary. + */ +export const preprocessDomElement = (props: HTMLElement) => { + const inputPreprocessArray = Array.from(props.querySelectorAll('input, textarea, select')) + + // Since we serialize on innerHTML, we also need to account for the props element itself in the case it is an input, select, or textarea. + inputPreprocessArray.push(props) + + // Hydrate values in the HTML copy so when serialized they show up correctly in snapshot. + // We do this by mapping certain properties to attributes that are not already reflected in the attributes map. + // Things like id, class, type, and others are reflected in the attribute map and do not need to be explicitly added. + inputPreprocessArray.forEach((el: any) => { + switch (el.type) { + case 'checkbox': + case 'radio': + if (el.checked) { + el.setAttribute('checked', '') + } + + break + case 'select-one': + case 'select-multiple': { + const options = el.type === 'select-one' ? el.options : el.selectedOptions + + if (el.selectedIndex !== -1) { + for (let option of options) { + if (option.selected) { + option.setAttribute('selected', 'true') + } else { + option.removeAttribute('selected') + } + } + } + } + break + case 'textarea': { + el.innerHTML = el.value + } + break + default: + if (el.value !== undefined) { + el.setAttribute('value', el.value) + } + } + }) + + const el: PreprocessedHTMLElement = { + tagName: props.tagName, + attributes: {}, + innerHTML: props.innerHTML, + serializationKey: 'dom', + } + + // get all attributes and classes off the element + props.getAttributeNames().forEach((attributeName) => { + el.attributes[attributeName] = props.getAttribute(attributeName) || '' + }) + + return el +} + +/** + * Takes an PreprocessedHTMLElement that might represent a given snapshot or any other element that needs to be reified + * after postMessage() serialization. The method here is to do a very basic reification, + * attempting to create an element based off the PreprocessedHTMLElement tagName, and populating some basic state if applicable, + * such as element type, id, value, classes, attributes, etc. + * + * @param {PreprocessedHTMLElement} props - a preprocessed element that was fed through postMessage() that need to be reified in the primary. + * @returns {HTMLElement} a reified element, likely a log snapshot, $el, or consoleProp elements. + */ +export const reifyDomElement = (props: any) => { + const reifiedEl = document.createElement(props.tagName) + + reifiedEl.innerHTML = props.innerHTML + + Object.keys(props.attributes).forEach((attribute) => { + reifiedEl.setAttribute(attribute, props.attributes[attribute]) + }) + + return reifiedEl +} + +/** + * Attempts to preprocess an Object/Array by excluding unserializable values except for DOM elements and possible functions (if attemptToSerializeFunctions is true). + * DOM elements are processed to a serializable object via preprocessDomElement, and functions are serialized to an object with a value key containing their output contents. + * + * @param {any} props an Object/Array that needs to be preprocessed before being sent through postMessage(). + * @param {boolean} [attemptToSerializeFunctions=false] - Whether or not the function should attempt to preprocess a function by invoking it. + * @returns + */ +export const preprocessObjectLikeForSerialization = (props, attemptToSerializeFunctions = false) => { + if (_.isArray(props)) { + return props.map((prop) => preprocessLogLikeForSerialization(prop, attemptToSerializeFunctions)) + } + + if (_.isPlainObject(props)) { + // only attempt to try and serialize dom elements and functions (if attemptToSerializeFunctions is set to true) + let objWithPossiblySerializableProps = _.pickBy(props, (value) => { + const isSerializable = isSerializableInCurrentBrowser(value) + + if (!isSerializable && $dom.isDom(value) || _.isFunction(value) || _.isObject(value)) { + return true + } + + return false + }) + + let objWithOnlySerializableProps = _.pickBy(props, (value) => isSerializableInCurrentBrowser(value)) + + // assign the properties we know we can serialize here + let preprocessed: any = preprocessForSerialization(objWithOnlySerializableProps) + + // and attempt to serialize possibly unserializable props here and fail gracefully if unsuccessful + _.forIn(objWithPossiblySerializableProps, (value, key) => { + preprocessed[key] = preprocessLogLikeForSerialization(value, attemptToSerializeFunctions) + }) + + return preprocessed + } + + return preprocessForSerialization(props) +} + +/** + * Attempts to take an Object and reify it correctly. Most of this is handled by reifyLogLikeFromSerialization, with the exception here being DOM elements. + * DOM elements, if needed to match against the snapshot DOM, are defined as getters on the object to have their values calculated at request. + * This is important for certain log items, such as consoleProps, to be rendered correctly against the snapshot. Other DOM elements, such as snapshots, do not need to be matched + * against the current DOM and can be reified immediately. Since there is a potential need for object getters to exist within an array, arrays are wrapped in a proxy, with array indices + * proxied to the reified object or array, and other methods proxying to the preprocessed array (such as native array methods like map, foreach, etc...). + * + * @param {Object} props - a preprocessed Object/Array that was fed through postMessage() that need to be reified in the primary. + * @param {boolean} matchElementsAgainstSnapshotDOM - whether DOM elements within the Object/Array should be matched against + * @returns {Object|Proxy} - a reified version of the Object or Array (Proxy). + */ +export const reifyObjectLikeForSerialization = (props, matchElementsAgainstSnapshotDOM) => { + let reifiedObjectOrArray = {} + + _.forIn(props, (value, key) => { + const val = reifyLogLikeFromSerialization(value, matchElementsAgainstSnapshotDOM) + + if (val?.serializationKey === 'dom') { + if (matchElementsAgainstSnapshotDOM) { + // dynamically calculate the element (snapshot or otherwise). + // This is important for consoleProp/$el based properties on the log because it calculates the requested element AFTER the snapshot has been rendered into the AUT. + reifiedObjectOrArray = { + ...reifiedObjectOrArray, + get [key] () { + return val.reifyElement() + }, + } + } else { + // The DOM element in question is something like a snapshot. It can be reified immediately + reifiedObjectOrArray[key] = val.reifyElement() + } + } else { + reifiedObjectOrArray[key] = reifyLogLikeFromSerialization(value, matchElementsAgainstSnapshotDOM) + } + }) + + // NOTE: transforms arrays into objects to have defined getters for DOM elements, and proxy back to that object via an ES6 Proxy. + if (_.isArray(props)) { + // if an array, map the array to our special getter object. + return new Proxy(reifiedObjectOrArray, { + get (target, name) { + return target[name] || props[name] + }, + }) + } + + // otherwise, just returned the object with our special getter + return reifiedObjectOrArray +} + +/** + * Attempts to take a generic data structure that is log-like and preprocess them for serialization. This generic may contain properties that are either + * a) unserializable entirely + * b) unserializable natively but can be processed to a serializable form (DOM elements or Functions) + * c) serializable + * + * DOM elements are preprocessed via some key properties + * (attributes, classes, ids, tagName, value) including their innerHTML. Before the innerHTML is captured, inputs are traversed to set their stateful value + * inside the DOM element. This is crucial for body copy snapshots that are being sent to the primary domain to make the snapshot 'stateful'. Functions, if + * explicitly stated, will be preprocessed with whatever value they return (assuming that value is serializable). If a value cannot be preprocessed for whatever reason, + * null is returned. + * + * + * NOTE: this function recursively calls itself to preprocess a log + * + * @param {any} props a generic variable that represents a value that needs to be preprocessed before being sent through postMessage(). + * @param {boolean} [attemptToSerializeFunctions=false] - Whether or not the function should attempt to preprocess a function by invoking it. USE WITH CAUTION! + * @returns {any} the serializable version of the generic. + */ +export const preprocessLogLikeForSerialization = (props, attemptToSerializeFunctions = false) => { + try { + if ($dom.isDom(props)) { + if (props.length !== undefined && $dom.isJquery(props)) { + const serializableArray: any[] = [] + + // in the case we are dealing with a jQuery array, preprocess to a native array to nuke any prevObject(s) or unserializable values + props.each((key) => serializableArray.push(preprocessLogLikeForSerialization(props[key], attemptToSerializeFunctions))) + + return serializableArray + } + + // otherwise, preprocess the element to an object with pertinent DOM properties + const serializedDom = preprocessDomElement(props) + + return serializedDom + } + + /** + * When preprocessing a log, there might be certain functions we want to attempt to serialize. + * One of these instances is the 'table' key in consoleProps, which has contents that CAN be serialized. + * If there are other functions that have serializable contents, the invoker/developer will need to be EXPLICIT + * in what needs serialization. Otherwise, functions should NOT be serialized. + */ + if (_.isFunction(props)) { + if (attemptToSerializeFunctions) { + return { + value: preprocessLogLikeForSerialization(props(), attemptToSerializeFunctions), + serializationKey: 'function', + } as PreprocessedFunction + } + + return null + } + + if (_.isObject(props)) { + return preprocessObjectLikeForSerialization(props, attemptToSerializeFunctions) + } + + return preprocessForSerialization(props) + } catch (e) { + return null + } +} + +/** + * Attempts to take in a preprocessed/serialized log-like attributes and reify them. DOM elements are lazily calculated via + * getter properties on an object. If these DOM elements are in an array, the array is defined as an ES6 proxy that + * ultimately proxies to these getter objects. Functions, if serialized, are rewrapped. If a value cannot be reified for whatever reason, + * null is returned. + * + * This is logLike because there is a need outside of logs, such as in the iframe-model in the runner. + * to serialize DOM elements, such as the final snapshot upon request. + * + * NOTE: this function recursively calls itself to reify a log + * + * @param {any} props - a generic variable that represents a value that has been preprocessed and sent through postMessage() and needs to be reified. + * @param {boolean} matchElementsAgainstSnapshotDOM - Whether or not the element should be reconstructed lazily + * against the currently rendered DOM (usually against a rendered snapshot) or should be completely recreated from scratch (common with snapshots as they will replace the DOM) + * @returns {any} the reified version of the generic. + */ +export const reifyLogLikeFromSerialization = (props, matchElementsAgainstSnapshotDOM = true) => { + try { + if (props?.serializationKey === 'dom') { + props.reifyElement = function () { + let reifiedElement + + // If the element needs to be matched against the currently rendered DOM. This is useful when analyzing consoleProps or $el in a log + // where elements need to be evaluated LAZILY after the snapshot is attached to the page. + // this option is set to false when reifying snapshots, since they will be replacing the current DOM when the user interacts with said snapshot. + if (matchElementsAgainstSnapshotDOM) { + const attributes = Object.keys(props.attributes).map((attribute) => { + return `[${attribute}="${props.attributes[attribute]}"]` + }).join('') + + const selector = `${props.tagName}${attributes}` + + reifiedElement = Cypress.$(selector) + + if (reifiedElement.length) { + return reifiedElement.length > 1 ? reifiedElement : reifiedElement[0] + } + } + + // if the element couldn't be found, return a synthetic copy that doesn't actually exist on the page + return reifyDomElement(props) + } + + return props + } + + if (props?.serializationKey === 'function') { + const reifiedFunctionData = reifyLogLikeFromSerialization(props.value, matchElementsAgainstSnapshotDOM) + + return () => reifiedFunctionData + } + + if (_.isObject(props)) { + return reifyObjectLikeForSerialization(props, matchElementsAgainstSnapshotDOM) + } + + return props + } catch (e) { + return null + } +} + +/** + * Preprocess a snapshot to a serializable form before piping them over through postMessage(). + * This method is also used by a spec bridge on request if a 'final state' snapshot is requested outside that of the primary domain + * + * @param {any} snapshot - a snapshot matching the same structure that is returned from cy.createSnapshot. + * @returns a serializable form of a snapshot, including a serializable with styles + */ +export const preprocessSnapshotForSerialization = (snapshot) => { + try { + const preprocessedSnapshot = preprocessLogLikeForSerialization(snapshot, true) + + if (!preprocessedSnapshot.body.get) { + return null + } + + preprocessedSnapshot.styles = cy.getStyles(snapshot) + + return preprocessedSnapshot + } catch (e) { + return null + } +} + +/** + * Reifies a snapshot from the serializable from to an actual HTML body snapshot that exists in the primary document. + * @param {any} snapshot - a snapshot that has been preprocessed and sent through post message and needs to be reified in the primary. + * @returns the reified snapshot that exists in the primary document + */ +export const reifySnapshotFromSerialization = (snapshot) => { + snapshot.body = reifyLogLikeFromSerialization(snapshot.body, false) + + return cy.createSnapshot(snapshot.name, null, snapshot) +} + +/** + * Sanitizes the log messages going to the primary domain before piping them to postMessage(). + * This is designed to function as an extension of preprocessForSerialization, but also attempts to serialize DOM elements, + * as well as functions if explicitly stated. + * + * DOM elements are serialized with their outermost properties (attributes, classes, ids, tagName) including their innerHTML. + * DOM Traversal serialization is not possible with larger html bodies and will likely cause a stack overflow. + * + * Functions are serialized when explicitly state (ex: table in consoleProps). + * NOTE: If not explicitly handling function serialization for a given property, the property will be set to null + * + * @param logAttrs raw log attributes passed in from either a log:changed or log:added event + * @returns a serializable form of the log, including attempted serialization of DOM elements and Functions (if explicitly stated) + */ +export const preprocessLogForSerialization = (logAttrs) => { + let { snapshots, ...logAttrsRest } = logAttrs + + const preprocessed = preprocessLogLikeForSerialization(logAttrsRest) + + if (preprocessed) { + if (snapshots) { + preprocessed.snapshots = snapshots.map((snapshot) => preprocessSnapshotForSerialization(snapshot)) + } + + if (logAttrs?.consoleProps?.table) { + preprocessed.consoleProps.table = preprocessLogLikeForSerialization(logAttrs.consoleProps.table, true) + } + } + + return preprocessed +} + +/** + * Redefines log messages being received in the primary domain before sending them out through the event-manager. + * + * Efforts here include importing captured snapshots from the spec bridge into the primary snapshot document, importing inline + * snapshot styles into the snapshot css map, and reconstructing DOM elements and functions somewhat naively. + * + * To property render consoleProps/$el elements in snapshots or the console, DOM elements are lazily calculated via + * getter properties on an object. If these DOM elements are in an array, the array is defined as an ES6 proxy that + * ultimately proxies to these getter objects. + * + * The secret here is that consoleProp DOM elements needs to be reified at console printing runtime AFTER the serialized snapshot + * is attached to the DOM so the element can be located and displayed properly + * + * In most cases, the element can be queried by attributes that exist specifically on the element or by the `HIGHLIGHT_ATTR`. If that fails or does not locate an element, + * then a new element is created against the snapshot context. This element will NOT be found on the page, but will represent what the element + * looked like at the time of the snapshot. + * + * @param logAttrs serialized/preprocessed log attributes passed to the primary domain from a spec bridge + * @returns a reified version of what a log is supposed to look like in Cypress + */ +export const reifyLogFromSerialization = (logAttrs) => { + let { snapshots, ... logAttrsRest } = logAttrs + + if (snapshots) { + snapshots = snapshots.filter((snapshot) => !!snapshot).map((snapshot) => reifySnapshotFromSerialization(snapshot)) + } + + const reified = reifyLogLikeFromSerialization(logAttrsRest) + + if (reified.$el && reified.$el.length) { + // Make sure $els are jQuery Arrays to keep what is expected in the log. + reified.$el = Cypress.$(reified.$el.map((el) => el)) + } + + reified.snapshots = snapshots + + return reified +} diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index 82a0754ded3e..4c470a0f8b9a 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -46,6 +46,8 @@ declare namespace Cypress { isAnticipatingCrossOriginResponseFor: IStability['isAnticipatingCrossOriginResponseFor'] fail: (err: Error, options:{ async?: boolean }) => Error getRemoteLocation: ILocation['getRemoteLocation'] + createSnapshot: ISnapshots['createSnapshot'] + getStyles: ISnapshots['getStyles'] } interface Cypress { diff --git a/packages/driver/types/spec-types.d.ts b/packages/driver/types/spec-types.d.ts new file mode 100644 index 000000000000..c0ebdbfc6d2a --- /dev/null +++ b/packages/driver/types/spec-types.d.ts @@ -0,0 +1,8 @@ +// NOTE: This is for internal Cypress spec types that exist in support/utils.js for testing convenience and do not ship with Cypress + +declare namespace Cypress { + interface Chainable { + getAll(...aliases: string[]): Chainable + shouldWithTimeout(cb: (subj: {}) => void, timeout?: number): Chainable + } +} diff --git a/packages/runner-ct/src/iframe/iframes.tsx b/packages/runner-ct/src/iframe/iframes.tsx index 2c7771e1c9e9..2658c9a269b3 100644 --- a/packages/runner-ct/src/iframe/iframes.tsx +++ b/packages/runner-ct/src/iframe/iframes.tsx @@ -133,6 +133,8 @@ export const Iframes = namedObserver('Iframes', ({ restoreDom: autIframe.current.restoreDom, highlightEl: autIframe.current.highlightEl, detachDom: autIframe.current.detachDom, + isAUTSameOrigin: autIframe.current.doesAUTMatchTopOriginPolicy, + removeSrc: autIframe.current.removeSrcAttribute, snapshotControls: (snapshotProps) => ( { + // If the test is over and the user enters interactive snapshot mode, do not add cross origin logs to the test runner. + if (Cypress.state('test')?.final) return + // Create a new local log representation of the cross origin log. // It will be attached to the current command. // We also keep a reference to it to update it in the future. diff --git a/packages/runner-shared/src/iframe/aut-iframe.js b/packages/runner-shared/src/iframe/aut-iframe.js index 96275c00221f..4a321ba4f31e 100644 --- a/packages/runner-shared/src/iframe/aut-iframe.js +++ b/packages/runner-shared/src/iframe/aut-iframe.js @@ -69,6 +69,40 @@ export class AutIframe { return Cypress.cy.detachDom(this._contents()) } + /** + * If the AUT is cross origin relative to top, a security error is thrown and the method returns false + * If the AUT is cross origin relative to top and chromeWebSecurity is false, origins of the AUT and top need to be compared and returns false + * Otherwise, if top and the AUT match origins, the method returns true. + * If the AUT origin is "about://blank", that means the src attribute has been stripped off the iframe and is adhering to same origin policy + */ + doesAUTMatchTopOriginPolicy = () => { + const Cypress = eventManager.getCypress() + + if (!Cypress) return + + try { + const { href: currentHref } = this.$iframe[0].contentWindow.document.location + const locationTop = Cypress.Location.create(window.location.href) + const locationAUT = Cypress.Location.create(currentHref) + + return locationTop.originPolicy === locationAUT.originPolicy || locationAUT.originPolicy === 'about://blank' + } catch (err) { + if (err.name === 'SecurityError') { + return false + } + + throw err + } + } + + /** + * Removes the src attribute from the AUT iframe, resulting in 'about:blank' being loaded into the iframe + * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-src for more details + */ + removeSrcAttribute = () => { + this.$iframe.removeAttr('src') + } + visitBlank = ({ type } = { type: null }) => { return new Promise((resolve) => { this.$iframe[0].src = 'about:blank' @@ -91,6 +125,23 @@ export class AutIframe { } restoreDom = (snapshot) => { + if (!this.doesAUTMatchTopOriginPolicy()) { + /** + * A load event fires here when the src is removed (as does an unload event). + * This is equivalent to loading about:blank (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-src). + * This doesn't resort in a log message being generated for a new page. + * In the event-manager code, we stop adding logs from other domains once the spec is finished. + */ + this.$iframe.one('load', () => { + this.restoreDom(snapshot) + }) + + // The iframe is in a cross origin state. Remove the src attribute to adhere to same origin policy. NOTE: This should only be done ONCE. + this.removeSrcAttribute() + + return + } + const Cypress = eventManager.getCypress() const { headStyles, bodyStyles } = Cypress ? Cypress.cy.getStyles(snapshot) : {} const { body, htmlAttrs } = snapshot diff --git a/packages/runner-shared/src/iframe/iframe-model.js b/packages/runner-shared/src/iframe/iframe-model.js index e2639ce6974c..fca3665fb22c 100644 --- a/packages/runner-shared/src/iframe/iframe-model.js +++ b/packages/runner-shared/src/iframe/iframe-model.js @@ -6,12 +6,14 @@ import { studioRecorder } from '../studio' import { eventManager } from '../event-manager' export class IframeModel { - constructor ({ state, detachDom, restoreDom, highlightEl, snapshotControls }) { + constructor ({ state, detachDom, restoreDom, highlightEl, snapshotControls, isAUTSameOrigin, removeSrc }) { this.state = state this.detachDom = detachDom this.restoreDom = restoreDom this.highlightEl = highlightEl this.snapshotControls = snapshotControls + this.isAUTSameOrigin = isAUTSameOrigin + this.removeSrc = removeSrc this._reset() } @@ -217,6 +219,31 @@ export class IframeModel { } _storeOriginalState () { + if (!this.isAUTSameOrigin()) { + const Cypress = eventManager.getCypress() + + /** + * This only happens if the AUT ends in a cross origin state that the primary doesn't have access to. + * In this case, the final snapshot request from the primary is sent out to the cross-origin spec bridges. + * The spec bridge that matches the origin policy will take a snapshot and send it back to the primary for the runner to store in originalState. + */ + Cypress.primaryOriginCommunicator.toAllSpecBridges('generate:final:snapshot', this.state.url) + Cypress.primaryOriginCommunicator.once('final:snapshot:generated', (finalSnapshot) => { + this.originalState = { + body: finalSnapshot.body, + htmlAttrs: finalSnapshot.htmlAttrs, + snapshot: finalSnapshot, + url: this.state.url, + // TODO: use same attr for both runner and runner-ct states. + // these refer to the same thing - the viewport dimensions. + viewportWidth: this.state.width, + viewportHeight: this.state.height, + } + }) + + return + } + const finalSnapshot = this.detachDom() if (!finalSnapshot) return diff --git a/packages/runner/index.d.ts b/packages/runner/index.d.ts index 918d244cd9e7..8e559dc28663 100644 --- a/packages/runner/index.d.ts +++ b/packages/runner/index.d.ts @@ -8,3 +8,4 @@ /// /// +/// diff --git a/packages/runner/src/iframe/iframes.jsx b/packages/runner/src/iframe/iframes.jsx index cf5407378eef..f5aea2a75673 100644 --- a/packages/runner/src/iframe/iframes.jsx +++ b/packages/runner/src/iframe/iframes.jsx @@ -109,6 +109,8 @@ export default class Iframes extends Component { restoreDom: this.autIframe.restoreDom, highlightEl: this.autIframe.highlightEl, detachDom: this.autIframe.detachDom, + isAUTSameOrigin: this.autIframe.doesAUTMatchTopOriginPolicy, + removeSrc: this.autIframe.removeSrcAttribute, snapshotControls: (snapshotProps) => ( Date: Fri, 22 Apr 2022 15:58:02 -0400 Subject: [PATCH 116/177] chore: Updates based on PR feedback (#21137) * add generic to cy.origin type * fix log type, update/add comments * fix comment indentation * specific generic * move RemoteState to internal types * add on links to experimental flag descriptions * chore: reduce nesting by flipping condition * fix test title * simplify failing log * rename variable * delete error property * fix types * fix type * remove unnecessary todo * update wait test * jquery -> this * update comment * remove vestigial autoRun * use finally * re-throw non-security errors * move back getting index * add new state types * remove unnecessary export * startsWith -> includes * it -> them * update system test * remove use of promise constructor * Revert "remove use of promise constructor" This reverts commit 35ccc28b6f0fdc4b768b020d14225a55f82773c5. * log errors from Page.getFrameTree * test if anything breaks when removing optional chaining operator * remove vestigial file * handle queue ending in cross-origin driver * fix coordinates spec * improve chrome/firefox check in extension * improve secure cookie regex * use production mode for cross-origin driver bundle * adding remoteStates.getPrimary * catch and ignore queue errors * remove optional chaining in postMessage handler * removed unnecessary async * update frame tree on cri client reconnect * fix formatting * renaming remoteStates variable * prevent requests from being paused if experimentalSessionAndOrigin flag is off Co-authored-by: Matt Schile --- cli/schema/cypress.schema.json | 2 +- cli/types/cypress.d.ts | 23 +- .../driver/cypress/fixtures/auth/index.html | 2 - .../integration/commands/navigation_spec.js | 6 +- .../cypress/integration/cypress/log_spec.js | 2 +- .../integration/dom/coordinates_spec.ts | 9 +- .../commands/multi_domain_log.spec.ts | 2 +- .../commands/multi_domain_waiting.spec.ts | 6 +- .../multi-domain/multi_domain_yield_spec.ts | 4 +- packages/driver/src/cy/commands/navigation.ts | 82 +++--- packages/driver/src/cy/location.ts | 2 +- packages/driver/src/cypress/command_queue.ts | 6 +- packages/driver/src/cypress/cy.ts | 33 +-- packages/driver/src/dom/coordinates.ts | 5 + .../driver/src/multi-domain/communicator.ts | 6 +- packages/driver/src/multi-domain/domain_fn.ts | 21 +- packages/driver/src/util/deferred.ts | 18 -- packages/driver/types/internal-types.d.ts | 8 + packages/driver/types/remote-state.d.ts | 14 + packages/extension/app/background.js | 29 +- .../test/integration/background_spec.js | 253 ++++++++---------- packages/network/lib/index.ts | 2 - .../proxy/lib/http/response-middleware.ts | 2 +- .../unit/http/response-middleware.spec.ts | 19 +- packages/runner-shared/src/event-manager.js | 7 +- packages/runner/webpack.config.ts | 2 +- packages/server/index.d.ts | 2 + .../server/lib/browsers/cdp_automation.ts | 2 +- packages/server/lib/browsers/chrome.ts | 35 ++- packages/server/lib/browsers/cri-client.ts | 18 +- packages/server/lib/browsers/electron.js | 4 +- packages/server/lib/browsers/firefox-util.ts | 5 +- packages/server/lib/browsers/firefox.ts | 3 +- packages/server/lib/controllers/files.js | 2 +- packages/server/lib/controllers/runner.ts | 2 +- packages/server/lib/remote_states.ts | 10 +- packages/server/lib/routes.ts | 6 - packages/server/lib/server-base.ts | 18 +- packages/server/lib/server-ct.ts | 2 +- packages/server/lib/server-e2e.ts | 14 +- packages/server/lib/socket-base.ts | 7 + .../server/test/unit/browsers/chrome_spec.js | 30 ++- .../test/unit/browsers/cri-client_spec.ts | 7 +- .../test/unit/browsers/electron_spec.js | 14 + .../server/test/unit/remote_states.spec.ts | 38 +++ .../test/multi_domain_retries_spec.ts | 5 +- 46 files changed, 447 insertions(+), 342 deletions(-) delete mode 100644 packages/driver/src/util/deferred.ts create mode 100644 packages/driver/types/remote-state.d.ts diff --git a/cli/schema/cypress.schema.json b/cli/schema/cypress.schema.json index 1d18748be48b..5b7251565274 100644 --- a/cli/schema/cypress.schema.json +++ b/cli/schema/cypress.schema.json @@ -256,7 +256,7 @@ "experimentalSessionAndOrigin": { "type": "boolean", "default": false, - "description": "Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands." + "description": "Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands. See https://on.cypress.io/origin and https://on.cypress.io/session." }, "experimentalSourceRewriting": { "type": "boolean", diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index b5842c1178b4..30e09a61bf1c 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -56,15 +56,6 @@ declare namespace Cypress { password: string } - interface RemoteState { - auth?: Auth - domainName: string - strategy: 'file' | 'http' - origin: string - fileServer: string | null - props: Record - } - interface Backend { /** * Firefox only: Force Cypress to run garbage collection routines. @@ -1430,7 +1421,7 @@ declare namespace Cypress { * cy.get('h1').should('equal', 'Example Domain') * }) */ - origin(urlOrDomain: string, fn: () => void): Chainable + origin(urlOrDomain: string, fn: () => void): Chainable /** * Enables running Cypress commands in a secondary origin. @@ -1441,9 +1432,9 @@ declare namespace Cypress { * expect(foo).to.equal('foo') * }) */ - origin(urlOrDomain: string, options: { + origin(urlOrDomain: string, options: { args: T - }, fn: (args: T) => void): Chainable + }, fn: (args: T) => void): Chainable /** * Get the parent DOM element of a set of DOM elements. @@ -2846,7 +2837,7 @@ declare namespace Cypress { */ experimentalInteractiveRunEvents: boolean /** - * Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands. + * Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands. See https://on.cypress.io/origin and https://on.cypress.io/session. * @default false */ experimentalSessionAndOrigin: boolean @@ -2981,7 +2972,6 @@ declare namespace Cypress { projectName: string projectRoot: string proxyUrl: string - remote: RemoteState report: boolean reporterRoute: string reporterUrl: string @@ -5766,7 +5756,8 @@ declare namespace Cypress { } interface LogConfig extends Timeoutable { - id: number + /** Unique id for the log, in the form of '-' */ + id: string /** The JQuery element for the command. This will highlight the command in the main window when debugging */ $el: JQuery /** The scope of the log entry. If child, will appear nested below parents, prefixed with '-' */ @@ -5779,7 +5770,7 @@ 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 */ + /** Set to true to immediately finish the log */ end: boolean /** Return an object that will be printed in the dev tools console */ consoleProps(): ObjectLike diff --git a/packages/driver/cypress/fixtures/auth/index.html b/packages/driver/cypress/fixtures/auth/index.html index 75d81cd652e7..bc7f2327ab10 100644 --- a/packages/driver/cypress/fixtures/auth/index.html +++ b/packages/driver/cypress/fixtures/auth/index.html @@ -70,8 +70,6 @@ } else { const token = JSON.parse(cypressAuthToken) - // ToDo, check for expiry maybe? - // If the token exists, hooray, give them a logout button to destroy the token and refresh. const tag = document.createElement("p"); const text = document.createTextNode(`Welcome ${token.body.username}`); diff --git a/packages/driver/cypress/integration/commands/navigation_spec.js b/packages/driver/cypress/integration/commands/navigation_spec.js index cf3a9b5c94ef..1b9a09669743 100644 --- a/packages/driver/cypress/integration/commands/navigation_spec.js +++ b/packages/driver/cypress/integration/commands/navigation_spec.js @@ -2344,9 +2344,9 @@ describe('src/cy/commands/navigation', () => { } cy.on('command:queue:before:end', () => { - // force us to become unstable immediately - // else the beforeunload event fires at the end - // of the tick which is too late + // force us to become unstable immediately + // else the beforeunload event fires at the end + // of the tick which is too late cy.isStable(false, 'testing') win.location.href = '/timeout?ms=100' diff --git a/packages/driver/cypress/integration/cypress/log_spec.js b/packages/driver/cypress/integration/cypress/log_spec.js index 956209d37b87..c534282bd670 100644 --- a/packages/driver/cypress/integration/cypress/log_spec.js +++ b/packages/driver/cypress/integration/cypress/log_spec.js @@ -91,7 +91,7 @@ describe('src/cypress/log', function () { expect(LogUtils.countLogsByTests(tests)).to.equal(6) }) - it('returns zero if there are no agents routes or commands', () => { + it('returns zero if there are no agents, routes, or commands', () => { const tests = { a: { notAThing: true, diff --git a/packages/driver/cypress/integration/dom/coordinates_spec.ts b/packages/driver/cypress/integration/dom/coordinates_spec.ts index 84483caf04a5..abcc50232e02 100644 --- a/packages/driver/cypress/integration/dom/coordinates_spec.ts +++ b/packages/driver/cypress/integration/dom/coordinates_spec.ts @@ -238,15 +238,18 @@ describe('src/dom/coordinates', () => { } it('returns true if parent is a window and not an iframe', () => { - const win = getWindowLikeObject() + const win = cy.state('window') expect(isAUTFrame(win)).to.be.true }) - it('returns true if parent is a window and getting its frameElement property throws an error', () => { + it('returns true if parent is a window and getting its frameElement property throws a cross-origin error', () => { const win = getWindowLikeObject() + const err = new Error('cross-origin error') + + err.name = 'SecurityError' - cy.stub($elements, 'getNativeProp').throws('cross-origin error') + cy.stub($elements, 'getNativeProp').throws(err) expect(isAUTFrame(win)).to.be.true }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_log.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_log.spec.ts index 5b8fa6ae2fce..63b2b283db7a 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_log.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_log.spec.ts @@ -16,7 +16,7 @@ context('cy.origin log', () => { }) it('logs in primary and secondary origins', () => { - cy.origin('http://foobar.com:3500', () => { + cy.origin('http://foobar.com:3500', () => { const afterLogAdded = new Promise((resolve) => { const listener = (attrs) => { if (attrs.message === 'test log in cy.origin') { diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts index 67f188e603e7..2def9b96919b 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_waiting.spec.ts @@ -8,7 +8,11 @@ context('cy.origin waiting', () => { it('.wait()', () => { cy.origin('http://foobar.com:3500', () => { - cy.wait(500) + const delay = cy.spy(Cypress.Promise, 'delay') + + cy.wait(50).then(() => { + expect(delay).to.be.calledWith(50, 'wait') + }) }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts index cc3fd35ac5cc..ce229fe9a8f4 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts @@ -118,7 +118,7 @@ describe('cy.origin yields', () => { done() }) - cy.origin('http://foobar.com:3500', () => { + cy.origin('http://foobar.com:3500', () => { cy.get('[data-cy="dom-check"]') }) .then((subject) => subject.text()) @@ -134,7 +134,7 @@ describe('cy.origin yields', () => { done() }) - cy.origin('http://foobar.com:3500', () => { + cy.origin<{ key: Function }>('http://foobar.com:3500', () => { cy.wrap({ key: () => { return 'whoops' diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index 407e0aa76e90..1d560bca334e 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -13,7 +13,7 @@ import debugFn from 'debug' const debug = debugFn('cypress:driver:navigation') let id = null -let previousUrlVisited: LocationObject | undefined +let previouslyVisitedLocation: LocationObject | undefined let hasVisitedAboutBlank: boolean = false let currentlyVisitingAboutBlank: boolean = false let knownCommandCausedInstability: boolean = false @@ -30,7 +30,7 @@ const reset = (test: any = {}) => { // continuously reset this // before each test run! - previousUrlVisited = undefined + previouslyVisitedLocation = undefined // make sure we reset that we haven't // visited about blank again @@ -53,33 +53,7 @@ const timedOutWaitingForPageLoad = (ms, log) => { const anticipatedCrossOriginHref = cy.state('anticipatingCrossOriginResponse')?.href // Were we anticipating a cross origin page when we timed out? - if (anticipatedCrossOriginHref) { - // We remain in an anticipating state until either a load even happens or a timeout. - cy.isAnticipatingCrossOriginResponseFor(undefined) - - // By default origins is just this location. - let originPolicies = [$Location.create(location.href).originPolicy] - - const currentCommand = cy.queue.state('current') - - if (currentCommand?.get('name') === 'origin') { - // If the current command is a cy.origin command, we should have gotten a request on the origin it expects. - originPolicies = [cy.state('latestActiveOriginPolicy')] - } else if (Cypress.isCrossOriginSpecBridge && cy.queue.isOnLastCommand()) { - // If this is a cross origin spec bridge and we're on the last command, we should have gotten a request on the origin of one of the parents. - originPolicies = cy.state('parentOriginPolicies') - } - - $errUtils.throwErrByPath('navigation.cross_origin_load_timed_out', { - args: { - configFile: Cypress.config('configFile'), - ms, - crossOriginUrl: $Location.create(anticipatedCrossOriginHref), - originPolicies, - }, - onFail: log, - }) - } else { + if (!anticipatedCrossOriginHref) { $errUtils.throwErrByPath('navigation.timed_out', { args: { configFile: Cypress.config('configFile'), @@ -88,9 +62,35 @@ const timedOutWaitingForPageLoad = (ms, log) => { onFail: log, }) } + + // We remain in an anticipating state until either a load even happens or a timeout. + cy.isAnticipatingCrossOriginResponseFor(undefined) + + // By default origins is just this location. + let originPolicies = [$Location.create(location.href).originPolicy] + + const currentCommand = cy.queue.state('current') + + if (currentCommand?.get('name') === 'origin') { + // If the current command is a cy.origin command, we should have gotten a request on the origin it expects. + originPolicies = [cy.state('latestActiveOriginPolicy')] + } else if (Cypress.isCrossOriginSpecBridge && cy.queue.isOnLastCommand()) { + // If this is a cross origin spec bridge and we're on the last command, we should have gotten a request on the origin of one of the parents. + originPolicies = cy.state('parentOriginPolicies') + } + + $errUtils.throwErrByPath('navigation.cross_origin_load_timed_out', { + args: { + configFile: Cypress.config('configFile'), + ms, + crossOriginUrl: $Location.create(anticipatedCrossOriginHref), + originPolicies, + }, + onFail: log, + }) } -const cannotVisitDifferentOrigin = ({ remote, existing, originalUrl, previousUrlVisited, log, isCrossOriginSpecBridge = false }) => { +const cannotVisitDifferentOrigin = ({ remote, existing, originalUrl, previouslyVisitedLocation, log, isCrossOriginSpecBridge = false }) => { const differences: string[] = [] if (remote.protocol !== existing.protocol) { @@ -109,7 +109,7 @@ const cannotVisitDifferentOrigin = ({ remote, existing, originalUrl, previousUrl onFail: log, args: { differences: differences.join(', '), - previousUrl: previousUrlVisited, + previousUrl: previouslyVisitedLocation, attemptedUrl: remote, originalUrl, isCrossOriginSpecBridge, @@ -123,12 +123,12 @@ const cannotVisitDifferentOrigin = ({ remote, existing, originalUrl, previousUrl $errUtils.throwErrByPath('visit.cannot_visit_different_origin', errOpts) } -const cannotVisitPreviousOrigin = ({ remote, originalUrl, previousUrlVisited, log }) => { +const cannotVisitPreviousOrigin = ({ remote, originalUrl, previouslyVisitedLocation, log }) => { const errOpts = { onFail: log, args: { attemptedUrl: remote, - previousUrl: previousUrlVisited, + previousUrl: previouslyVisitedLocation, originalUrl, }, errProps: { @@ -434,9 +434,7 @@ const stabilityChanged = (Cypress, state, config, stable) => { } const onCrossOriginFailure = (err) => { - options._log.set('message', '--page loaded--').snapshot().end() - options._log.set('state', 'failed') - options._log.set('error', err) + options._log.set('message', '--page loaded--').snapshot().error(err) resolve() } @@ -526,6 +524,7 @@ type InvalidContentTypeError = Error & { interface InternalVisitOptions extends Partial { _log?: Log + hasAlreadyVisitedUrl: boolean } export default (Commands, Cypress, cy, state, config) => { @@ -852,7 +851,7 @@ export default (Commands, Cypress, cy, state, config) => { onLoad () {}, }) - options.hasAlreadyVisitedUrl = !!previousUrlVisited + options.hasAlreadyVisitedUrl = !!previouslyVisitedLocation if (!_.isUndefined(options.qs) && !_.isObject(options.qs)) { $errUtils.throwErrByPath('visit.invalid_qs', { args: { qs: String(options.qs) } }) @@ -1125,7 +1124,7 @@ export default (Commands, Cypress, cy, state, config) => { // if the origin currently matches // then go ahead and change the iframe's src if (remote.originPolicy === existing.originPolicy) { - previousUrlVisited = remote + previouslyVisitedLocation = remote url = $Location.fullyQualifyUrl(url) @@ -1138,10 +1137,10 @@ export default (Commands, Cypress, cy, state, config) => { // if we've already cy.visit'ed in the test and we are visiting a new origin, // throw an error, else we'd be in a endless loop, // we also need to disable retries to prevent the endless loop - if (previousUrlVisited) { + if (previouslyVisitedLocation) { $utils.getTestFromRunnable(state('runnable'))._retries = 0 - const params = { remote, existing, originalUrl, previousUrlVisited, log: options._log } + const params = { remote, existing, originalUrl, previouslyVisitedLocation, log: options._log } return cannotVisitDifferentOrigin(params) } @@ -1151,7 +1150,7 @@ export default (Commands, Cypress, cy, state, config) => { // origin which isn't allowed within a cy.origin block if (Cypress.isCrossOriginSpecBridge) { const existingAutOrigin = win ? $Location.create(win.location.href) : $Location.create(Cypress.state('currentActiveOriginPolicy')) - const params = { remote, existing, originalUrl, previousUrlVisited: existingAutOrigin, log: options._log, isCrossOriginSpecBridge: true, isPrimaryOrigin } + const params = { remote, existing, originalUrl, previouslyVisitedLocation: existingAutOrigin, log: options._log, isCrossOriginSpecBridge: true, isPrimaryOrigin } return isPrimaryOrigin ? cannotVisitPreviousOrigin(params) : cannotVisitDifferentOrigin(params) } @@ -1238,6 +1237,7 @@ export default (Commands, Cypress, cy, state, config) => { // not a network failure, and we should throw the original error if (err.isCallbackError || err.isCrossOrigin) { delete err.isCallbackError + delete err.isCrossOrigin throw err } diff --git a/packages/driver/src/cy/location.ts b/packages/driver/src/cy/location.ts index c7b3658db694..34b9d51dd158 100644 --- a/packages/driver/src/cy/location.ts +++ b/packages/driver/src/cy/location.ts @@ -15,7 +15,7 @@ export const create = (state) => ({ return location } catch (e) { // it is possible we do not have access to the location - // for example, if the app has redirected to a 2nd origin + // for example, if the app has redirected to a different origin return '' } }, diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 132564db7f9c..ea9b091e295a 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -262,15 +262,15 @@ export class CommandQueue extends Queue { // @ts-ignore run () { const next = () => { - // start at 0 index if one is not already set - let index = this.state('index') || this.state('index', 0) - // bail if we've been told to abort in case // an old command continues to run after if (this.stopped) { return } + // start at 0 index if one is not already set + let index = this.state('index') || this.state('index', 0) + const command = this.at(index) // if the command should be skipped, just bail and increment index diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d6f9c479d103..a0f3a9e5ab6d 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -125,7 +125,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert config: any Cypress: any Cookies: any - autoRun: boolean devices: { keyboard: Keyboard @@ -208,7 +207,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert private testConfigOverride: TestConfigOverride private commandFns: Record = {} - constructor (specWindow, Cypress, Cookies, state, config, autoRun = true) { + constructor (specWindow, Cypress, Cookies, state, config) { super() state('specWindow', specWindow) @@ -219,7 +218,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.config = config this.Cypress = Cypress this.Cookies = Cookies - this.autoRun = autoRun initVideoRecorder(Cypress) this.testConfigOverride = new TestConfigOverride() @@ -339,7 +337,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.ensureSubjectByType = ensures.ensureSubjectByType this.ensureRunnable = ensures.ensureRunnable - const snapshots = createSnapshots(jquery.$$, state) + const snapshots = createSnapshots(this.$$, state) this.createSnapshot = snapshots.createSnapshot this.detachDom = snapshots.detachDom @@ -476,10 +474,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert } initialize ($autIframe) { - const signalStable = () => { - this.isStable(true, 'load') - } - this.state('$autIframe', $autIframe) // dont need to worry about a try/catch here @@ -538,17 +532,15 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert cy.state('autOrigin', remoteLocation.originPolicy) this.Cypress.primaryOriginCommunicator.toAllSpecBridges('window:load', { url: remoteLocation.href }) - - signalStable() } catch (err: any) { // this catches errors thrown by user-registered event handlers // for `window:load`. this is used in the `catch` below so they // aren't mistaken as cross-origin errors err.isFromWindowLoadEvent = true - signalStable() - throw err + } finally { + this.isStable(true, 'load') } } catch (err: any) { if (err.isFromWindowLoadEvent) { @@ -723,9 +715,20 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert cy.warnMixingPromisesAndCommands() } - if (cy.autoRun) { - cy.queue.run() - } + cy.queue.run() + .then(() => { + const onQueueEnd = cy.state('onQueueEnd') + + if (onQueueEnd) { + onQueueEnd() + } + }) + .catch(() => { + // errors from the queue are propagated to cy.fail by the queue itself + // and can be safely ignored here. omitting this catch causes + // unhandled rejections to be logged because Bluebird sees a promise + // chain with no catch handler + }) } return chain diff --git a/packages/driver/src/dom/coordinates.ts b/packages/driver/src/dom/coordinates.ts index 371e108829f9..976f6665e9ef 100644 --- a/packages/driver/src/dom/coordinates.ts +++ b/packages/driver/src/dom/coordinates.ts @@ -25,6 +25,11 @@ const isAUTFrame = (win) => { // a cross-origin error, meaning this is the AUT // NOTE: this will need to be updated once we add support for // cross-origin iframes + if (err.name !== 'SecurityError') { + // re-throw any error that's not a cross-origin error + throw err + } + return true } } diff --git a/packages/driver/src/multi-domain/communicator.ts b/packages/driver/src/multi-domain/communicator.ts index c9f8cfa3e97f..0e98df5ee5ef 100644 --- a/packages/driver/src/multi-domain/communicator.ts +++ b/packages/driver/src/multi-domain/communicator.ts @@ -35,7 +35,7 @@ export class PrimaryOriginCommunicator extends EventEmitter { onMessage ({ data, source }) { // check if message is cross origin and if so, feed the message into // the cross origin bus with args and strip prefix - if (data?.event?.includes(CROSS_ORIGIN_PREFIX)) { + if (data?.event?.startsWith(CROSS_ORIGIN_PREFIX)) { const messageName = data.event.replace(CROSS_ORIGIN_PREFIX, '') // NOTE: need a special case here for 'bridge:ready' @@ -77,7 +77,7 @@ export class PrimaryOriginCommunicator extends EventEmitter { const preprocessedData = preprocessForSerialization(data) - // if user defined arguments are passed in, do NOT sanitize it. + // if user defined arguments are passed in, do NOT sanitize them. if (data?.args) { preprocessedData.args = data.args } @@ -96,7 +96,7 @@ export class PrimaryOriginCommunicator extends EventEmitter { const preprocessedData = preprocessForSerialization(data) - // if user defined arguments are passed in, do NOT sanitize it. + // if user defined arguments are passed in, do NOT sanitize them. if (data?.args) { preprocessedData.args = data.args } diff --git a/packages/driver/src/multi-domain/domain_fn.ts b/packages/driver/src/multi-domain/domain_fn.ts index bc91f5c40cc4..fbb4416cb9d0 100644 --- a/packages/driver/src/multi-domain/domain_fn.ts +++ b/packages/driver/src/multi-domain/domain_fn.ts @@ -101,6 +101,16 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { syncConfigToCurrentOrigin(config) syncEnvToCurrentOrigin(env) + cy.state('onQueueEnd', () => { + queueFinished = true + setRunnableStateToPassed() + Cypress.specBridgeCommunicator.toPrimary('queue:finished', { + subject: cy.state('subject'), + }, { + syncGlobals: true, + }) + }) + cy.state('onFail', (err) => { setRunnableStateToPassed() if (queueFinished) { @@ -155,16 +165,5 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { return } - - cy.queue.run() - .then(() => { - queueFinished = true - setRunnableStateToPassed() - Cypress.specBridgeCommunicator.toPrimary('queue:finished', { - subject: cy.state('subject'), - }, { - syncGlobals: true, - }) - }) }) } diff --git a/packages/driver/src/util/deferred.ts b/packages/driver/src/util/deferred.ts deleted file mode 100644 index e15423d51560..000000000000 --- a/packages/driver/src/util/deferred.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Bluebird from 'bluebird' - -export interface Deferred { - promise: Bluebird - resolve: (thenableOrResult?: unknown) => void - reject: (thenableOrResult?: unknown) => void -} - -export const createDeferred = () => { - const deferred = {} as Deferred - - deferred.promise = new Bluebird((resolve, reject) => { - deferred.resolve = resolve - deferred.reject = reject - }) - - return deferred -} diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index 4c470a0f8b9a..e310a6481e9f 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -3,6 +3,7 @@ /// /// /// +/// interface InternalWindowLoadDetails { type: 'same:origin' | 'cross:origin' | 'cross:origin:failure' @@ -88,6 +89,13 @@ declare namespace Cypress { (k: 'document', v?: Document): Document (k: 'window', v?: Window): Window (k: 'logGroupIds', v?: Array): Array + (k: 'autOrigin', v?: string): string + (k: 'originCommandBaseUrl', v?: string): string + (k: 'currentActiveOriginPolicy', v?: string): string + (k: 'latestActiveOriginPolicy', v?: string): string + (k: 'duringUserTestExecution', v?: boolean): boolean + (k: 'onQueueEnd', v?: () => void): () => void + (k: 'onFail', v?: (err: Error) => void): (err: Error) => void (k: string, v?: any): any state: Cypress.state } diff --git a/packages/driver/types/remote-state.d.ts b/packages/driver/types/remote-state.d.ts new file mode 100644 index 000000000000..443f83a66d85 --- /dev/null +++ b/packages/driver/types/remote-state.d.ts @@ -0,0 +1,14 @@ +declare namespace Cypress { + interface RemoteState { + auth?: Auth + domainName: string + strategy: 'file' | 'http' + origin: string + fileServer: string | null + props: Record + } + + interface RuntimeConfigOptions { + remote: RemoteState + } +} diff --git a/packages/extension/app/background.js b/packages/extension/app/background.js index b63de47a06ce..9856bbc68370 100644 --- a/packages/extension/app/background.js +++ b/packages/extension/app/background.js @@ -1,4 +1,4 @@ -/* global window */ +const get = require('lodash/get') const map = require('lodash/map') const pick = require('lodash/pick') const once = require('lodash/once') @@ -19,9 +19,17 @@ const firstOrNull = (cookies) => { return cookies[0] != null ? cookies[0] : null } -const connect = function (host, path, extraOpts) { - const isChromeLike = !!window.chrome && !window.browser +const checkIfFirefox = async () => { + if (!browser || !get(browser, 'runtime.getBrowserInfo')) { + return false + } + + const { name } = await browser.runtime.getBrowserInfo() + return name === 'Firefox' +} + +const connect = function (host, path, extraOpts) { const listenToCookieChanges = once(() => { return browser.cookies.onChanged.addListener((info) => { if (info.cause !== 'overwrite') { @@ -122,14 +130,21 @@ const connect = function (host, path, extraOpts) { } }) - ws.on('connect', () => { + ws.on('automation:config', async (config) => { + const isFirefox = await checkIfFirefox() + listenToCookieChanges() - // chrome-like browsers use CDP instead - if (!isChromeLike) { + // Non-Firefox browsers use CDP for these instead + if (isFirefox) { listenToDownloads() - listenToOnBeforeHeaders() + + if (config.experimentalSessionAndOrigin) { + listenToOnBeforeHeaders() + } } + }) + ws.on('connect', () => { ws.emit('automation:client:connected') }) diff --git a/packages/extension/test/integration/background_spec.js b/packages/extension/test/integration/background_spec.js index 03b4062e76e1..1558e0a966ac 100644 --- a/packages/extension/test/integration/background_spec.js +++ b/packages/extension/test/integration/background_spec.js @@ -4,6 +4,7 @@ const http = require('http') const socket = require('@packages/socket') const Promise = require('bluebird') const mockRequire = require('mock-require') +const client = require('../../app/client') const browser = { cookies: { @@ -25,9 +26,7 @@ const browser = { windows: { getLastFocused () {}, }, - runtime: { - - }, + runtime: {}, tabs: { query () {}, executeScript () {}, @@ -116,18 +115,23 @@ describe('app/background', () => { this.httpSrv = http.createServer() this.server = socket.server(this.httpSrv, { path: '/__socket.io' }) - this.onConnect = (callback) => { - const client = background.connect(`http://localhost:${PORT}`, '/__socket.io') - - client.on('connect', _.once(() => { - callback(client) - })) + const ws = { + on: sinon.stub(), + emit: sinon.stub(), } - this.stubEmit = (callback) => { - this.onConnect((client) => { - client.emit = _.once(callback) - }) + sinon.stub(client, 'connect').returns(ws) + + browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox' }), + + this.connect = async (options = {}) => { + const ws = background.connect(`http://localhost:${PORT}`, '/__socket.io') + + // skip 'connect' and 'automation:client:connected' and trigger + // the handler that kicks everything off + await ws.on.withArgs('automation:config').args[0][1](options) + + return ws } this.httpSrv.listen(PORT, done) @@ -142,72 +146,47 @@ describe('app/background', () => { }) context('.connect', () => { - it('can connect', function (done) { - this.server.on('connection', () => { - return done() - }) - - return background.connect(`http://localhost:${PORT}`, '/__socket.io') - }) + it('emits \'automation:client:connected\'', async function () { + const ws = background.connect(`http://localhost:${PORT}`, '/__socket.io') - it('emits \'automation:client:connected\'', (done) => { - const client = background.connect(`http://localhost:${PORT}`, '/__socket.io') + await ws.on.withArgs('connect').args[0][1]() - sinon.spy(client, 'emit') - - return client.on('connect', _.once(() => { - expect(client.emit).to.be.calledWith('automation:client:connected') - - return done() - })) + expect(ws.emit).to.be.calledWith('automation:client:connected') }) - it('listens to cookie changes', (done) => { + it('listens to cookie changes', async function () { const addListener = sinon.stub(browser.cookies.onChanged, 'addListener') - const client = background.connect(`http://localhost:${PORT}`, '/__socket.io') - return client.on('connect', _.once(() => { - expect(addListener).to.be.calledOnce + await this.connect() - return done() - })) + expect(addListener).to.be.calledOnce }) }) context('cookies', () => { - it('onChanged does not emit when cause is overwrite', function (done) { + it('onChanged does not emit when cause is overwrite', async function () { const addListener = sinon.stub(browser.cookies.onChanged, 'addListener') + const ws = await this.connect() + const fn = addListener.getCall(0).args[0] - this.onConnect((client) => { - sinon.spy(client, 'emit') - - const fn = addListener.getCall(0).args[0] - - fn({ cause: 'overwrite' }) + fn({ cause: 'overwrite' }) - expect(client.emit).not.to.be.calledWith('automation:push:request') - - done() - }) + expect(ws.emit).not.to.be.calledWith('automation:push:request') }) - it('onChanged emits automation:push:request change:cookie', function (done) { + it('onChanged emits automation:push:request change:cookie', async function () { const info = { cause: 'explicit', cookie: { name: 'foo', value: 'bar' } } - sinon.stub(browser.cookies.onChanged, 'addListener').yieldsAsync(info) + sinon.stub(browser.cookies.onChanged, 'addListener').yields(info) - this.stubEmit((req, msg, data) => { - expect(req).to.eq('automation:push:request') - expect(msg).to.eq('change:cookie') - expect(data).to.deep.eq(info) + const ws = await this.connect() - done() - }) + expect(ws.emit).to.be.calledWith('automation:push:request', 'change:cookie', info) }) }) context('downloads', () => { - it('onCreated emits automation:push:request create:download', function (done) { + it('onCreated emits automation:push:request create:download', async function () { const downloadItem = { id: '1', filename: '/path/to/download.csv', @@ -215,23 +194,19 @@ describe('app/background', () => { url: 'http://localhost:1234/download.csv', } - sinon.stub(browser.downloads.onCreated, 'addListener').yieldsAsync(downloadItem) + sinon.stub(browser.downloads.onCreated, 'addListener').yields(downloadItem) - this.stubEmit((req, msg, data) => { - expect(req).to.eq('automation:push:request') - expect(msg).to.eq('create:download') - expect(data).to.deep.eq({ - id: `${downloadItem.id}`, - filePath: downloadItem.filename, - mime: downloadItem.mime, - url: downloadItem.url, - }) + const ws = await this.connect() - done() + expect(ws.emit).to.be.calledWith('automation:push:request', 'create:download', { + id: `${downloadItem.id}`, + filePath: downloadItem.filename, + mime: downloadItem.mime, + url: downloadItem.url, }) }) - it('onChanged emits automation:push:request complete:download', function (done) { + it('onChanged emits automation:push:request complete:download', async function () { const downloadDelta = { id: '1', state: { @@ -239,34 +214,29 @@ describe('app/background', () => { }, } - sinon.stub(browser.downloads.onChanged, 'addListener').yieldsAsync(downloadDelta) + sinon.stub(browser.downloads.onChanged, 'addListener').yields(downloadDelta) - this.stubEmit((req, msg, data) => { - expect(req).to.eq('automation:push:request') - expect(msg).to.eq('complete:download') - expect(data).to.deep.eq({ id: `${downloadDelta.id}` }) + const ws = await this.connect() - done() + expect(ws.emit).to.be.calledWith('automation:push:request', 'complete:download', { + id: `${downloadDelta.id}`, }) }) - it('onChanged does not emit if state does not exist', function (done) { + it('onChanged does not emit if state does not exist', async function () { const downloadDelta = { id: '1', } const addListener = sinon.stub(browser.downloads.onChanged, 'addListener') - this.onConnect((client) => { - sinon.spy(client, 'emit') - addListener.getCall(0).args[0](downloadDelta) + const ws = await this.connect() - expect(client.emit).not.to.be.calledWith('automation:push:request') + addListener.getCall(0).args[0](downloadDelta) - done() - }) + expect(ws.emit).not.to.be.calledWith('automation:push:request') }) - it('onChanged does not emit if state.current is not "complete"', function (done) { + it('onChanged does not emit if state.current is not "complete"', async function () { const downloadDelta = { id: '1', state: { @@ -275,64 +245,68 @@ describe('app/background', () => { } const addListener = sinon.stub(browser.downloads.onChanged, 'addListener') - this.onConnect((client) => { - sinon.spy(client, 'emit') + const ws = await this.connect() - addListener.getCall(0).args[0](downloadDelta) + addListener.getCall(0).args[0](downloadDelta) - expect(client.emit).not.to.be.calledWith('automation:push:request') - - done() - }) + expect(ws.emit).not.to.be.calledWith('automation:push:request') }) - it('does not add downloads listener if in chrome-like browser', function (done) { - global.window.chrome = {} + it('does not add downloads listener if in non-Firefox browser', async function () { + browser.runtime.getBrowserInfo = undefined const onCreated = sinon.stub(browser.downloads.onCreated, 'addListener') const onChanged = sinon.stub(browser.downloads.onChanged, 'addListener') - this.onConnect(() => { - expect(onCreated).not.to.be.called - expect(onChanged).not.to.be.called + await this.connect() - done() - }) + expect(onCreated).not.to.be.called + expect(onChanged).not.to.be.called }) }) context('add header to aut iframe requests', () => { - it('does not add header if it is the top frame', function (done) { + const withExperimentalFlagOn = { + experimentalSessionAndOrigin: true, + } + + it('does not listen to `onBeforeSendHeaders` if experimental flag is off', async function () { + sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') + + await this.connect() + + expect(browser.webRequest.onBeforeSendHeaders.addListener).not.to.be.called + }) + + it('does not add header if it is the top frame', async function () { const details = { parentFrameId: -1, } sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - this.onConnect(() => { - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + await this.connect(withExperimentalFlagOn) - expect(result).to.be.undefined - done() - }) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + + expect(result).to.be.undefined }) - it('does not add header if it is a nested frame', function (done) { + it('does not add header if it is a nested frame', async function () { const details = { parentFrameId: 12345, } sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - this.onConnect(() => { - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + await this.connect(withExperimentalFlagOn) - expect(result).to.be.undefined - done() - }) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + + expect(result).to.be.undefined }) - it('does not add header if it is not a sub frame request', function (done) { + it('does not add header if it is not a sub frame request', async function () { const details = { parentFrameId: 0, type: 'stylesheet', @@ -340,15 +314,14 @@ describe('app/background', () => { sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - this.onConnect(() => { - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + await this.connect(withExperimentalFlagOn) - expect(result).to.be.undefined - done() - }) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + + expect(result).to.be.undefined }) - it('does not add header if it is a spec frame request', function (done) { + it('does not add header if it is a spec frame request', async function () { const details = { parentFrameId: 0, type: 'sub_frame', @@ -357,15 +330,13 @@ describe('app/background', () => { sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - this.onConnect(() => { - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) + await this.connect(withExperimentalFlagOn) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - expect(result).to.be.undefined - done() - }) + expect(result).to.be.undefined }) - it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', function (done) { + it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () { const details = { parentFrameId: 0, type: 'sub_frame', @@ -377,36 +348,31 @@ describe('app/background', () => { sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - this.onConnect(() => { - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - - expect(result).to.deep.equal({ - requestHeaders: [ - { - name: 'X-Foo', - value: 'Bar', - }, - { - name: 'X-Cypress-Is-AUT-Frame', - value: 'true', - }, - ], - }) + await this.connect(withExperimentalFlagOn) + const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - done() + expect(result).to.deep.equal({ + requestHeaders: [ + { + name: 'X-Foo', + value: 'Bar', + }, + { + name: 'X-Cypress-Is-AUT-Frame', + value: 'true', + }, + ], }) }) - it('does not add before-headers listener if in chrome-like browser', function (done) { - global.window.chrome = {} + it('does not add before-headers listener if in non-Firefox browser', async function () { + browser.runtime.getBrowserInfo = undefined const onBeforeSendHeaders = sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - this.onConnect(() => { - expect(onBeforeSendHeaders).not.to.be.called + await this.connect(withExperimentalFlagOn) - done() - }) + expect(onBeforeSendHeaders).not.to.be.called }) }) @@ -527,6 +493,9 @@ describe('app/background', () => { context('integration', () => { beforeEach(function (done) { done = _.once(done) + + client.connect.restore() + this.server.on('connection', (socket1) => { this.socket = socket1 diff --git a/packages/network/lib/index.ts b/packages/network/lib/index.ts index 19001918fa9b..90d94efa41cb 100644 --- a/packages/network/lib/index.ts +++ b/packages/network/lib/index.ts @@ -19,5 +19,3 @@ export { export { allowDestroy } from './allow-destroy' export { concatStream } from './concat-stream' - -export type { ParsedHost } from './types' diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 82697b51c37c..2455b4d0306c 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -413,7 +413,7 @@ interface EnsureSameSiteNoneProps { } const cookieSameSiteRegex = /SameSite=(\w+)/i -const cookieSecureRegex = /Secure/i +const cookieSecureRegex = /(^|\W)Secure(\W|$)/i const cookieSecureSemicolonRegex = /;\s*Secure/i const ensureSameSiteNone = ({ cookie, browser, isLocalhost, url }: EnsureSameSiteNoneProps) => { diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts index a3faa680f77c..912751d6132f 100644 --- a/packages/proxy/test/unit/http/response-middleware.spec.ts +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -808,9 +808,10 @@ describe('http/response-middleware', function () { ['SameSite=Invalid', output], ['SameSite=None', output], ['', output], - // When there's Secure and no SameSite, it ends up as + // When Secure is first or there's no SameSite, it ends up as // "Secure; SameSite=None" instead of "Secure" being second ['Secure', flippedOutput || output], + ['Secure; SameSite=None', flippedOutput || output], ] } @@ -821,12 +822,12 @@ describe('http/response-middleware', function () { describe('not Firefox', function () { makeScenarios('SameSite=None; Secure', 'Secure; SameSite=None').forEach(([input, output]) => { it(`${input} -> ${output}`, async function () { - const { appendStub, ctx } = prepareContextWithCookie(`cookie=value${input ? '; ' : ''}${input}`) + const { appendStub, ctx } = prepareContextWithCookie(`insecure=true; cookie=value${input ? '; ' : ''}${input}`) await testMiddleware([CopyCookiesFromIncomingRes], ctx) expect(appendStub).to.be.calledOnce - expect(appendStub).to.be.calledWith('Set-Cookie', `cookie=value; ${output}`) + expect(appendStub).to.be.calledWith('Set-Cookie', `insecure=true; cookie=value; ${output}`) }) }) }) @@ -834,7 +835,7 @@ describe('http/response-middleware', function () { describe('Firefox + non-localhost', function () { makeScenarios('SameSite=None; Secure', 'Secure; SameSite=None').forEach(([input, output]) => { it(`${input} -> ${output}`, async function () { - const { appendStub, ctx } = prepareContextWithCookie(`cookie=value${input ? '; ' : ''}${input}`, { + const { appendStub, ctx } = prepareContextWithCookie(`insecure=true; cookie=value${input ? '; ' : ''}${input}`, { req: { proxiedUrl: 'https://foobar.com' }, ...withFirefox, }) @@ -842,7 +843,7 @@ describe('http/response-middleware', function () { await testMiddleware([CopyCookiesFromIncomingRes], ctx) expect(appendStub).to.be.calledOnce - expect(appendStub).to.be.calledWith('Set-Cookie', `cookie=value; ${output}`) + expect(appendStub).to.be.calledWith('Set-Cookie', `insecure=true; cookie=value; ${output}`) }) }) }) @@ -850,7 +851,7 @@ describe('http/response-middleware', function () { describe('Firefox + https://localhost', function () { makeScenarios('SameSite=None; Secure', 'Secure; SameSite=None').forEach(([input, output]) => { it(`${input} -> ${output}`, async function () { - const { appendStub, ctx } = prepareContextWithCookie(`cookie=value${input ? '; ' : ''}${input}`, { + const { appendStub, ctx } = prepareContextWithCookie(`insecure=true; cookie=value${input ? '; ' : ''}${input}`, { req: { proxiedUrl: 'https://localhost:3500' }, ...withFirefox, }) @@ -858,7 +859,7 @@ describe('http/response-middleware', function () { await testMiddleware([CopyCookiesFromIncomingRes], ctx) expect(appendStub).to.be.calledOnce - expect(appendStub).to.be.calledWith('Set-Cookie', `cookie=value; ${output}`) + expect(appendStub).to.be.calledWith('Set-Cookie', `insecure=true; cookie=value; ${output}`) }) }) }) @@ -866,12 +867,12 @@ describe('http/response-middleware', function () { describe('Firefox + http://localhost', function () { makeScenarios('SameSite=None').forEach(([input, output]) => { it(`${input} -> ${output}`, async function () { - const { appendStub, ctx } = prepareContextWithCookie(`cookie=value${input ? '; ' : ''}${input}`, withFirefox) + const { appendStub, ctx } = prepareContextWithCookie(`insecure=true; cookie=value${input ? '; ' : ''}${input}`, withFirefox) await testMiddleware([CopyCookiesFromIncomingRes], ctx) expect(appendStub).to.be.calledOnce - expect(appendStub).to.be.calledWith('Set-Cookie', `cookie=value; ${output}`) + expect(appendStub).to.be.calledWith('Set-Cookie', `insecure=true; cookie=value; ${output}`) }) }) }) diff --git a/packages/runner-shared/src/event-manager.js b/packages/runner-shared/src/event-manager.js index 5a0e97d3aebd..07617f443138 100644 --- a/packages/runner-shared/src/event-manager.js +++ b/packages/runner-shared/src/event-manager.js @@ -127,10 +127,7 @@ export const eventManager = { _.each(socketToDriverEvents, (event) => { ws.on(event, (...args) => { - // these events are set up before Cypress is instantiated, so it's - // possible it's undefined when an event fires, but it's okay to - // ignore at that point - Cypress?.emit(event, ...args) + Cypress.emit(event, ...args) }) }) @@ -325,7 +322,7 @@ export const eventManager = { // The window.top should not change between test reloads, and we only need to bind the message event once // Forward all message events to the current instance of the multi-origin communicator window.top?.addEventListener('message', ({ data, source }) => { - Cypress?.primaryOriginCommunicator.onMessage({ data, source }) + Cypress.primaryOriginCommunicator.onMessage({ data, source }) }, false) }, diff --git a/packages/runner/webpack.config.ts b/packages/runner/webpack.config.ts index 5d46f3fa68b7..c31d396efce8 100644 --- a/packages/runner/webpack.config.ts +++ b/packages/runner/webpack.config.ts @@ -85,7 +85,7 @@ mainConfig.resolve = { // @ts-ignore const crossOriginConfig: webpack.Configuration = { - mode: 'development', + mode: 'production', ...getSimpleConfig(), entry: { cypress_cross_origin_runner: [path.resolve(__dirname, 'src/multi-domain.js')], diff --git a/packages/server/index.d.ts b/packages/server/index.d.ts index 26b8a0ecc533..6ec737bfe2a5 100644 --- a/packages/server/index.d.ts +++ b/packages/server/index.d.ts @@ -13,6 +13,8 @@ /// /// +/// + // types for the `server` package export namespace CyServer { // TODO: pull this from main types diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index bfb7cebb8a94..16645e833d8b 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -205,7 +205,7 @@ export class CdpAutomation { }) } - private onNetworkRequestWillBeSent = async (params: Protocol.Network.RequestWillBeSentEvent) => { + private onNetworkRequestWillBeSent = (params: Protocol.Network.RequestWillBeSentEvent) => { debugVerbose('received networkRequestWillBeSent %o', params) let url = params.request.url diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 1135f65cd908..6e8b28eea96a 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -258,7 +258,16 @@ const _connectToChromeRemoteInterface = function (port, onError, browserDisplayN .then((wsUrl) => { debug('received wsUrl %s for port %d', wsUrl, port) - return CriClient.create(wsUrl, onError) + return CriClient.create({ + target: wsUrl, + onError, + onReconnect (client) { + // if the client disconnects (e.g. due to a computer sleeping), update + // the frame tree on reconnect in cases there were changes while + // the client was disconnected + _updateFrameTree(client)() + }, + }) }) } @@ -336,13 +345,16 @@ const _updateFrameTree = (client) => async () => { debug('update frame tree') gettingFrameTree = new Promise(async (resolve) => { - frameTree = (await client.send('Page.getFrameTree')).frameTree - - debug('frame tree updated') - - gettingFrameTree = null - - resolve() + try { + frameTree = (await client.send('Page.getFrameTree')).frameTree + debug('frame tree updated') + } catch (err) { + debug('failed to update frame tree:', err.stack) + } finally { + gettingFrameTree = null + + resolve() + } }) } @@ -648,8 +660,11 @@ export = { await this._maybeRecordVideo(criClient, options, browser.majorVersion) await this._navigateUsingCRI(criClient, url) await this._handleDownloads(criClient, options.downloadsFolder, automation) - await this._handlePausedRequests(criClient) - _listenForFrameTreeChanges(criClient) + + if (options.experimentalSessionAndOrigin) { + await this._handlePausedRequests(criClient) + _listenForFrameTreeChanges(criClient) + } // return the launched browser process // with additional method to close the remote connection diff --git a/packages/server/lib/browsers/cri-client.ts b/packages/server/lib/browsers/cri-client.ts index 5ebbad6bcbf5..a9901d2b6c45 100644 --- a/packages/server/lib/browsers/cri-client.ts +++ b/packages/server/lib/browsers/cri-client.ts @@ -139,7 +139,14 @@ export { chromeRemoteInterface } type DeferredPromise = { resolve: Function, reject: Function } -export const create = async (target: websocketUrl, onAsynchronousError: Function): Promise => { +interface CriClientOptions { + target: websocketUrl + onError: Function + onReconnect?: (client: CRIWrapper) => void +} + +export const create = async (options: CriClientOptions): Promise => { + const { target, onError, onReconnect } = options const subscriptions: {eventName: CRI.EventName, cb: Function}[] = [] const enableCommands: CRI.Command[] = [] let enqueuedCommands: {command: CRI.Command, params: any, p: DeferredPromise }[] = [] @@ -180,8 +187,12 @@ export const create = async (target: websocketUrl, onAsynchronousError: Function }) enqueuedCommands = [] + + if (onReconnect) { + onReconnect(client) + } } catch (err) { - onAsynchronousError(errors.get('CDP_COULD_NOT_RECONNECT', err)) + onError(errors.get('CDP_COULD_NOT_RECONNECT', err)) } } @@ -285,9 +296,6 @@ export const create = async (target: websocketUrl, onAsynchronousError: Function return cri.close() }, - - // @ts-ignore - reconnect, } return client diff --git a/packages/server/lib/browsers/electron.js b/packages/server/lib/browsers/electron.js index 1c2e10090c1f..aaf60e9eba66 100644 --- a/packages/server/lib/browsers/electron.js +++ b/packages/server/lib/browsers/electron.js @@ -265,7 +265,9 @@ module.exports = { return this._enableDebugger(win.webContents) }) .then(() => { - this._listenToOnBeforeHeaders(win) + if (options.experimentalSessionAndOrigin) { + this._listenToOnBeforeHeaders(win) + } return this._handleDownloads(win, options.downloadsFolder, automation) }) diff --git a/packages/server/lib/browsers/firefox-util.ts b/packages/server/lib/browsers/firefox-util.ts index 832355cdd1b0..43a22fe97f26 100644 --- a/packages/server/lib/browsers/firefox-util.ts +++ b/packages/server/lib/browsers/firefox-util.ts @@ -103,7 +103,10 @@ const attachToTabMemory = Bluebird.method((tab) => { async function setupRemote (remotePort, automation, options) { const wsUrl = await protocol.getWsTargetFor(remotePort, 'Firefox') - const criClient = await CriClient.create(wsUrl, options.onError) + const criClient = await CriClient.create({ + target: wsUrl, + onError: options.onError, + }) const cdpAutomation = new CdpAutomation({ sendDebuggerCommandFn: criClient.send, onFn: criClient.on, diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index 9c102afd2cbc..4c74bd6d395d 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -510,7 +510,8 @@ export async function open (browser: Browser, url, options: any = {}, automation }) try { - await firefoxUtil.setup({ automation, + await firefoxUtil.setup({ + automation, extensions: launchOptions.extensions, url, foxdriverPort, diff --git a/packages/server/lib/controllers/files.js b/packages/server/lib/controllers/files.js index 76b34d7bec9c..1fdd9166c5a1 100644 --- a/packages/server/lib/controllers/files.js +++ b/packages/server/lib/controllers/files.js @@ -39,7 +39,7 @@ module.exports = { const iframeOptions = { title: this.getTitle(test), - domain: remoteStates.current().domainName, + domain: remoteStates.getPrimary().domainName, scripts: JSON.stringify(allFilesToSend), } diff --git a/packages/server/lib/controllers/runner.ts b/packages/server/lib/controllers/runner.ts index d12ce38bba78..a3bfec6069ec 100644 --- a/packages/server/lib/controllers/runner.ts +++ b/packages/server/lib/controllers/runner.ts @@ -69,7 +69,7 @@ export const runner = { // } // TODO: Find out what the problem. if (options.testingType === 'e2e') { - config.remote = remoteStates.current() + config.remote = remoteStates.getPrimary() } config.version = pkg.version diff --git a/packages/server/lib/remote_states.ts b/packages/server/lib/remote_states.ts index f9ceb3e6a056..836f237e5e2d 100644 --- a/packages/server/lib/remote_states.ts +++ b/packages/server/lib/remote_states.ts @@ -60,6 +60,14 @@ export class RemoteStates { return _.cloneDeep(state) } + getPrimary () { + const state = Array.from(this.remoteStates.entries())[0][1] + + debug('getting primary remote state: %o', state) + + return state + } + isInOriginStack (url: string): boolean { return this.originStack.includes(cors.getOriginPolicy(url)) } @@ -78,8 +86,8 @@ export class RemoteStates { const stateArray = Array.from(this.remoteStates.entries()) + // reset the remoteStates and originStack to the primary this.remoteStates = new Map([stateArray[0]]) - this.originStack = [stateArray[0][0]] } diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index 860054bd09b7..d83221961dc9 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -49,9 +49,6 @@ export const createCommonRoutes = ({ router.get('/__cypress/iframes/*', (req, res) => { if (testingType === 'e2e') { - // ensure the remote state gets cleaned up from any previous tests/runs - remoteStates.reset() - iframesController.e2e({ config, getSpec, remoteStates }, req, res) } @@ -69,9 +66,6 @@ export const createCommonRoutes = ({ router.get(clientRoute, (req, res) => { debug('Serving Cypress front-end by requested URL:', req.url) - // ensure the remote state gets cleaned up from any previous tests/runs - remoteStates.reset() - runner.serve(req, res, testingType === 'e2e' ? 'runner' : 'runner-ct', { config, testingType, diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index edad746878e2..1d33a4ea5851 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -118,7 +118,7 @@ export abstract class ServerBase { protected _netStubbingState?: NetStubbingState protected _httpsProxy?: httpsProxy protected _eventBus: EventEmitter - protected remoteStates: RemoteStates + protected _remoteStates: RemoteStates constructor () { this.isListening = false @@ -130,7 +130,7 @@ export abstract class ServerBase { this._baseUrl = null this._fileServer = null - this.remoteStates = new RemoteStates(() => { + this._remoteStates = new RemoteStates(() => { return { serverPort: this._port(), fileServerPort: this._fileServer?.port(), @@ -164,6 +164,10 @@ export abstract class ServerBase { return this.ensureProp(this._httpsProxy, 'open') } + get remoteStates () { + return this._remoteStates + } + setupCrossOriginRequestHandling () { this._eventBus.on('cross:origin:delaying:html', (request) => { this.socket.localBus.once('cross:origin:release:html', () => { @@ -212,7 +216,7 @@ export abstract class ServerBase { clientCertificates.loadClientCertificateConfig(config) - this.createNetworkProxy({ config, getCurrentBrowser, remoteStates: this.remoteStates, shouldCorrelatePreRequests }) + this.createNetworkProxy({ config, getCurrentBrowser, remoteStates: this._remoteStates, shouldCorrelatePreRequests }) if (config.experimentalSourceRewriting) { createInitialWorkers() @@ -223,7 +227,7 @@ export abstract class ServerBase { const routeOptions: InitializeRoutes = { config, specsStore, - remoteStates: this.remoteStates, + remoteStates: this._remoteStates, nodeProxy: this.nodeProxy, networkProxy: this._networkProxy!, onError, @@ -234,7 +238,7 @@ export abstract class ServerBase { } this.setupCrossOriginRequestHandling() - this.remoteStates.addEventListeners(this.socket.localBus) + this._remoteStates.addEventListeners(this.socket.localBus) const runnerSpecificRouter = testingType === 'e2e' ? createRoutesE2E(routeOptions) @@ -331,7 +335,7 @@ export abstract class ServerBase { options.onResetServerState = () => { this.networkProxy.reset() this.netStubbingState.reset() - this.remoteStates.reset() + this._remoteStates.reset() } const io = this.socket.startListening(this.server, automation, config, options) @@ -466,7 +470,7 @@ export abstract class ServerBase { const baseUrl = this._baseUrl ?? '' - return this.remoteStates.set(baseUrl) + return this._remoteStates.set(baseUrl) } _close () { diff --git a/packages/server/lib/server-ct.ts b/packages/server/lib/server-ct.ts index 92295bcf5938..63e2fa07baf7 100644 --- a/packages/server/lib/server-ct.ts +++ b/packages/server/lib/server-ct.ts @@ -38,7 +38,7 @@ export class ServerCt extends ServerBase { // once we open set the domain to root by default // which prevents a situation where navigating // to http sites redirects to /__/ cypress - this.remoteStates.set(baseUrl) + this._remoteStates.set(baseUrl) return resolve([port]) }) diff --git a/packages/server/lib/server-e2e.ts b/packages/server/lib/server-e2e.ts index 8dadffa18768..3188bb46b1cf 100644 --- a/packages/server/lib/server-e2e.ts +++ b/packages/server/lib/server-e2e.ts @@ -121,7 +121,7 @@ export class ServerE2E extends ServerBase { // once we open set the domain to root by default // which prevents a situation where navigating // to http sites redirects to /__/ cypress - this.remoteStates.set(baseUrl != null ? baseUrl : '') + this._remoteStates.set(baseUrl != null ? baseUrl : '') return resolve([port, warning]) }) @@ -159,8 +159,8 @@ export class ServerE2E extends ServerBase { const request = this.request let handlingLocalFile = false - const previousRemoteState = this.remoteStates.current() - const previousRemoteStateIsPrimary = this.remoteStates.isPrimaryOrigin(previousRemoteState.origin) + const previousRemoteState = this._remoteStates.current() + const previousRemoteStateIsPrimary = this._remoteStates.isPrimaryOrigin(previousRemoteState.origin) // nuke any hashes from our url since // those those are client only and do @@ -209,7 +209,7 @@ export class ServerE2E extends ServerBase { options.headers['x-cypress-authorization'] = this._fileServer.token - const state = this.remoteStates.set(urlStr, options) + const state = this._remoteStates.set(urlStr, options) urlFile = url.resolve(state.fileServer as string, urlStr) urlStr = url.resolve(state.origin as string, urlStr) @@ -299,7 +299,7 @@ export class ServerE2E extends ServerBase { !((options.hasAlreadyVisitedUrl || options.isCrossOrigin) && !cors.urlOriginsMatch(previousRemoteState.origin, newUrl))) { // if we're not handling a local file set the remote state if (!handlingLocalFile) { - this.remoteStates.set(newUrl as string, options) + this._remoteStates.set(newUrl as string, options) } const responseBufferStream = new stream.PassThrough({ @@ -322,7 +322,7 @@ export class ServerE2E extends ServerBase { restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary) } - details.isPrimaryOrigin = this.remoteStates.isPrimaryOrigin(newUrl!) + details.isPrimaryOrigin = this._remoteStates.isPrimaryOrigin(newUrl!) return resolve(details) }) @@ -334,7 +334,7 @@ export class ServerE2E extends ServerBase { } const restorePreviousRemoteState = (previousRemoteState: Cypress.RemoteState, previousRemoteStateIsPrimary: boolean) => { - this.remoteStates.set(previousRemoteState, { isCrossOrigin: !previousRemoteStateIsPrimary }) + this._remoteStates.set(previousRemoteState, { isCrossOrigin: !previousRemoteStateIsPrimary }) } // if they're POSTing an object, querystringify their POST body diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 96a14a6b3a98..6c5bf42f48eb 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -71,6 +71,7 @@ const retry = (fn: (res: any) => void) => { } export class SocketBase { + protected experimentalSessionAndOrigin: boolean protected ended: boolean protected _io?: socketIo.SocketIOServer protected testsDir: string | null @@ -78,6 +79,7 @@ export class SocketBase { localBus: EventEmitter constructor (config: Record) { + this.experimentalSessionAndOrigin = config.experimentalSessionAndOrigin this.ended = false this.testsDir = null this.localBus = new EventEmitter() @@ -204,6 +206,11 @@ export class SocketBase { debug('automation:client connected') + // only send the necessary config + automationClient.emit('automation:config', { + experimentalSessionAndOrigin: this.experimentalSessionAndOrigin, + }) + // if our automation disconnects then we're // in trouble and should probably bomb everything automationClient.on('disconnect', () => { diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 2af90355b334..d78eac863bf2 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -70,14 +70,14 @@ describe('lib/browsers/chrome', () => { return chrome.open('chrome', 'http://', {}, this.automation) .then(() => { expect(utils.getPort).to.have.been.calledOnce // to get remote interface port - expect(this.criClient.send.callCount).to.equal(6) expect(this.criClient.send).to.have.been.calledWith('Page.bringToFront') expect(this.criClient.send).to.have.been.calledWith('Page.navigate') expect(this.criClient.send).to.have.been.calledWith('Page.enable') expect(this.criClient.send).to.have.been.calledWith('Page.setDownloadBehavior') expect(this.criClient.send).to.have.been.calledWith('Network.enable') - expect(this.criClient.send).to.have.been.calledWith('Fetch.enable') + + expect(this.criClient.send.callCount).to.equal(5) }) }) @@ -365,6 +365,10 @@ describe('lib/browsers/chrome', () => { }) describe('adding header to AUT iframe request', function () { + const withExperimentalFlagOn = { + experimentalSessionAndOrigin: true, + } + beforeEach(function () { const frameTree = { frameTree: { @@ -388,8 +392,20 @@ describe('lib/browsers/chrome', () => { this.criClient.send.withArgs('Page.getFrameTree').resolves(frameTree) }) + it('does not listen to Fetch.requestPaused if experimental flag is off', async function () { + await chrome.open('chrome', 'http://', { experimentalSessionAndOrigin: false }, this.automation) + + expect(this.criClient.on).not.to.be.calledWith('Fetch.requestPaused') + }) + + it('sends Fetch.enable', async function () { + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) + + expect(this.criClient.send).to.have.been.calledWith('Fetch.enable') + }) + it('does not add header when not a document', async function () { - await chrome.open('chrome', 'http://', {}, this.automation) + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) this.criClient.on.withArgs('Fetch.requestPaused').yield({ requestId: '1234', @@ -402,7 +418,7 @@ describe('lib/browsers/chrome', () => { }) it('does not add header when it is a spec frame request', async function () { - await chrome.open('chrome', 'http://', {}, this.automation) + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) this.criClient.on.withArgs('Page.frameAttached').yield() @@ -421,7 +437,7 @@ describe('lib/browsers/chrome', () => { }) it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () { - await chrome.open('chrome', 'http://', {}, this.automation) + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) this.criClient.on.withArgs('Page.frameAttached').yield() @@ -453,7 +469,7 @@ describe('lib/browsers/chrome', () => { }) it('gets frame tree on Page.frameAttached', async function () { - await chrome.open('chrome', 'http://', {}, this.automation) + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) this.criClient.on.withArgs('Page.frameAttached').yield() @@ -461,7 +477,7 @@ describe('lib/browsers/chrome', () => { }) it('gets frame tree on Page.frameDetached', async function () { - await chrome.open('chrome', 'http://', {}, this.automation) + await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation) this.criClient.on.withArgs('Page.frameDetached').yield() diff --git a/packages/server/test/unit/browsers/cri-client_spec.ts b/packages/server/test/unit/browsers/cri-client_spec.ts index 597fa1463a4a..e8dae3f359ed 100644 --- a/packages/server/test/unit/browsers/cri-client_spec.ts +++ b/packages/server/test/unit/browsers/cri-client_spec.ts @@ -40,7 +40,12 @@ describe('lib/browsers/cri-client', function () { 'chrome-remote-interface': criImport, }) - getClient = () => criClient.create(DEBUGGER_URL, onError) + getClient = () => { + return criClient.create({ + target: DEBUGGER_URL, + onError, + }) + } }) context('.create', function () { diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index 42f4e2706670..1b2ec3bd5106 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -286,7 +286,21 @@ describe('lib/browsers/electron', () => { }) }) + it('does not listen to `onBeforeSendHeaders` if experimental flag is off', function () { + this.options.experimentalSessionAndOrigin = false + sinon.stub(this.win.webContents.session.webRequest, 'onBeforeSendHeaders') + + return electron._launch(this.win, this.url, this.automation, this.options) + .then(() => { + expect(this.win.webContents.session.webRequest.onBeforeSendHeaders).not.to.be.called + }) + }) + describe('adding header aut iframe requests', function () { + beforeEach(function () { + this.options.experimentalSessionAndOrigin = true + }) + it('does not add header if not a sub frame', function () { sinon.stub(this.win.webContents.session.webRequest, 'onBeforeSendHeaders') diff --git a/packages/server/test/unit/remote_states.spec.ts b/packages/server/test/unit/remote_states.spec.ts index 6ad429044c4d..ae3ce77a0e89 100644 --- a/packages/server/test/unit/remote_states.spec.ts +++ b/packages/server/test/unit/remote_states.spec.ts @@ -78,6 +78,44 @@ describe('remote states', () => { }) }) + context('#getPrimary', () => { + it('returns the primary when there is only the primary in remote states', function () { + const state = this.remoteStates.getPrimary() + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:3500', + strategy: 'http', + domainName: 'localhost', + fileServer: null, + props: { + port: '3500', + domain: '', + tld: 'localhost', + }, + }) + }) + + it('returns the primary when there are multiple remote states', function () { + this.remoteStates.set('https://staging.google.com/foo/bar', { isCrossOrigin: true }) + + const state = this.remoteStates.getPrimary() + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:3500', + strategy: 'http', + domainName: 'localhost', + fileServer: null, + props: { + port: '3500', + domain: '', + tld: 'localhost', + }, + }) + }) + }) + context('#isInOriginStack', () => { it('returns true when the requested url is in the origin stack', function () { const isInOriginStack = this.remoteStates.isInOriginStack('http://localhost:3500') diff --git a/system-tests/test/multi_domain_retries_spec.ts b/system-tests/test/multi_domain_retries_spec.ts index a45d623d9766..111e7b34d826 100644 --- a/system-tests/test/multi_domain_retries_spec.ts +++ b/system-tests/test/multi_domain_retries_spec.ts @@ -38,8 +38,9 @@ describe('e2e cy.origin retries', () => { const res = await exec() // verify that retrying tests with cy.origin doesn't cause serialization problems to spec bridges on test:before:run:async - expect(res.stdout).to.not.contain('TypeError') - expect(res.stdout).to.not.contain('Cannot set property message of which has only a getter') + expect(res.stdout).not.to.contain('TypeError') + expect(res.stdout).not.to.contain('Cannot set property message') + expect(res.stdout).not.to.contain('which has only a getter') expect(res.stdout).to.contain('AssertionError') expect(res.stdout).to.contain('expected true to be false') From d86674636b93a49a7e2dbbc24bf56666dc7d10b9 Mon Sep 17 00:00:00 2001 From: Matt Henkes Date: Fri, 22 Apr 2022 15:27:50 -0500 Subject: [PATCH 117/177] Update packages/runner-shared/src/event-manager.js Co-authored-by: Zach Bloomquist --- packages/runner-shared/src/event-manager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runner-shared/src/event-manager.js b/packages/runner-shared/src/event-manager.js index 07617f443138..755ac17efe2d 100644 --- a/packages/runner-shared/src/event-manager.js +++ b/packages/runner-shared/src/event-manager.js @@ -321,7 +321,9 @@ export const eventManager = { // The window.top should not change between test reloads, and we only need to bind the message event once // Forward all message events to the current instance of the multi-origin communicator - window.top?.addEventListener('message', ({ data, source }) => { + if (!window.top) throw new Error('missing window.top in event-manager') + + window.top.addEventListener('message', ({ data, source }) => { Cypress.primaryOriginCommunicator.onMessage({ data, source }) }, false) }, From b8ae51edbcbce77416b42cec620b023e2311b4ca Mon Sep 17 00:00:00 2001 From: mjhenkes Date: Fri, 22 Apr 2022 19:11:02 -0500 Subject: [PATCH 118/177] Remove reference to migration guide --- packages/driver/src/cypress/error_messages.ts | 3 +-- system-tests/projects/e2e/cypress/integration/session_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 28adaa64859e..4c41358263e4 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1657,8 +1657,7 @@ export default { if (experimentalSessionSupport) { return { message: stripIndent` - ${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0. Please see the migration guide for updating.`, - docsUrl: 'https://on.cypress.io/migration-guide', + ${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.`, } } diff --git a/system-tests/projects/e2e/cypress/integration/session_spec.js b/system-tests/projects/e2e/cypress/integration/session_spec.js index 10a87cf8c5df..a5de24bbeb1d 100644 --- a/system-tests/projects/e2e/cypress/integration/session_spec.js +++ b/system-tests/projects/e2e/cypress/integration/session_spec.js @@ -773,7 +773,7 @@ describe('errors', () => { it('throws error when experimentalSessionSupport is enabled through test config', { experimentalSessionAndOrigin: false, experimentalSessionSupport: true }, (done) => { cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0. Please see the migration guide for updating.') + expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') done() }) @@ -784,7 +784,7 @@ describe('errors', () => { Cypress.config('experimentalSessionSupport', true) cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0. Please see the migration guide for updating.') + expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') done() }) From f473d29b2c97eb4e23fcb9f20011bd1e08183088 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Fri, 22 Apr 2022 18:35:44 -0600 Subject: [PATCH 119/177] removing migration guide reference --- .../__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html | 2 +- packages/errors/src/errors.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html index e04c72ab0d68..9b86f4dc903b 100644 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html +++ b/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html @@ -36,5 +36,5 @@
The experimentalSessionSupport configuration option was removed in Cypress version 9.6.0 and replaced with experimentalSessionAndOrigin. Please update your config to use experimentalSessionAndOrigin instead.
 
-https://on.cypress.io/migration-guide
+https://on.cypress.io/session
 
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index c6dd76ca8c26..10e6113467bf 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1054,7 +1054,7 @@ export const AllCypressErrors = { return errTemplate`\ The ${fmt.highlight(`experimentalSessionSupport`)} configuration option was removed in ${fmt.cypressVersion(`9.6.0`)} and replaced with ${fmt.highlight(`experimentalSessionAndOrigin`)}. Please update your config to use ${fmt.highlight(`experimentalSessionAndOrigin`)} instead. - https://on.cypress.io/migration-guide` + https://on.cypress.io/session` }, EXPERIMENTAL_SHADOW_DOM_REMOVED: () => { return errTemplate`\ From 2d866f387c6f12ed5cf08a97ceae233f599da995 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 25 Apr 2022 12:39:03 -0500 Subject: [PATCH 120/177] release 9.6.0 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4bdf9869630..fd8138be6116 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "9.5.4", + "version": "9.6.0", "description": "Cypress.io end to end testing tool", "private": true, "scripts": { From 2338635d024d654e8afc6dfc98df737435804694 Mon Sep 17 00:00:00 2001 From: mjhenkes Date: Mon, 25 Apr 2022 16:10:43 -0500 Subject: [PATCH 121/177] chore: remove feature-multidomain branch from circle.yml --- circle.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 49a982791e36..919f1b9e606e 100644 --- a/circle.yml +++ b/circle.yml @@ -29,7 +29,6 @@ mainBuildFilters: &mainBuildFilters only: - develop - 10.0-release - - feature-multidomain - unify-1449-beta-slug-length # usually we don't build Mac app - it takes a long time @@ -39,7 +38,6 @@ macWorkflowFilters: &mac-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ feature-multidomain, << pipeline.git.branch >> ] - equal: [ unify-1449-beta-slug-length, << pipeline.git.branch >> ] - matches: pattern: "-release$" @@ -50,7 +48,6 @@ windowsWorkflowFilters: &windows-workflow-filters or: - equal: [ master, << pipeline.git.branch >> ] - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ feature-multidomain, << pipeline.git.branch >> ] - equal: [ unify-1449-beta-slug-length, << pipeline.git.branch >> ] - matches: pattern: "-release$" @@ -1658,7 +1655,7 @@ jobs: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "unify-1449-beta-slug-length" && "$CIRCLE_BRANCH" != "feature-multidomain" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "unify-1449-beta-slug-length" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi From 4972872a259a9b1dcabd09106e8ff84024f34031 Mon Sep 17 00:00:00 2001 From: Tyler Biethman Date: Tue, 26 Apr 2022 17:02:05 +0000 Subject: [PATCH 122/177] chore: Updating system test node modules cache logic to support binary tests (#21187) * Updating state cache key names to limit improper fallback matches. Sorting output for cache key for cross-arch determinism. * Trying to normalize cache output * Lost tracking for this change somehow * Changing key name to invalidate current cache. * Tweaking install script * Tweaking install script again * Another commit to test existing cache * Resetting keys for PR * Whoops, this slipped into the last commit * Missed one key rename somehow * Update system-tests/scripts/cache-key.sh Co-authored-by: Emily Rohrbough Co-authored-by: Emily Rohrbough --- circle.yml | 11 +++++------ system-tests/lib/dep-installer/index.ts | 3 +-- system-tests/scripts/cache-key.sh | 10 +++++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/circle.yml b/circle.yml index 919f1b9e606e..d80e6981b5df 100644 --- a/circle.yml +++ b/circle.yml @@ -201,7 +201,6 @@ commands: name: Restore system tests node_modules cache keys: - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache- update_cached_system_tests_deps: description: 'Update the cached node_modules for projects in "system-tests/projects/**"' @@ -215,7 +214,7 @@ commands: - restore_cache: name: Restore cache state, to check for known modules cache existence keys: - - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-state-{{ checksum "system_tests_cache_key" }} + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - run: name: Send root honeycomb event for this CI build command: cd system-tests/scripts && node ./send-root-honecomb-event.js @@ -238,11 +237,11 @@ commands: name: Save system tests node_modules cache key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} paths: - - ~/.cache/cy-system-tests-node-modules + - /tmp/cy-system-tests-node-modules - run: touch /tmp/system_tests_node_modules_installed - save_cache: name: Save system tests node_modules cache state key - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-state-{{ checksum "system_tests_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} paths: - /tmp/system_tests_node_modules_installed @@ -262,7 +261,7 @@ commands: command: echo $PLATFORM > platform_key - restore_cache: name: Restore cache state, to check for known modules cache existence - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-state-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }} - run: name: Bail if cache exists command: | @@ -304,7 +303,7 @@ commands: - run: touch node_modules_installed - save_cache: name: Saving node-modules cache state key - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-state-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }} paths: - node_modules_installed - save_cache: diff --git a/system-tests/lib/dep-installer/index.ts b/system-tests/lib/dep-installer/index.ts index 169ee5a57b28..2c9cb2789b84 100644 --- a/system-tests/lib/dep-installer/index.ts +++ b/system-tests/lib/dep-installer/index.ts @@ -1,6 +1,5 @@ import fs from 'fs-extra' import path from 'path' -import cachedir from 'cachedir' import execa from 'execa' import { cyTmpDir, projectPath, projects, root } from '../fixtures' import { getYarnCommand } from './yarn' @@ -162,7 +161,7 @@ export async function scaffoldProjectNodeModules (project: string, updateLockFil await execa(cmd, { cwd: projectDir, stdio: 'inherit', shell: true }) } - const cacheNodeModulesDir = path.join(cachedir('cy-system-tests-node-modules'), project, 'node_modules') + const cacheNodeModulesDir = path.join('/tmp', 'cy-system-tests-node-modules', project, 'node_modules') const tmpNodeModulesDir = path.join(projectPath(project), 'node_modules') async function removeWorkspacePackages (packages: string[]): Promise { diff --git a/system-tests/scripts/cache-key.sh b/system-tests/scripts/cache-key.sh index 482d8dd6ca36..73bb10d8fbce 100755 --- a/system-tests/scripts/cache-key.sh +++ b/system-tests/scripts/cache-key.sh @@ -5,4 +5,12 @@ # cd to this "scripts" directory cd "$(dirname "${BASH_SOURCE[0]}")" -cat ../projects/**/{package.json,yarn.lock} \ No newline at end of file +# Sort glob output, as it can vary based on architecture. LC_ALL=C required for locale-agnostic sort. +file_list=$(ls ../projects/**/{package.json,yarn.lock} | LC_ALL=C sort -f) + +contents='' +for t in ${file_list[@]}; do + contents+=$(<$t) +done + +echo $contents From 363c1a8a3ae5d2a5d20959190e7a548065671198 Mon Sep 17 00:00:00 2001 From: Bryan Lee Date: Wed, 27 Apr 2022 05:07:26 +0800 Subject: [PATCH 123/177] chore: replace git.io link with the original URL (#21206) --- packages/server/lib/project_static.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/project_static.ts b/packages/server/lib/project_static.ts index b216451b2662..417314ae3880 100644 --- a/packages/server/lib/project_static.ts +++ b/packages/server/lib/project_static.ts @@ -27,7 +27,7 @@ export async function getPathsAndIds () { const projectRoots: string[] = await cache.getProjectRoots() // this assumes that the configFile for a cached project is 'cypress.json' - // https://git.io/JeGyF + // https://github.com/cypress-io/cypress/pull/3246/files/774af917ce73d8129b8208a1d8c7b24f60e85557#r299968521 return Promise.all(projectRoots.map(async (projectRoot) => { return { path: projectRoot, @@ -136,7 +136,7 @@ export function remove (path) { export async function add (path, options) { // don't cache a project if a non-default configFile is set - // https://git.io/JeGyF + // https://github.com/cypress-io/cypress/pull/3246/files/774af917ce73d8129b8208a1d8c7b24f60e85557#r299968521 if (settings.configFile(options) !== 'cypress.json') { return Promise.resolve({ path }) } From 2709b69e9e05da18b2573dfb4896b89abd0e3793 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Tue, 26 Apr 2022 21:07:35 -0400 Subject: [PATCH 124/177] chore: cache Percy's Chromium browser at yarn install (#21218) --- circle.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/circle.yml b/circle.yml index d80e6981b5df..07e6760c2dfc 100644 --- a/circle.yml +++ b/circle.yml @@ -277,6 +277,9 @@ commands: - run: name: Install Node Modules command: | + # avoid installing Percy's Chromium every time we use @percy/cli + # https://docs.percy.io/docs/caching-asset-discovery-browser-in-ci + PERCY_POSTINSTALL_BROWSER=true \ yarn --prefer-offline --frozen-lockfile --cache-folder ~/.yarn no_output_timeout: 20m - prepare-modules-cache: From fbaaeb78b29763e48e7345e8221cc10624eccbe7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 01:42:52 +0000 Subject: [PATCH 125/177] =?UTF-8?q?chore(deps):=20update=20dependency=20@p?= =?UTF-8?q?ercy/cypress=20to=20^3.1.1=20=F0=9F=8C=9F=20(#21217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot Co-authored-by: Zach Bloomquist --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fd8138be6116..f213c28b0b05 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@cypress/request": "2.88.10", "@cypress/request-promise": "4.2.6", "@percy/cli": "1.0.0-beta.48", - "@percy/cypress": "^3.1.0", + "@percy/cypress": "^3.1.1", "@semantic-release/changelog": "5.0.1", "@semantic-release/git": "9.0.0", "@types/bluebird": "3.5.29", diff --git a/yarn.lock b/yarn.lock index b9f426ffe45b..de82f6507548 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6182,10 +6182,10 @@ rimraf "^3.0.2" ws "^7.4.1" -"@percy/cypress@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@percy/cypress/-/cypress-3.1.0.tgz#19533772739b35759c30e3edd3070922f6ea6b6d" - integrity sha512-x4RPo7vMEGA6hmLbQIFwawZZlmiZknhSqPyzckxXgj8VyoW17QQOINxZo/DZTfV0UoUVOzNoHNrJcV3XJnN4FQ== +"@percy/cypress@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@percy/cypress/-/cypress-3.1.1.tgz#4e7c5bdeccf1240b2150fc9d608df72c2f213d4b" + integrity sha512-khvWmCOJW7pxwDZPB5ovvbSe11FfNtH8Iyq8PHRYLD9ibAkiAWHZVs07bLK5wju1Q9X8s7zg5uj2yWxIlB1yjA== dependencies: "@percy/sdk-utils" "^1.0.0-beta.44" From f9ad6a820e929e5557ace0c8c71056884e254605 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 03:28:15 +0000 Subject: [PATCH 126/177] =?UTF-8?q?chore(deps):=20update=20dependency=20@p?= =?UTF-8?q?ercy/cli=20to=20v1.1.0=20=F0=9F=8C=9F=20(#21219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renovate Bot Co-authored-by: Zach Bloomquist --- package.json | 2 +- yarn.lock | 337 +++++++++++++++++++++------------------------------ 2 files changed, 139 insertions(+), 200 deletions(-) diff --git a/package.json b/package.json index f213c28b0b05..5fc6d249fa57 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@cypress/questions-remain": "1.0.1", "@cypress/request": "2.88.10", "@cypress/request-promise": "4.2.6", - "@percy/cli": "1.0.0-beta.48", + "@percy/cli": "1.1.0", "@percy/cypress": "^3.1.1", "@semantic-release/changelog": "5.0.1", "@semantic-release/git": "9.0.0", diff --git a/yarn.lock b/yarn.lock index de82f6507548..e4cee82f194e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5783,72 +5783,6 @@ node-gyp "^7.1.0" read-package-json-fast "^2.0.1" -"@oclif/command@^1.5.20", "@oclif/command@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.0.tgz#c1a499b10d26e9d1a611190a81005589accbb339" - integrity sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw== - dependencies: - "@oclif/config" "^1.15.1" - "@oclif/errors" "^1.3.3" - "@oclif/parser" "^3.8.3" - "@oclif/plugin-help" "^3" - debug "^4.1.1" - semver "^7.3.2" - -"@oclif/config@^1.15.1", "@oclif/config@^1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.0.tgz#ba8639118633102a7e481760c50054623d09fcab" - integrity sha512-Lmfuf6ubjQ4ifC/9bz1fSCHc6F6E653oyaRXxg+lgT4+bYf9bk+nqrUpAbrXyABkCqgIBiFr3J4zR/kiFdE1PA== - dependencies: - "@oclif/errors" "^1.3.3" - "@oclif/parser" "^3.8.0" - debug "^4.1.1" - globby "^11.0.1" - is-wsl "^2.1.1" - tslib "^2.0.0" - -"@oclif/errors@^1.2.2", "@oclif/errors@^1.3.3": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.4.tgz#a96f94536b4e25caa72eff47e8b3ed04f6995f55" - integrity sha512-pJKXyEqwdfRTUdM8n5FIHiQQHg5ETM0Wlso8bF9GodczO40mF5Z3HufnYWJE7z8sGKxOeJCdbAVZbS8Y+d5GCw== - dependencies: - clean-stack "^3.0.0" - fs-extra "^8.1" - indent-string "^4.0.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -"@oclif/linewrap@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@oclif/linewrap/-/linewrap-1.0.0.tgz#aedcb64b479d4db7be24196384897b5000901d91" - integrity sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw== - -"@oclif/parser@^3.8.0", "@oclif/parser@^3.8.3": - version "3.8.5" - resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.5.tgz#c5161766a1efca7343e1f25d769efbefe09f639b" - integrity sha512-yojzeEfmSxjjkAvMRj0KzspXlMjCfBzNRPkWw8ZwOSoNWoJn+OCS/m/S+yfV6BvAM4u2lTzX9Y5rCbrFIgkJLg== - dependencies: - "@oclif/errors" "^1.2.2" - "@oclif/linewrap" "^1.0.0" - chalk "^2.4.2" - tslib "^1.9.3" - -"@oclif/plugin-help@^3", "@oclif/plugin-help@^3.2.0": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.2.tgz#063ee08cee556573a5198fbdfdaa32796deba0ed" - integrity sha512-SPZ8U8PBYK0n4srFjCLedk0jWU4QlxgEYLCXIBShJgOwPhTTQknkUlsEwaMIevvCU4iCQZhfMX+D8Pz5GZjFgA== - dependencies: - "@oclif/command" "^1.5.20" - "@oclif/config" "^1.15.1" - "@oclif/errors" "^1.2.2" - chalk "^4.1.0" - indent-string "^4.0.0" - lodash.template "^4.4.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" - widest-line "^3.1.0" - wrap-ansi "^4.0.0" - "@octokit/app@2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@octokit/app/-/app-2.2.2.tgz#a1b8248f64159eeccbe4000d888fdae4163c4ad8" @@ -6070,117 +6004,105 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== -"@percy/cli-build@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.0.0-beta.48.tgz#7768f402090a2e91b5bd9461650aaa00821dcc7f" - integrity sha512-nvahMqLI7qhN5G73Yg20Ikwpe/bAd6aMkzt2FtkC0eHf1nF4Xw//dqoPDnIQa3psTY/mXJLufm4O9FPO5LNaJA== +"@percy/cli-build@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.1.0.tgz#0b154f482506f1abae4684758fd5f50447eb6dd8" + integrity sha512-99LDYYE5LPyRH6TL5mYapb0dNIQZkSrLq2I4ZyfTIUueoZ598QW1hg9IHo+URawFtjdFoMhaLlFGydMnizxCTw== dependencies: - "@percy/cli-command" "^1.0.0-beta.48" - "@percy/client" "^1.0.0-beta.48" - "@percy/env" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" + "@percy/cli-command" "1.1.0" -"@percy/cli-command@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.0.0-beta.48.tgz#61d93d9eb87253241f4be80d58cdeb4911471c6d" - integrity sha512-rYcL29tnkccDLOLoaYGlXzODzxNexHLv55sDDtAIX2XfdQfXQnxZN2iWJ+COd2YAoahUnODWHoVxFYcyIQaN8w== +"@percy/cli-command@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.1.0.tgz#820e3c1fc1a403a6887861e9c8e6584fcb0cfe63" + integrity sha512-SMU/gVqXLm5GlThadby0uP8psDOsrUfNUIaFPtiPDSm0dT2dAyN1CAyvioEPQ2znPPMQSVegCvH23lfcYM4RXA== dependencies: - "@oclif/command" "^1.8.0" - "@oclif/config" "^1.17.0" - "@oclif/plugin-help" "^3.2.0" - "@percy/config" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" + "@percy/config" "1.1.0" + "@percy/core" "1.1.0" + "@percy/logger" "1.1.0" -"@percy/cli-config@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.0.0-beta.48.tgz#1f7366bec101626a9f54f65b5c57588ef6d758a6" - integrity sha512-aI5CJSdTrBFUKrtzWmeA4m3MpmpKfG6XcoL198G23WYpBjOdkHTOcgG8JA1x+s1GMM2mBr+TOMsDLRPm8Hk/xQ== +"@percy/cli-config@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.1.0.tgz#65d0f6e4ba68a4fbf37fd0b6d5b2c8e20a5a6b5e" + integrity sha512-zLtyippRN402sNIDVJw1ZYPEXj5zWZgOJMUWfBq8NUbfxRzYYGCz0opMKGocx1fV8VfaFs2DywewRWKlhPM/Aw== dependencies: - "@oclif/command" "^1.8.0" - "@oclif/config" "^1.17.0" - "@percy/config" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" + "@percy/cli-command" "1.1.0" -"@percy/cli-exec@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.0.0-beta.48.tgz#48ca07effad64d53a45f741ae80181fff4087cbd" - integrity sha512-B1YTnniWJcLiEkR7dyACn2apS6abQ21qAEY/0A2Nhz3s7QiFj0sbmZcGn6HlUjpE0hA/tL/5EyfvlXVc8x+fNg== +"@percy/cli-exec@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.1.0.tgz#377670c92200b1963d4dc3b71001c01d2f524947" + integrity sha512-7c3mePcW1ZJnsv+BAaA6YzZcTM55Eaa4WFoBEMrPWvHGLHZe3O+gehbEFyooIsDJ1I/SplDNarH3j1L5m4GQFg== dependencies: - "@percy/cli-command" "^1.0.0-beta.48" - "@percy/core" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" + "@percy/cli-command" "1.1.0" cross-spawn "^7.0.3" which "^2.0.2" -"@percy/cli-snapshot@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.0.0-beta.48.tgz#332d271e653b286c79ed95700e0e38e0489b1d13" - integrity sha512-6XXCWffgUN8qb3pX241GAuwRNQy98L9yPbfRbfCELX9Lpvjrd7HrDeaN4pXHormrcz/zQMMD5Oqb6vvdJK0UxA== +"@percy/cli-snapshot@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.1.0.tgz#e418488967a5d30de047f8aceeb11e117dc386db" + integrity sha512-6MQhUGrRFl2g9PfqLXfTL1CVoBmamWu2fCjzfgmb9E/HHKIDzE07PrwszNzEXHhdzBHG9gRZ0Bt6kzZ/Fta5UQ== dependencies: - "@percy/cli-command" "^1.0.0-beta.48" - "@percy/config" "^1.0.0-beta.48" - "@percy/core" "^1.0.0-beta.48" - "@percy/dom" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" - globby "^11.0.1" - serve-handler "^6.1.3" - yaml "^1.10.0" + "@percy/cli-command" "1.1.0" + yaml "^2.0.0" -"@percy/cli-upload@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.0.0-beta.48.tgz#a85c5a9419128e26db940085f4cbb2eed9797160" - integrity sha512-XQ0ZY742j9yCNYO3IuIcCSiRIdRu+3hyaE7mbEg3jqPOqLRWKBwi2BxEUkUkK+RTP+it66DWhZtBIJEOYSsiuQ== +"@percy/cli-upload@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.1.0.tgz#99f8ee0a13c0e4cd80aa815adcb75d626846192e" + integrity sha512-P1GC3g0wYjy88h4zFBkInUcGWM8i99cJLyhKWg5WAK+bU5ot/Hc/4KgmNayHVxNJ/orcr1eHaP9pANbw82+igA== dependencies: - "@percy/cli-command" "^1.0.0-beta.48" - "@percy/client" "^1.0.0-beta.48" - "@percy/config" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" - globby "^11.0.1" + "@percy/cli-command" "1.1.0" + fast-glob "^3.2.11" image-size "^1.0.0" -"@percy/cli@1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.0.0-beta.48.tgz#e6e876f6253dea658039e4ae7095fa810ef24237" - integrity sha512-DS4d6EpN5Ejck8aKnTTmBrpMnVwaZEknUVVXvkmZik0vUAAs5ihEyn0PKKINqPcbHLvMa72wddNIYfBj0wsWgg== - dependencies: - "@oclif/plugin-help" "^3.2.0" - "@percy/cli-build" "^1.0.0-beta.48" - "@percy/cli-config" "^1.0.0-beta.48" - "@percy/cli-exec" "^1.0.0-beta.48" - "@percy/cli-snapshot" "^1.0.0-beta.48" - "@percy/cli-upload" "^1.0.0-beta.48" - -"@percy/client@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.0.0-beta.48.tgz#cdb8ada9e56b5703c19ce635a9d4b6865b8dc0a4" - integrity sha512-hYohEultQEcgkTgXf1Zyuu92h6cM0nxacy0TjBA/NLC0tlwQK2ZOyWsN3aCnwe3jiJ33Gt/+ryx4n/QbLj982w== +"@percy/cli@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.1.0.tgz#587ac4c9553e9d72fff7b5310a168b7ea61b5763" + integrity sha512-3GSXhYEr3FA3xJnYsSMbP+GGnV/JSTcPjCVdS/yYq+PUSZlc9/36z6OE55mj/GBbskwlZ0B4xEsKTccpDtZngw== + dependencies: + "@percy/cli-build" "1.1.0" + "@percy/cli-command" "1.1.0" + "@percy/cli-config" "1.1.0" + "@percy/cli-exec" "1.1.0" + "@percy/cli-snapshot" "1.1.0" + "@percy/cli-upload" "1.1.0" + "@percy/client" "1.1.0" + "@percy/logger" "1.1.0" + +"@percy/client@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.1.0.tgz#2f6f63d901604548c8df868fcf561e5896809601" + integrity sha512-AHbOlz/sAgTJeyMuDedW9+y/hYdKLneYEoE0kFTFQMelDzCn+CD7sW58tLLnBG8+wpjCAz65WGeK+3WNJ+R3IA== dependencies: - "@percy/env" "^1.0.0-beta.48" + "@percy/env" "1.1.0" + "@percy/logger" "1.1.0" -"@percy/config@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.0.0-beta.48.tgz#f34189a03e949ab506cf0dfeb69e27dacac1ecff" - integrity sha512-GDm9ry7tU5Ps96Jk5cPHbPT8n6IIXsxRpzevrlRIinKdht2QQ5w0LTDyucag1vzu+M1wUXF0Hk5PWj+M6/8ufQ== +"@percy/config@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.1.0.tgz#3b15f2f342c5f05690be26a92b04320ff95e3441" + integrity sha512-Lsbyf81B6T9qfUYZnPzKplnScY8NBOXtXwd+r7oYLpMny80c4hKQ1zi9W1aOfpoWIq7j5L7N68exEQjE465LLQ== dependencies: - "@percy/logger" "^1.0.0-beta.48" - ajv "^8.0.5" + "@percy/logger" "1.1.0" + ajv "^8.6.2" cosmiconfig "^7.0.0" - yaml "^1.10.0" + yaml "^2.0.0" -"@percy/core@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.0.0-beta.48.tgz#daf3cd3da78467c2478fec5fc6ada88106980046" - integrity sha512-5IyaV9+qdlhb2VbVLQRcG1eZ5FcBgK+l+FZX5mzBMZ6uxDzAIFIWI3SKgnePanZtEr//d1y6BoLhLB1LMJf27A== - dependencies: - "@percy/client" "^1.0.0-beta.48" - "@percy/config" "^1.0.0-beta.48" - "@percy/dom" "^1.0.0-beta.48" - "@percy/logger" "^1.0.0-beta.48" +"@percy/core@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.1.0.tgz#485911e3fbbe4cec1d3fc2fb74ee8df06565341c" + integrity sha512-k7uBp4vXSwICM9n0sRgQiPXkiUbjNbTY3wPDVGY0h/lQwKL2CESt2G+pVl5h7I7OHmNHDZEDgrqpGuq/kDkxvQ== + dependencies: + "@percy/client" "1.1.0" + "@percy/config" "1.1.0" + "@percy/dom" "1.1.0" + "@percy/logger" "1.1.0" + content-disposition "^0.5.4" cross-spawn "^7.0.3" extract-zip "^2.0.1" - progress "^2.0.3" + fast-glob "^3.2.11" + micromatch "^4.0.4" + mime-types "^2.1.34" + path-to-regexp "^6.2.0" rimraf "^3.0.2" - ws "^7.4.1" + ws "^8.0.0" "@percy/cypress@^3.1.1": version "3.1.1" @@ -6189,17 +6111,20 @@ dependencies: "@percy/sdk-utils" "^1.0.0-beta.44" -"@percy/dom@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.0.0-beta.48.tgz#b3b251a643b6e75571ce207c34d69c77eef43614" - integrity sha512-Kb2d3oPRVl28RaAWNwUaLa2fORbtw9xhmA/qoBXJ53YsuG10Ubf/5TZFIvovhQErXNGk/HLOqefvxLEyr6CNgg== +"@percy/dom@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.1.0.tgz#743f46eaf298dd501c38739f1d5c35020ab78112" + integrity sha512-6doaOh81Guv+aPMayMCTA3O1Pa/YwH2uwT5NGo5dNjCdeomk/BnA+ZhtTRiarW0ymF7q+z6paGhq3R+717YuUQ== -"@percy/env@^1.0.0-beta.48": - version "1.0.0-beta.48" - resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.0.0-beta.48.tgz#93756fadd82ef608f68d559b915a6f302cc3b421" - integrity sha512-nAlQydF/qxYTT9oDZt+dOpM8sKomGGdm3PJjKyZ+VIb1Kof0r/ZndsveJt8DXuB+WeXOeP37PQ1h9LiLDtpF7A== - dependencies: - dotenv "^8.2.0" +"@percy/env@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.1.0.tgz#b04e769db0b6b71136b3112555727920302b2f77" + integrity sha512-Y2lSuiP+zyLSYf7IV/95MgNqiL6nRDYk9Ji8CMZPKuvPIrnCDExZ7nqszCli3hJ4qi6U4m1NykJWPDei4GjZNw== + +"@percy/logger@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.1.0.tgz#6bb5df0563d3566071f8b718e61f11a04c5a2722" + integrity sha512-bAlxBcdnViTpGQZtjs361vXSlaxEj6Zt4Wt1Mo7EdPwv/zya2cBpLFNNcRycWto4mdb5Qnpht+IPXf7RFXJ/nw== "@percy/logger@^1.0.0-beta.48": version "1.0.0-beta.48" @@ -10721,7 +10646,7 @@ ajv@6.5.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@8.6.2, ajv@^8.0.0, ajv@^8.0.5: +ajv@8.6.2, ajv@^8.0.0: version "8.6.2" resolved "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== @@ -10751,6 +10676,16 @@ ajv@^7.0.2: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.6.2: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -13812,13 +13747,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clean-stack@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-3.0.1.tgz#155bf0b2221bf5f4fba89528d24c5953f17fe3a8" - integrity sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg== - dependencies: - escape-string-regexp "4.0.0" - clean-webpack-plugin@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-2.0.2.tgz#805a19ff20d46a06125298a25eb31142ecad2166" @@ -14613,6 +14541,13 @@ content-disposition@0.5.3, content-disposition@~0.5.2: dependencies: safe-buffer "5.1.2" +content-disposition@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + content-type@1.0.4, content-type@^1.0.4, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -18854,7 +18789,7 @@ 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, fast-glob@^3.2.9: +fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.11, fast-glob@^3.2.4, fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== @@ -19782,7 +19717,7 @@ fs-extra@7.0.1, fs-extra@^7.0.0, fs-extra@^7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@8.1.0, fs-extra@^8.0.0, fs-extra@^8.1, fs-extra@^8.1.0: +fs-extra@8.1.0, fs-extra@^8.0.0, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -26565,6 +26500,11 @@ mime-db@1.49.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -26591,6 +26531,13 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.21, mime-types@^2.1.27, dependencies: mime-db "1.49.0" +mime-types@^2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0, mime@^1.3.4, mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -29841,6 +29788,11 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" + integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -34197,7 +34149,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -34722,20 +34674,6 @@ serve-handler@6.1.2: path-to-regexp "2.2.1" range-parser "1.2.0" -serve-handler@^6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" - integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - fast-url-parser "1.1.3" - mime-types "2.1.18" - minimatch "3.0.4" - path-is-inside "1.0.2" - path-to-regexp "2.2.1" - range-parser "1.2.0" - serve-index@^1.7.2, serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -40667,15 +40605,6 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" -wrap-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131" - integrity sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg== - dependencies: - ansi-styles "^3.2.0" - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -40798,11 +40727,16 @@ ws@^6.0.0, ws@^6.1.2, ws@^6.2.1: dependencies: async-limiter "~1.0.0" -ws@^7.2.0, ws@^7.4.1, ws@~7.4.2: +ws@^7.2.0, ws@~7.4.2: version "7.4.4" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +ws@^8.0.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^8.1.0: version "8.2.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.0.tgz#0b738cd484bfc9303421914b11bb4011e07615bb" @@ -41010,6 +40944,11 @@ yaml@^1.10.0, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.1.tgz#71886d6021f3da28169dbefde78d4dd0f8d83650" + integrity sha512-1NpAYQ3wjzIlMs0mgdBmYzLkFgWBIWrzYVDYfrixhoFNNgJ444/jT2kUT2sicRbJES3oQYRZugjB6Ro8SjKeFg== + yargs-parser@13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" From 6b3e79d5638d1cf1f8933814c145c3bfc742baa6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 10:49:12 -0500 Subject: [PATCH 127/177] chore: Update Chrome (stable) to 101.0.4951.41 (#21222) Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> --- browser-versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-versions.json b/browser-versions.json index f304171fae27..f1393fd67dff 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { "chrome:beta": "101.0.4951.41", - "chrome:stable": "100.0.4896.127" + "chrome:stable": "101.0.4951.41" } From 7fa946d479e8d6ffc5e3630592bf5f57e4b53b04 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 28 Apr 2022 09:22:09 -0500 Subject: [PATCH 128/177] chore: update the release guide to ensure all changes have prs and tweak the order (#21194) --- guides/release-process.md | 57 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/guides/release-process.md b/guides/release-process.md index 69baa03ad4e1..b3d8b52b99a5 100644 --- a/guides/release-process.md +++ b/guides/release-process.md @@ -56,29 +56,20 @@ of Cypress. You can see the progress of the test projects by opening the status In the following instructions, "X.Y.Z" is used to denote the [next version of Cypress being published](./next-version.md). -1. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog. - -2. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. - - Use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub: - ```shell - cd packages/issues-in-release - yarn do:changelog --release - ``` - - Ensure the changelog is up-to-date and has the correct date. - - Merge any release-specific documentation changes into the main release PR. - - You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check. - -3. `develop` should contain all of the changes made in `master`. However, this occasionally may not be the case. +1. `develop` should contain all of the changes made in `master`. However, this occasionally may not be the case. - Ensure that `master` does not have any additional commits that are not on `develop`. - Ensure all auto-generated pull requests designed to merge master into develop have been successfully merged. + - If there are additional commits necessary to merge `master` to `develop`, submit, get approvals on, and merge a new PR + +2. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog. Also ensure that every closed issue in any obsolete releases are moved to the appropriate release in ZehHub. For example, if the open releases are 9.5.5 and 9.6.0, the current release is 9.6.0, then all closed issues marked as 9.5.5 should be moved to 9.6.0. Ensure that there are no commits on `develop` since the last release that are user facing and aren't marked with the current release. -4. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version. +3. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version. -5. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64//cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64//cypress.tgz`, publishing can proceed. +4. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64//cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64//cypress.tgz`, publishing can proceed. -6. [Set up](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress) an AWS SSO profile with the [Team-CypressApp-Prod](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress#Team-CypressApp-Prod) role. The release scripts assumes the name of your profile is `production`. If you have setup your credentials under a different profile, be sure to set the `AWS_PROFILE` environment variable. Log into AWS SSO with `aws sso login --profile `. +5. [Set up](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress) an AWS SSO profile with the [Team-CypressApp-Prod](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress#Team-CypressApp-Prod) role. The release scripts assumes the name of your profile is `production`. If you have setup your credentials under a different profile, be sure to set the `AWS_PROFILE` environment variable. Log into AWS SSO with `aws sso login --profile `. -7. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens: +6. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens: * the binaries for `` are moved from `beta` to the `desktop` folder for `` in S3 * the Cloudflare cache for this version is purged * the pre-prod `cypress.tgz` NPM package is converted to a stable NPM package ready for release @@ -89,22 +80,22 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy You can pass `--dry-run` to see the commands this would run under the hood. -8. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`. +7. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`. -9. Publish the generated npm package under the `dev` tag, using your personal npm account. +8. Publish the generated npm package under the `dev` tag, using your personal npm account. ```shell npm publish /tmp/cypress-prod.tgz --tag dev ``` -10. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output: +9. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output: ```shell dist-tags: dev: 3.4.0 latest: 3.3.2 ``` -11. Test `cypress@X.Y.Z` to make sure everything is working. +10. Test `cypress@X.Y.Z` to make sure everything is working. - Install the new version: `npm install -g cypress@X.Y.Z` - Run a quick, manual smoke test: - `cypress open` @@ -113,6 +104,16 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy - [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation. - Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress dashboard repo. +11. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. This PR must be merged, built, and deployed before moving to the next step. + - Use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub: + ```shell + cd packages/issues-in-release + yarn do:changelog --release + ``` + - Ensure the changelog is up-to-date and has the correct date. + - Merge any release-specific documentation changes into the main release PR. + - You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check. + 12. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version: ```shell @@ -134,24 +135,18 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy - Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date. - Move all issues that are still open from the current release to the appropriate future release. -17. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up: +17. Bump `version` in [`package.json`](package.json), submit, get approvals on, and merge a new PR for the change. After it merges: ```shell - git commit -am "release X.Y.Z [skip ci]" + git checkout develop + git pull origin develop git log --pretty=oneline # copy sha of the previous commit git tag -a vX.Y.Z git push origin vX.Y.Z ``` -18. Merge `develop` into `master` and push both branches up. Note: pushing to `master` will automatically publish any independent npm packages that have not yet been published. - - ```shell - git push origin develop - git checkout master - git merge develop - git push origin master - ``` +18. Submit, get approvals on, and merge a new PR that merges `develop` to `master` 19. Inside of [cypress-io/release-automations][release-automations]: - Publish GitHub release to [cypress-io/cypress/releases](https://github.com/cypress-io/cypress/releases) using package `set-releases`: From b62599481dd3cc25dc6462b94166e628b1296275 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 28 Apr 2022 13:49:05 -0400 Subject: [PATCH 129/177] chore: clean up root-level scripts (#21215) --- .gitignore | 2 - .vscode/launch.json | 14 ------ .vscode/tasks.json | 39 --------------- __snapshots__/mocha-snapshot-spec.js | 28 ----------- circle.yml | 16 ++---- package.json | 6 --- scripts/add_newlines_to_snapshots.js | 26 ---------- scripts/binary/s3-api-demo.ts | 21 -------- scripts/mocha-snapshot-spec.js | 22 -------- scripts/rename-workspace-packages.js | 24 --------- scripts/spec.js | 4 -- scripts/test-debug-package.js | 30 ----------- scripts/test-unique-npm-and-binary.js | 50 ------------------- scripts/test-unit.js | 32 ------------ ...-event.js => send-root-honeycomb-event.js} | 0 yarn.lock | 5 -- 16 files changed, 5 insertions(+), 314 deletions(-) delete mode 100644 .vscode/tasks.json delete mode 100644 __snapshots__/mocha-snapshot-spec.js delete mode 100644 scripts/add_newlines_to_snapshots.js delete mode 100644 scripts/binary/s3-api-demo.ts delete mode 100644 scripts/mocha-snapshot-spec.js delete mode 100644 scripts/rename-workspace-packages.js delete mode 100644 scripts/spec.js delete mode 100644 scripts/test-debug-package.js delete mode 100644 scripts/test-unique-npm-and-binary.js delete mode 100644 scripts/test-unit.js rename system-tests/scripts/{send-root-honecomb-event.js => send-root-honeycomb-event.js} (100%) diff --git a/.gitignore b/.gitignore index 8634a7f20da7..7168223e0fcb 100644 --- a/.gitignore +++ b/.gitignore @@ -103,8 +103,6 @@ system-tests/fixtures/large-img scripts/support binary-url.json -# Allows us to dynamically create eslint rules that override the default for Decaffeinate scripts -.eslintrc.js cli/visual-snapshots # Created by https://www.gitignore.io/api/osx,git,node,windows,intellij,linux diff --git a/.vscode/launch.json b/.vscode/launch.json index 4e76bbcdfd22..ff2618c4bf7d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,20 +8,6 @@ "processId": "${command:PickProcess}", "continueOnAttach": true }, - { - "type": "node", - "request": "launch", - "name": "test: active", - "runtimeExecutable": "yarn", - "runtimeArgs": [ - "test-debug-package" - ], - "args": [ - "${file}" - ], - "port": 5566, - "console": "integratedTerminal" - }, { "type": "node", "request": "attach", diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index c7b562dfbd02..000000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "rename yarn workspace packages", - "type": "shell", - "command": "node ./scripts/rename-workspace-packages --file ${file}", - "presentation": { - "echo": true, - "reveal": "silent", - "focus": false, - "panel": "shared", - "showReuseMessage": true, - "clear": false - }, - "problemMatcher": [] - }, - { - "label": "decaffeinate-bulk file", - "type": "shell", - "command": "yarn decaffeinate-bulk convert --file ${file}", - "problemMatcher": [] - }, - { - "label": "decaffeinate-bulk multiple files", - "type": "shell", - "command": "yarn decaffeinate-bulk convert --file ${file} ${file}", - "problemMatcher": [] - }, - { - "label": "decaffeinate-bulk dir", - "type": "shell", - "command": "yarn decaffeinate-bulk --dir ${fileDirname} convert", - "problemMatcher": [] - } - ] -} diff --git a/__snapshots__/mocha-snapshot-spec.js b/__snapshots__/mocha-snapshot-spec.js deleted file mode 100644 index 5e72aad2f262..000000000000 --- a/__snapshots__/mocha-snapshot-spec.js +++ /dev/null @@ -1,28 +0,0 @@ -exports['mocha snapshot captures mocha output 1'] = ` - - command: npm run test-mocha - code: 0 - failed: false - killed: false - signal: null - timedOut: false - - stdout: - ------- - > cypress@x.y.z test-mocha - > mocha --reporter spec scripts/spec.js - - - - mocha sanity check - Y works - - - 1 passing (