From 2eea5f0ce676a41a0674bb243dc7b26987dd7d1e Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Thu, 19 May 2022 15:53:49 -0600 Subject: [PATCH 1/6] chore (origin): handle waiting for aliased intercepts --- .../integration/commands/waiting_spec.js | 10 +- .../e2e/origin/commands/aliasing.spec.ts | 31 +- .../e2e/origin/commands/waiting.spec.ts | 358 +++++++++++++++++- packages/driver/src/cy/aliases.ts | 4 +- packages/driver/src/cy/commands/waiting.ts | 59 ++- packages/driver/src/cy/retries.ts | 2 +- packages/runner/webpack.config.ts | 3 +- 7 files changed, 429 insertions(+), 38 deletions(-) diff --git a/packages/driver/cypress/integration/commands/waiting_spec.js b/packages/driver/cypress/integration/commands/waiting_spec.js index 3134a9f38691..00ec884a2c46 100644 --- a/packages/driver/cypress/integration/commands/waiting_spec.js +++ b/packages/driver/cypress/integration/commands/waiting_spec.js @@ -68,7 +68,7 @@ describe('src/cy/commands/waiting', () => { return null }) - .wait('@fetch').then((xhr) => { + .wait('@fetch.response').then((xhr) => { expect(xhr.responseBody).to.deep.eq(response) }) }) @@ -302,7 +302,7 @@ describe('src/cy/commands/waiting', () => { .wait('getAny').then(() => {}) }) - it('throws when 2nd alias doesnt match any registered alias', (done) => { + it('throws when 2nd alias doesn\'t match any registered alias', (done) => { cy.on('fail', (err) => { expect(err.message).to.eq('`cy.wait()` could not find a registered alias for: `@bar`.\nAvailable aliases are: `foo`.') @@ -339,7 +339,7 @@ describe('src/cy/commands/waiting', () => { .wait(['@foo', 'bar']) }) - it('throws when 2nd alias isnt a route alias', (done) => { + it('throws when 2nd alias isn\'t a route alias', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('`cy.wait()` only accepts aliases for routes.\nThe alias: `bar` did not match a route.') expect(err.docsUrl).to.eq('https://on.cypress.io/wait') @@ -448,7 +448,7 @@ describe('src/cy/commands/waiting', () => { .wait(['@foo', 'bar']) }) - it('does not throw again when 2nd alias doesnt reference a route', { + it('does not throw again when 2nd alias doesn\'t reference a route', { requestTimeout: 100, }, (done) => { Promise.onPossiblyUnhandledRejection(done) @@ -1132,7 +1132,7 @@ describe('src/cy/commands/waiting', () => { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'wait', 'Waited For': 'getFoo, getBar', - Yielded: [xhrs[0], xhrs[1]], // explictly create the array here + Yielded: [xhrs[0], xhrs[1]], // explicitly create the array here }) }) }) diff --git a/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts b/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts index cd36fef8fbd2..2ff2273865d6 100644 --- a/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts +++ b/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts @@ -1,21 +1,37 @@ import { findCrossOriginLogs } from '../../../../support/utils' context('cy.origin aliasing', () => { + let logs: Map + beforeEach(() => { cy.visit('/fixtures/primary-origin.html') - cy.get('a[data-cy="dom-link"]').click() }) - it('.as()', () => { - cy.origin('http://foobar.com:3500', () => { - cy.get(':checkbox[name="colors"][value="blue"]').as('checkbox') - cy.get('@checkbox').click().should('be.checked') + context('.as()', () => { + it('supports dom elements inside origin', () => { + cy.get('a[data-cy="dom-link"]').click() + + cy.origin('http://foobar.com:3500', () => { + cy.get(':checkbox[name="colors"][value="blue"]').as('checkbox') + cy.get('@checkbox').click().should('be.checked') + }) + }) + + it('fails for dom elements outside origin', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.equal('`cy.get()` could not find a registered alias for: `@welcome_button`.\nYou have not aliased anything yet.') + done() + }) + + cy.get('[data-cy="welcome"]').as('welcome_button') + + cy.origin('http://foobar.com:3500', () => { + cy.get('@welcome_button').click() + }) }) }) context('#consoleProps', () => { - let logs: Map - beforeEach(() => { logs = new Map() @@ -25,6 +41,7 @@ context('cy.origin aliasing', () => { }) it('.as()', () => { + cy.get('a[data-cy="dom-link"]').click() cy.origin('http://foobar.com:3500', () => { cy.get('#button').as('buttonAlias') }) diff --git a/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts b/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts index d6f48af7acf9..7fcafce8a73e 100644 --- a/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts +++ b/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts @@ -1,33 +1,314 @@ import { findCrossOriginLogs } from '../../../../support/utils' -context('cy.origin waiting', () => { +declare global { + interface Window { + xhrGet: any + abortRequests: any + } +} + +let reqQueue: XMLHttpRequest[] = [] + +const xhrGet = (url) => { + const xhr = new window.XMLHttpRequest() + + xhr.open('GET', url) + reqQueue.push(xhr) + xhr.send() +} + +const abortRequests = () => { + reqQueue.forEach((xhr) => xhr.abort()) + reqQueue = [] +} + +context('cy.origin #.wait()', () => { + before(() => { + cy.origin('http://foobar.com:3500', () => { + let reqQueue: XMLHttpRequest[] = [] + + window.xhrGet = (url) => { + const xhr = new window.XMLHttpRequest() + + xhr.open('GET', url) + reqQueue.push(xhr) + xhr.send() + } + + window.abortRequests = () => { + reqQueue.forEach((xhr) => xhr.abort()) + reqQueue = [] + } + }) + }) + + let logs: Map + beforeEach(() => { + cy.origin('http://foobar.com:3500', () => { + window.abortRequests() + }) + + abortRequests() + + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + cy.visit('/fixtures/primary-origin.html') - cy.get('a[data-cy="cross-origin-secondary-link"]').click() }) - it('.wait()', () => { - cy.origin('http://foobar.com:3500', () => { - const delay = cy.spy(Cypress.Promise, 'delay') + context('number', () => { + it('waits for the specified value', () => { + cy.origin('http://foobar.com:3500', () => { + const delay = cy.spy(Cypress.Promise, 'delay') - cy.wait(50).then(() => { - expect(delay).to.be.calledWith(50, 'wait') + cy.wait(50).then(() => { + expect(delay).to.be.calledWith(50, 'wait') + }) }) }) }) - context('#consoleProps', () => { - let logs: Map + context('alias', () => { + it('waits for the route alias to have a request', () => { + cy.intercept('/foo', (req) => { + // delay the response so only the request will be available + req.reply({ + delay: 500, + }) + }).as('foo') + + cy.once('command:retry', () => xhrGet('/foo')) + + cy.origin('http://www.foobar.com:3500', () => { + cy.wait('@foo.request').then((xhr) => { + expect(xhr.request.url).to.include('/foo') + expect(xhr.response).to.be.undefined + }) + }) + }) + + it('waits for the route alias to have a response', () => { + const response = { foo: 'foo' } + + cy.intercept('/foo', (req) => { + // delay the response to ensure the wait will wait for response + req.reply({ + delay: 100, + body: response, + }) + }).as('foo') + + cy.once('command:retry', () => xhrGet('/foo')) + + cy.origin('http://www.foobar.com:3500', { args: { response } }, ({ response }) => { + cy.wait('@foo.response').then((xhr) => { + expect(xhr.request.url).to.include('/foo') + expect(xhr.response?.body).to.deep.equal(response) + }) + }) + }) + + it('waits for the route alias (aliased through \'as\')', () => { + const response = { foo: 'foo' } + + cy.intercept('/foo', response).as('foo') + + cy.origin('http://www.foobar.com:3500', { args: { response } }, ({ response }) => { + cy.then(() => window.xhrGet('/foo')) + cy.wait('@foo').its('response.body').should('deep.equal', response) + }) + }) + + it('waits for the route alias (aliased through req object)', () => { + const response = { foo: 'foo' } + + cy.intercept('/foo', (req) => { + req.reply(response) + req.alias = 'foo' + }) + + cy.origin('http://www.foobar.com:3500', { args: { response } }, ({ response }) => { + cy.then(() => window.xhrGet('/foo')) + + cy.wait('@foo').its('response.body').should('deep.equal', response) + }) + }) + + it('doesn\'t log when log: false', () => { + const response = { foo: 'foo' } + + cy.intercept('/foo', response).as('foo') + + cy.origin('http://www.foobar.com:3500', { args: { response } }, ({ response }) => { + cy.then(() => { + window.xhrGet('/foo') + }) + + cy.wait('@foo', { log: false }) + }) + + cy.shouldWithTimeout(() => { + const expectedLogs = findCrossOriginLogs('wait', logs, 'localhost') + + expect(expectedLogs).to.be.empty + }) + }) + + it('waits for multiple aliases', () => { + const fooResponse = { foo: 'foo' } + const barResponse = { bar: 'bar' } - beforeEach(() => { - logs = new Map() + cy.intercept('/foo', fooResponse).as('foo') + cy.intercept('/bar', barResponse).as('bar') - cy.on('log:changed', (attrs, log) => { - logs.set(attrs.id, log) + cy.origin('http://www.foobar.com:3500', { args: { fooResponse, barResponse } }, ({ fooResponse, barResponse }) => { + cy.then(() => { + window.xhrGet('/foo') + window.xhrGet('/bar') + }) + + cy.wait(['@foo', '@bar']).then((interceptions) => { + expect(interceptions[0].response?.body).to.deep.equal(fooResponse) + expect(interceptions[1].response?.body).to.deep.equal(barResponse) + }) + }) + }) + + it('waits for multiple aliases using separate waits', () => { + const fooResponse = { foo: 'foo' } + const barResponse = { bar: 'bar' } + + cy.intercept('/foo', fooResponse).as('foo') + cy.intercept('/bar', barResponse).as('bar') + + cy.origin('http://www.foobar.com:3500', { args: { fooResponse, barResponse } }, ({ fooResponse, barResponse }) => { + cy.then(() => { + window.xhrGet('/foo') + window.xhrGet('/bar') + }) + + cy.wait('@foo').then((interception) => { + expect(interception.response?.body).to.deep.equal(fooResponse) + }) + .wait('@bar').then((interception) => { + expect(interception.response?.body).to.deep.equal(barResponse) + }) + }) + }) + + context('errors', () => { + it('throws when request is not made', { requestTimeout: 100 }, (done) => { + cy.on('fail', (err) => { + expect(err.message).to.equal('Timed out retrying after 100ms: `cy.wait()` timed out waiting `100ms` for the 1st request to the route: `not_called`. No request ever occurred.') + expect(err.docsUrl).to.equal('https://on.cypress.io/wait') + done() + }) + + cy.intercept('/not_called').as('not_called') + + cy.origin('http://www.foobar.com:3500', () => { + cy.wait('@not_called') + }) + }) + + it('throw when alias doesn\'t match any registered alias', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.equal('`cy.wait()` could not find a registered alias for: `@not_found`.\nAvailable aliases are: `foo`.') + done() + }) + + cy.intercept('/foo', {}).as('foo') + + cy.origin('http://www.foobar.com:3500', () => { + cy.wait('@not_found') + }) + }) + + it('throws when 2nd alias doesn\'t match any registered alias', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('`cy.wait()` could not find a registered alias for: `@bar`.\nAvailable aliases are: `foo`.') + + done() + }) + + cy.intercept('/foo', {}).as('foo') + + cy.origin('http://www.foobar.com:3500', () => { + cy.then(() => window.xhrGet('/foo')) + .wait(['@foo', '@bar']) + }) + }) + + it('throws when alias is missing \'@\' but matches an available alias', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('Invalid alias: `foo`.\nYou forgot the `@`. It should be written as: `@foo`.') + + done() + }) + + cy.intercept('/foo', {}).as('foo') + + cy.origin('http://www.foobar.com:3500', () => { + cy.wait('foo') + }) + }) + + it('throws when 2nd alias isn\'t a route alias', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('`cy.wait()` only accepts aliases for routes.\nThe alias: `bar` did not match a route.') + expect(err.docsUrl).to.eq('https://on.cypress.io/wait') + + done() + }) + + cy.intercept('/foo', {}).as('foo') + cy.get('body').as('bar') + + cy.origin('http://www.foobar.com:3500', () => { + cy.then(() => window.xhrGet('/foo')) + .wait(['@foo', '@bar']) + }) + }) + + it('throws when foo cannot resolve', { requestTimeout: 100 }, (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('`cy.wait()` timed out waiting `100ms` for the 1st request to the route: `foo`. No request ever occurred.') + + done() + }) + + cy.once('command:retry', () => xhrGet('/bar')) + + cy.intercept('/foo', {}).as('foo') + cy.intercept('/bar', {}).as('bar') + + cy.origin('http://www.foobar.com:3500', () => { + cy.wait(['@foo', '@bar']) + }) + }) + + it('throws when passed multiple string arguments', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('`cy.wait()` was passed invalid arguments. You cannot pass multiple strings. If you\'re trying to wait for multiple routes, use an array.') + expect(err.docsUrl).to.eq('https://on.cypress.io/wait') + + done() + }) + + cy.origin('http://www.foobar.com:3500', () => { + // @ts-ignore + cy.wait('@foo', '@bar') + }) }) }) + }) - it('.wait()', () => { + context('#consoleProps', () => { + it('number', () => { cy.origin('http://foobar.com:3500', () => { cy.wait(200) }) @@ -36,7 +317,54 @@ context('cy.origin waiting', () => { 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') + expect(consoleProps['Waited For']).to.equal('200ms before continuing') + }) + }) + + context('alias', () => { + it('waiting on one alias', () => { + cy.intercept('/foo', {}).as('foo') + + cy.origin('http://www.foobar.com:3500', () => { + cy.then(() => window.xhrGet('/foo')) + cy.wait('@foo') + }) + + cy.shouldWithTimeout(() => { + const log = findCrossOriginLogs('wait', logs, 'localhost') + const consoleProps = log.consoleProps() + + expect(consoleProps.Command).to.equal('wait') + expect(consoleProps['Waited For']).to.equal('foo') + expect(consoleProps.Yielded).to.equal(Cypress.state('routes')[consoleProps.Yielded.routeId].requests[consoleProps.Yielded.id]) + }) + }) + + it('waiting on multiple aliases', () => { + cy.intercept('/foo', {}).as('foo') + cy.intercept('/bar', {}).as('bar') + + cy.origin('http://www.foobar.com:3500', () => { + cy.then(() => { + window.xhrGet('/foo') + window.xhrGet('/bar') + }) + + cy.wait(['@foo', '@bar']) + }) + + cy.shouldWithTimeout(() => { + const log = findCrossOriginLogs('wait', logs, 'localhost') + const consoleProps = log.consoleProps() + + expect(consoleProps.Command).to.equal('wait') + expect(consoleProps['Waited For']).to.equal('foo, bar') + const routes = Cypress.state('routes') + const yielded = consoleProps.Yielded + const expectedRoutes = [routes[yielded[0].routeId].requests[yielded[0].id], routes[yielded[1].routeId].requests[yielded[1].id]] + + expect(yielded).to.deep.equal(expectedRoutes) + }) }) }) }) diff --git a/packages/driver/src/cy/aliases.ts b/packages/driver/src/cy/aliases.ts index 3aa87e485ed9..ddd27ce60655 100644 --- a/packages/driver/src/cy/aliases.ts +++ b/packages/driver/src/cy/aliases.ts @@ -31,14 +31,14 @@ export const create = (cy: $Cy) => ({ getAlias (name, cmd, log) { const aliases = cy.state('aliases') || {} - // bail if the name doesnt reference an alias + // bail if the name doesn't reference an alias if (!aliasRe.test(name)) { return } + // slice off the '@' const alias = aliases[name.slice(1)] - // slice off the '@' if (!alias) { this.aliasNotFoundFor(name, cmd, log) } diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index cfcbfafe5649..716861c6fe45 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -5,6 +5,7 @@ import { isDynamicAliasingPossible } from '../net-stubbing/aliasing' import ordinal from 'ordinal' import $errUtils from '../../cypress/error_utils' +import { preprocessForSerialization } from '../../util/serialization' const getNumRequests = (state, alias) => { const requests = state('aliasRequests') || {} @@ -53,10 +54,13 @@ export default (Commands, Cypress, cy, state) => { } const waitString = (subject, str, options) => { - let log + let log: Cypress.InternalLogConfig & Cypress.Log if (options.log !== false) { log = options._log = Cypress.log({ + displayName: 'wait', + name: 'wait', + message: '', type: 'parent', aliasType: 'route', // avoid circular reference @@ -270,8 +274,51 @@ export default (Commands, Cypress, cy, state) => { }) } + Cypress.primaryOriginCommunicator.on('wait:for:xhr', ({ args: [str, options] }, originPolicy) => { + options.isCrossOriginSpecBridge = true + waitString(null, str, options).then((responses) => { + // remove props that aren't serializable (these aren't used by consumers so are okay to remove) + const omitFn = (response) => _.omit(response, ['subscriptions', 'setLogFlag']) + const preprocessedResponses = _.isArray(responses) ? responses.map(omitFn) : omitFn(responses) + + Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { responses: preprocessedResponses }) + }).catch((err) => { + options._log?.error(err) + Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { err: preprocessForSerialization(err) }) + }) + }) + + const deferToPrimaryOrigin = ([_subject, str, options]) => { + return new Promise((resolve, reject) => { + Cypress.specBridgeCommunicator.once('wait:for:xhr:end', ({ responses, err }) => { + if (err) { + if (options.log) { + Cypress.state('onBeforeLog', (log) => { + // skip this 'wait' log since it was already added through the primary + if (log.get('name') === 'wait') { + // unbind this function so we don't impact any other logs + cy.state('onBeforeLog', null) + + return false + } + + return + }) + } + + reject(err) + } + + resolve(responses) + }) + + // subject is not needed when waiting on aliased requests since the request/response will be yielded + Cypress.specBridgeCommunicator.toPrimary('wait:for:xhr', { args: [str, options] }) + }) + } + Commands.addAll({ prevSubject: 'optional' }, { - wait (subject, msOrAlias, options = {}) { + wait (subject, msOrAlias, options: { log?: boolean } = {}) { // check to ensure options is an object // if its a string the user most likely is trying // to wait on multiple aliases and forget to make this @@ -292,11 +339,11 @@ export default (Commands, Cypress, cy, state) => { return waitNumber.apply(window, args) } - if (_.isString(msOrAlias)) { - return waitString.apply(window, args) - } + if (_.isString(msOrAlias) || (_.isArray(msOrAlias) && !_.isEmpty(msOrAlias))) { + if (Cypress.isCrossOriginSpecBridge) { + return deferToPrimaryOrigin(args) + } - if (_.isArray(msOrAlias) && !_.isEmpty(msOrAlias)) { return waitString.apply(window, args) } diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index c8fb7b800662..94ba98515f82 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -80,7 +80,7 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou const autOrigin = Cypress.state('autOrigin') const commandOrigin = window.location.origin - if (autOrigin && !cors.urlOriginsMatch(commandOrigin, autOrigin)) { + if (!options.isCrossOriginSpecBridge && autOrigin && !cors.urlOriginsMatch(commandOrigin, autOrigin)) { const appendMsg = errByPath('miscellaneous.cross_origin_command', { commandOrigin, autOrigin, diff --git a/packages/runner/webpack.config.ts b/packages/runner/webpack.config.ts index 64173be899e1..2b96d64f9bd4 100644 --- a/packages/runner/webpack.config.ts +++ b/packages/runner/webpack.config.ts @@ -85,8 +85,7 @@ mainConfig.resolve = { // @ts-ignore const crossOriginConfig: webpack.Configuration = { - mode: 'production', - ...getSimpleConfig(), + ...getCommonConfig(), entry: { cypress_cross_origin_runner: [path.resolve(__dirname, 'src/cross-origin.js')], }, From cad53de5e3a3b059b0705ea73b81bc7402db39c4 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Thu, 19 May 2022 21:47:45 -0600 Subject: [PATCH 2/6] moving logs back --- .../cypress/integration/e2e/origin/commands/aliasing.spec.ts | 5 +++-- packages/driver/src/cy/commands/waiting.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts b/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts index 2ff2273865d6..0b879b654f6b 100644 --- a/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts +++ b/packages/driver/cypress/integration/e2e/origin/commands/aliasing.spec.ts @@ -1,8 +1,6 @@ import { findCrossOriginLogs } from '../../../../support/utils' context('cy.origin aliasing', () => { - let logs: Map - beforeEach(() => { cy.visit('/fixtures/primary-origin.html') }) @@ -32,6 +30,8 @@ context('cy.origin aliasing', () => { }) context('#consoleProps', () => { + let logs: Map + beforeEach(() => { logs = new Map() @@ -42,6 +42,7 @@ context('cy.origin aliasing', () => { it('.as()', () => { cy.get('a[data-cy="dom-link"]').click() + cy.origin('http://foobar.com:3500', () => { cy.get('#button').as('buttonAlias') }) diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index 716861c6fe45..dc088c65236a 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -54,7 +54,7 @@ export default (Commands, Cypress, cy, state) => { } const waitString = (subject, str, options) => { - let log: Cypress.InternalLogConfig & Cypress.Log + let log if (options.log !== false) { log = options._log = Cypress.log({ From d6c91aa6b997b76c89c033101db0069db355e4c9 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Thu, 19 May 2022 22:15:26 -0600 Subject: [PATCH 3/6] rename --- packages/driver/src/cy/commands/waiting.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index dc088c65236a..a515accd93a1 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -288,7 +288,7 @@ export default (Commands, Cypress, cy, state) => { }) }) - const deferToPrimaryOrigin = ([_subject, str, options]) => { + const delegateToPrimaryOrigin = ([_subject, str, options]) => { return new Promise((resolve, reject) => { Cypress.specBridgeCommunicator.once('wait:for:xhr:end', ({ responses, err }) => { if (err) { @@ -341,7 +341,7 @@ export default (Commands, Cypress, cy, state) => { if (_.isString(msOrAlias) || (_.isArray(msOrAlias) && !_.isEmpty(msOrAlias))) { if (Cypress.isCrossOriginSpecBridge) { - return deferToPrimaryOrigin(args) + return delegateToPrimaryOrigin(args) } return waitString.apply(window, args) From 133d56b7ce9096331e62fccde660e0c80b7e8646 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Fri, 20 May 2022 00:12:23 -0600 Subject: [PATCH 4/6] fixing test --- .../e2e/origin/commands/waiting.spec.ts | 29 ++++++++++++++++++- packages/driver/src/cy/commands/waiting.ts | 13 +++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts b/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts index 7fcafce8a73e..001b628ce533 100644 --- a/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts +++ b/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts @@ -22,7 +22,7 @@ const abortRequests = () => { reqQueue = [] } -context('cy.origin #.wait()', () => { +context('cy.origin waiting', () => { before(() => { cy.origin('http://foobar.com:3500', () => { let reqQueue: XMLHttpRequest[] = [] @@ -138,6 +138,33 @@ context('cy.origin #.wait()', () => { }) }) + it('has the correct log properties', () => { + const response = { foo: 'foo' } + + cy.intercept('/foo', response).as('foo') + + cy.origin('http://www.foobar.com:3500', { args: { response } }, ({ response }) => { + cy.then(() => window.xhrGet('/foo')) + cy.wait('@foo').its('response.body').should('deep.equal', response) + }) + + cy.shouldWithTimeout(() => { + const actualLog = Cypress._.pick(findCrossOriginLogs('wait', logs, 'localhost'), + ['name', 'referencesAlias', 'aliasType', 'type', 'instrument', 'message']) + + const expectedLog = { + name: 'wait', + referencesAlias: [{ name: 'foo', cardinal: 1, ordinal: '1st' }], + aliasType: 'route', + type: 'parent', + instrument: 'command', + message: '', + } + + expect(actualLog).to.deep.equal(expectedLog) + }) + }) + it('doesn\'t log when log: false', () => { const response = { foo: 'foo' } diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index a515accd93a1..67da86c46682 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -58,14 +58,21 @@ export default (Commands, Cypress, cy, state) => { if (options.log !== false) { log = options._log = Cypress.log({ - displayName: 'wait', - name: 'wait', - message: '', type: 'parent', aliasType: 'route', // avoid circular reference options: _.omit(options, '_log'), }) + + // if this came from the spec bridge, we need to set a few additional + // properties to ensure the log displays correctly + if (options.isCrossOriginSpecBridge) { + log.set({ + displayName: 'wait', + name: 'wait', + message: '', + }) + } } const checkForXhr = async function (alias, type, index, num, options) { From 957b80e549ec301eb382a3cef29a232288013c58 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Fri, 20 May 2022 15:40:08 -0600 Subject: [PATCH 5/6] updates --- .../e2e/origin/commands/waiting.spec.ts | 10 +++---- packages/driver/src/cy/commands/waiting.ts | 26 +++++++++---------- packages/runner/webpack.config.ts | 3 +-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts b/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts index 001b628ce533..f758ff1300e1 100644 --- a/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts +++ b/packages/driver/cypress/integration/e2e/origin/commands/waiting.spec.ts @@ -166,14 +166,10 @@ context('cy.origin waiting', () => { }) it('doesn\'t log when log: false', () => { - const response = { foo: 'foo' } - - cy.intercept('/foo', response).as('foo') + cy.intercept('/foo', {}).as('foo') - cy.origin('http://www.foobar.com:3500', { args: { response } }, ({ response }) => { - cy.then(() => { - window.xhrGet('/foo') - }) + cy.origin('http://www.foobar.com:3500', () => { + cy.then(() => window.xhrGet('/foo')) cy.wait('@foo', { log: false }) }) diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index 67da86c46682..0629a9793ec9 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -57,22 +57,24 @@ export default (Commands, Cypress, cy, state) => { let log if (options.log !== false) { - log = options._log = Cypress.log({ - type: 'parent', - aliasType: 'route', - // avoid circular reference - options: _.omit(options, '_log'), - }) + let specBridgeLogOptions = {} // if this came from the spec bridge, we need to set a few additional // properties to ensure the log displays correctly if (options.isCrossOriginSpecBridge) { - log.set({ - displayName: 'wait', + specBridgeLogOptions = { name: 'wait', message: '', - }) + } } + + log = options._log = Cypress.log({ + type: 'parent', + aliasType: 'route', + // avoid circular reference + options: _.omit(options, '_log'), + ...specBridgeLogOptions, + }) } const checkForXhr = async function (alias, type, index, num, options) { @@ -284,11 +286,7 @@ export default (Commands, Cypress, cy, state) => { Cypress.primaryOriginCommunicator.on('wait:for:xhr', ({ args: [str, options] }, originPolicy) => { options.isCrossOriginSpecBridge = true waitString(null, str, options).then((responses) => { - // remove props that aren't serializable (these aren't used by consumers so are okay to remove) - const omitFn = (response) => _.omit(response, ['subscriptions', 'setLogFlag']) - const preprocessedResponses = _.isArray(responses) ? responses.map(omitFn) : omitFn(responses) - - Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { responses: preprocessedResponses }) + Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { responses: preprocessForSerialization(responses) }) }).catch((err) => { options._log?.error(err) Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { err: preprocessForSerialization(err) }) diff --git a/packages/runner/webpack.config.ts b/packages/runner/webpack.config.ts index 2b96d64f9bd4..359eae8664f1 100644 --- a/packages/runner/webpack.config.ts +++ b/packages/runner/webpack.config.ts @@ -58,7 +58,6 @@ const mainConfig: webpack.Configuration = { }, } -// @ts-ignore mainConfig.plugins = [ // @ts-ignore ...mainConfig.plugins, @@ -85,7 +84,7 @@ mainConfig.resolve = { // @ts-ignore const crossOriginConfig: webpack.Configuration = { - ...getCommonConfig(), + ...commonConfig, entry: { cypress_cross_origin_runner: [path.resolve(__dirname, 'src/cross-origin.js')], }, From 1b15780a25ec1035bef5f4a915b3b1d060d1abfc Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Mon, 23 May 2022 14:53:58 -0600 Subject: [PATCH 6/6] remove serialization in waiting command --- packages/driver/src/cy/commands/waiting.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index 0629a9793ec9..b0d19ff0fd56 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -5,7 +5,6 @@ import { isDynamicAliasingPossible } from '../net-stubbing/aliasing' import ordinal from 'ordinal' import $errUtils from '../../cypress/error_utils' -import { preprocessForSerialization } from '../../util/serialization' const getNumRequests = (state, alias) => { const requests = state('aliasRequests') || {} @@ -59,8 +58,8 @@ export default (Commands, Cypress, cy, state) => { if (options.log !== false) { let specBridgeLogOptions = {} - // if this came from the spec bridge, we need to set a few additional - // properties to ensure the log displays correctly + // if this came from the spec bridge, we need to set a few additional properties to ensure the log displays correctly + // otherwise, these props will be pulled from the current command which will be cy.origin on the primary if (options.isCrossOriginSpecBridge) { specBridgeLogOptions = { name: 'wait', @@ -286,17 +285,20 @@ export default (Commands, Cypress, cy, state) => { Cypress.primaryOriginCommunicator.on('wait:for:xhr', ({ args: [str, options] }, originPolicy) => { options.isCrossOriginSpecBridge = true waitString(null, str, options).then((responses) => { - Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { responses: preprocessForSerialization(responses) }) + Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', responses) }).catch((err) => { options._log?.error(err) - Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', { err: preprocessForSerialization(err) }) + err.hasSpecBridgeError = true + Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'wait:for:xhr:end', err) }) }) const delegateToPrimaryOrigin = ([_subject, str, options]) => { return new Promise((resolve, reject) => { - Cypress.specBridgeCommunicator.once('wait:for:xhr:end', ({ responses, err }) => { - if (err) { + Cypress.specBridgeCommunicator.once('wait:for:xhr:end', (responsesOrErr) => { + // determine if this is an error by checking if there is a spec bridge error + if (responsesOrErr.hasSpecBridgeError) { + delete responsesOrErr.hasSpecBridgeError if (options.log) { Cypress.state('onBeforeLog', (log) => { // skip this 'wait' log since it was already added through the primary @@ -311,10 +313,10 @@ export default (Commands, Cypress, cy, state) => { }) } - reject(err) + reject(responsesOrErr) } - resolve(responses) + resolve(responsesOrErr) }) // subject is not needed when waiting on aliased requests since the request/response will be yielded