From 313566adf9e4b6ac489ef70b3c878e27cdd7213e Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 25 Jul 2022 14:45:05 -0400 Subject: [PATCH 1/8] add special serialization rules for snapshot prefix --- packages/app/src/runner/iframe-model.ts | 2 +- packages/driver/src/cross-origin/communicator.ts | 11 ++++++----- packages/driver/src/cross-origin/cypress.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/app/src/runner/iframe-model.ts b/packages/app/src/runner/iframe-model.ts index 9ce18a998a51..a8cdbfabf41c 100644 --- a/packages/app/src/runner/iframe-model.ts +++ b/packages/app/src/runner/iframe-model.ts @@ -258,7 +258,7 @@ export class IframeModel { * 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', autStore.url || '') - Cypress.primaryOriginCommunicator.once('final:snapshot:generated', (finalSnapshot) => { + Cypress.primaryOriginCommunicator.once('snapshot:final:generated', (finalSnapshot) => { // todo(lachlan): UNIFY-1318 - find correct default, if they are even needed, for required fields ($el, coords...) // @ts-ignore this.originalState = { diff --git a/packages/driver/src/cross-origin/communicator.ts b/packages/driver/src/cross-origin/communicator.ts index 0e98df5ee5ef..477407a7f543 100644 --- a/packages/driver/src/cross-origin/communicator.ts +++ b/packages/driver/src/cross-origin/communicator.ts @@ -9,7 +9,7 @@ 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` +const SNAPSHOT_EVENT_PREFIX = `${CROSS_ORIGIN_PREFIX}snapshot:` /** * Primary Origin communicator. Responsible for sending/receiving events throughout @@ -50,8 +50,8 @@ export class PrimaryOriginCommunicator extends EventEmitter { 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) { + // reify the final or requested snapshot coming back from the secondary domain if requested by the runner. + if (data?.event.includes(SNAPSHOT_EVENT_PREFIX) && !Cypress._.isEmpty(data?.data)) { data.data = reifySnapshotFromSerialization(data.data as any) } @@ -191,8 +191,9 @@ export class SpecBridgeCommunicator extends EventEmitter { 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) { + // If requested by the runner, preprocess the snapshot before sending through postMessage() to attempt to serialize the DOM body of the snapshot. + // NOTE: SNAPSHOT_EVENT_PREFIX events, if requested by the log manager, are namespaced per primary log + if (eventName.includes(SNAPSHOT_EVENT_PREFIX) && !Cypress._.isEmpty(data)) { data = preprocessSnapshotForSerialization(data as any) } diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index c30c4485f861..0e9293727db2 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -43,7 +43,7 @@ const createCypress = () => { // 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('snapshot:final:generated', finalSnapshot) } }) From cc0f1444ea50aa28fda7f501187f72cf5d59dbfb Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 25 Jul 2022 16:36:39 -0400 Subject: [PATCH 2/8] add failing regression tests --- .../cypress/e2e/e2e/origin/snapshots.cy.ts | 93 +++++++++++++++++++ .../cypress/fixtures/primary-origin.html | 1 + .../cypress/fixtures/xhr-fetch-onload.html | 18 ++++ 3 files changed, 112 insertions(+) create mode 100644 packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts create mode 100644 packages/driver/cypress/fixtures/xhr-fetch-onload.html diff --git a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts new file mode 100644 index 000000000000..d5ab7dd9fd67 --- /dev/null +++ b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts @@ -0,0 +1,93 @@ +// import to bind shouldWithTimeout into global cy commands +import '../../../support/utils' + +describe('cy.origin - snapshots', () => { + const findLog = (logMap: Map, displayName: string, url: string) => { + return Array.from(logMap.values()).find((log: any) => { + const props = log.get() + + return props.displayName === displayName && props.url === url + }) + } + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + + cy.fixture('foo.bar.baz.json').then((fooBarBaz) => { + cy.intercept('GET', '/foo.bar.baz.json', { body: fooBarBaz }) + }) + + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="xhr-fetch-requests"]').click() + }) + + it('verifies XHR requests made while a secondary origin is active eventually update with snapshots of the secondary origin', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get(`[data-cy="assertion-header"]`).should('exist') + // have some wait time to allow for the xhr/fetch requests to settle + cy.wait(200) + }) + + cy.shouldWithTimeout(() => { + const xhrLogFromSecondaryOrigin = findLog(logs, 'xhr', 'http://localhost:3500/foo.bar.baz.json')?.get() + + expect(xhrLogFromSecondaryOrigin).to.not.be.undefined + + const snapshots = xhrLogFromSecondaryOrigin.snapshots.map((snapshot) => snapshot.body.get()[0]) + + snapshots.forEach((snapshot) => { + expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!') + }) + }) + }) + + it('verifies fetch requests made while a secondary origin is active eventually update with snapshots of the secondary origin', () => { + cy.origin('http://foobar.com:3500', () => { + cy.get(`[data-cy="assertion-header"]`).should('exist') + // have some wait time to allow for the xhr/fetch requests to settle + cy.wait(200) + }) + + cy.shouldWithTimeout(() => { + const xhrLogFromSecondaryOrigin = findLog(logs, 'fetch', 'http://localhost:3500/foo.bar.baz.json')?.get() + + expect(xhrLogFromSecondaryOrigin).to.not.be.undefined + + const snapshots = xhrLogFromSecondaryOrigin.snapshots.map((snapshot) => snapshot.body.get()[0]) + + snapshots.forEach((snapshot) => { + expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!') + }) + }) + }) + + it('Does not take snapshots of XHR/fetch requests from secondary origin if the wrong origin is / origin mismatch, but instead the primary origin (existing behavior)', { + pageLoadTimeout: 5000, + }, + (done) => { + cy.on('fail', () => { + const xhrLogFromSecondaryOrigin = findLog(logs, 'fetch', 'http://localhost:3500/foo.bar.baz.json')?.get() + + expect(xhrLogFromSecondaryOrigin).to.not.be.undefined + + const snapshots = xhrLogFromSecondaryOrigin.snapshots.map((snapshot) => snapshot.body.get()[0]) + + snapshots.forEach((snapshot) => { + expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.be.null + }) + + done() + }) + + cy.origin('http://barbaz.com:3500', () => { + cy.get(`[data-cy="assertion-header"]`).should('exist') + // have some wait time to allow for the xhr/fetch requests to settle + cy.wait(200) + }) + }) +}) diff --git a/packages/driver/cypress/fixtures/primary-origin.html b/packages/driver/cypress/fixtures/primary-origin.html index d1c49ec73d7d..916d112b18ba 100644 --- a/packages/driver/cypress/fixtures/primary-origin.html +++ b/packages/driver/cypress/fixtures/primary-origin.html @@ -13,6 +13,7 @@
  • http://www.foobar.com:3500/fixtures/files-form.html
  • http://www.foobar.com:3500/fixtures/errors.html
  • http://www.foobar.com:3500/fixtures/screenshots.html
  • +
  • http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html
  • http://www.foobar.com:3500/fixtures/scripts-with-integrity.html
  • Login with Social
  • Login with Social (https)
  • diff --git a/packages/driver/cypress/fixtures/xhr-fetch-onload.html b/packages/driver/cypress/fixtures/xhr-fetch-onload.html new file mode 100644 index 000000000000..65fa87ea0cc4 --- /dev/null +++ b/packages/driver/cypress/fixtures/xhr-fetch-onload.html @@ -0,0 +1,18 @@ + + + +

    Making XHR and Fetch Requests behind the scenes!

    + + + \ No newline at end of file From d90da5ca2aad6e23f9b03b7e1d45390b9a9d345e Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 25 Jul 2022 16:38:03 -0400 Subject: [PATCH 3/8] allow for snapshots to delegate to the active spec bridge if applicable --- packages/app/src/runner/event-manager.ts | 21 +++++++ packages/driver/src/cross-origin/cypress.ts | 13 +++++ packages/driver/src/cypress/log.ts | 64 ++++++++++++++------- 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 5b8be0ed6ae1..c440c9dbbfb2 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -603,6 +603,27 @@ export class EventManager { Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload') }) + Cypress.on('request:snapshot:from:spec:bridge', ({ log, name, options, specBridge, addSnapshot }: { + log: Cypress.Log + name?: string + options?: any + specBridge: string + addSnapshot: (snapshot: any, options: any, shouldRebindSnapshotFn: boolean) => Cypress.Log + }) => { + const eventID = log.get('id') + + Cypress.primaryOriginCommunicator.once(`snapshot:for:log:generated:${eventID}`, (generatedCrossOriginSnapshot) => { + const snapshot = generatedCrossOriginSnapshot.body ? generatedCrossOriginSnapshot : null + + addSnapshot.apply(log, [snapshot, options, false]) + }) + + Cypress.primaryOriginCommunicator.toSpecBridge(specBridge, 'generate:snapshot:for:log', { + name, + id: eventID, + }) + }) + Cypress.primaryOriginCommunicator.on('window:load', ({ url }, originPolicy) => { // Sync stable if the expected origin has loaded. // Only listen to window load events from the most recent secondary origin, This prevents nondeterminism in the case where we redirect to an already diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index 0e9293727db2..2724fd9da847 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -47,6 +47,19 @@ const createCypress = () => { } }) + Cypress.specBridgeCommunicator.on('generate:snapshot:for:log', ({ name, id }) => { + // if the snapshot cannot be taken (in a transitory space), set to an empty object in order to not fail serialization + let requestedCrossOriginSnapshot = {} + + // don't attempt to take snapshots after the spec bridge has been unloaded. Instead, send an empty snapshot back to the primary + // to display current state of dom + if (cy.state('document') !== undefined) { + requestedCrossOriginSnapshot = cy.createSnapshot(name) || {} + } + + Cypress.specBridgeCommunicator.toPrimary(`snapshot:for:log:generated:${id}`, requestedCrossOriginSnapshot) + }) + Cypress.specBridgeCommunicator.toPrimary('bridge:ready') } diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 7346dbf178fa..00622f2e4f7b 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -341,47 +341,71 @@ export class Log { return _.pick(this.attributes, args) } - snapshot (name?, options: any = {}) { - // bail early and don't snapshot if we're in headless mode - // or we're not storing tests - if (!this.config('isInteractive') || (this.config('numTestsKeptInMemory') === 0)) { - return this - } - - _.defaults(options, { - at: null, - next: null, - }) - - const snapshot = this.cy.createSnapshot(name, this.get('$el')) - + private addSnapshot (snapshot, options, shouldRebindSnapshotFn = true) { const snapshots = this.get('snapshots') || [] // don't add snapshot if we couldn't create one, which can happen // if the snapshotting process errors // https://github.com/cypress-io/cypress/issues/15816 if (snapshot) { - // insert at index 'at' or whatever is the next position + // insert at index 'at' or whatever is the next position snapshots[options.at || snapshots.length] = snapshot } this.set('snapshots', snapshots) - if (options.next) { - const fn = this.snapshot + if (options.next && shouldRebindSnapshotFn) { + const originalLogSnapshotFn = this.snapshot this.snapshot = function () { - // restore the fn - this.snapshot = fn + // restore the original snapshot function + this.snapshot = originalLogSnapshotFn // call orig fn with next as name - return fn.call(this, options.next) + return originalLogSnapshotFn.call(this, options.next) } } return this } + snapshot (name?, options: any = {}) { + // bail early and don't snapshot if we're in headless mode + // or we're not storing tests + if (!this.config('isInteractive') || (this.config('numTestsKeptInMemory') === 0)) { + return this + } + + _.defaults(options, { + at: null, + next: null, + }) + + if (this.config('experimentalSessionAndOrigin') && !Cypress.isCrossOriginSpecBridge) { + const activeSpecBridgeOriginPolicyIfApplicable = this.state('currentActiveOriginPolicy') || undefined + // @ts-ignore + const { originPolicy: originPolicyThatIsSoonToBeOrIsActive } = Cypress.Location.create(this.state('anticipatingCrossOriginResponse')?.href || this.state('url')) + + if (activeSpecBridgeOriginPolicyIfApplicable && activeSpecBridgeOriginPolicyIfApplicable === originPolicyThatIsSoonToBeOrIsActive) { + Cypress.emit('request:snapshot:from:spec:bridge', { + log: this, + name, + options, + specBridge: activeSpecBridgeOriginPolicyIfApplicable, + addSnapshot: this.addSnapshot, + }) + + return this + } + } + + const snapshot = this.cy.createSnapshot(name, this.get('$el')) + + this.addSnapshot(snapshot, options) + + return this + } + error (err) { const logGroupIds = this.state('logGroupIds') || [] From 85e00361dafeb83349b29156fd41554d04e86f9b Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 25 Jul 2022 18:02:59 -0400 Subject: [PATCH 4/8] test against consoleProps URL which is more consistent than log url --- packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts index d5ab7dd9fd67..cf99b0d81574 100644 --- a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts @@ -6,7 +6,7 @@ describe('cy.origin - snapshots', () => { return Array.from(logMap.values()).find((log: any) => { const props = log.get() - return props.displayName === displayName && props.url === url + return props.displayName === displayName && (props?.consoleProps?.URL === url || props?.consoleProps()?.URL === url) }) } let logs: Map From 32b25f9aa61d1ad19ed0ac8296244599a680ed48 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 26 Jul 2022 10:26:45 -0400 Subject: [PATCH 5/8] clean up snapshot tests to set interactive mode in the spec bridge when XHR requests are made, as well as used aliases for requests over arbitrary waits --- .../cypress/e2e/e2e/origin/snapshots.cy.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts index cf99b0d81574..08af38a0a00e 100644 --- a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts @@ -19,7 +19,7 @@ describe('cy.origin - snapshots', () => { }) cy.fixture('foo.bar.baz.json').then((fooBarBaz) => { - cy.intercept('GET', '/foo.bar.baz.json', { body: fooBarBaz }) + cy.intercept('GET', '/foo.bar.baz.json', { body: fooBarBaz }).as('fooBarBaz') }) cy.visit('/fixtures/primary-origin.html') @@ -28,9 +28,11 @@ describe('cy.origin - snapshots', () => { it('verifies XHR requests made while a secondary origin is active eventually update with snapshots of the secondary origin', () => { cy.origin('http://foobar.com:3500', () => { + // need to set isInteractive in the spec bridge in order to take xhr snapshots in run mode, similar to how isInteractive is set within support/defaults.js + // @ts-ignore + Cypress.config('isInteractive', true) cy.get(`[data-cy="assertion-header"]`).should('exist') - // have some wait time to allow for the xhr/fetch requests to settle - cy.wait(200) + cy.wait('@fooBarBaz') }) cy.shouldWithTimeout(() => { @@ -48,9 +50,11 @@ describe('cy.origin - snapshots', () => { it('verifies fetch requests made while a secondary origin is active eventually update with snapshots of the secondary origin', () => { cy.origin('http://foobar.com:3500', () => { + // need to set isInteractive in the spec bridge in order to take xhr snapshots in run mode, similar to how isInteractive is set within support/defaults.js + // @ts-ignore + Cypress.config('isInteractive', true) cy.get(`[data-cy="assertion-header"]`).should('exist') - // have some wait time to allow for the xhr/fetch requests to settle - cy.wait(200) + cy.wait('@fooBarBaz') }) cy.shouldWithTimeout(() => { @@ -85,9 +89,11 @@ describe('cy.origin - snapshots', () => { }) cy.origin('http://barbaz.com:3500', () => { + // need to set isInteractive in the spec bridge in order to take xhr snapshots in run mode, similar to how isInteractive is set within support/defaults.js + // @ts-ignore + Cypress.config('isInteractive', true) cy.get(`[data-cy="assertion-header"]`).should('exist') - // have some wait time to allow for the xhr/fetch requests to settle - cy.wait(200) + cy.wait('@fooBarBaz') }) }) }) From 0ad5d74f1cdb36f0ec37b8b70d418381dd8644d1 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Wed, 17 Aug 2022 10:32:12 -0400 Subject: [PATCH 6/8] Update packages/driver/src/cypress/log.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 00622f2e4f7b..66197eda6a2c 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -358,7 +358,7 @@ export class Log { const originalLogSnapshotFn = this.snapshot this.snapshot = function () { - // restore the original snapshot function + // restore the original snapshot function this.snapshot = originalLogSnapshotFn // call orig fn with next as name From c48fac4975057064e43e8e7eff7f2ea9b369dc69 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Wed, 17 Aug 2022 10:32:22 -0400 Subject: [PATCH 7/8] Update packages/driver/src/cypress/log.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 66197eda6a2c..d6f0cd2f2d16 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -348,7 +348,7 @@ export class Log { // if the snapshotting process errors // https://github.com/cypress-io/cypress/issues/15816 if (snapshot) { - // insert at index 'at' or whatever is the next position + // insert at index 'at' or whatever is the next position snapshots[options.at || snapshots.length] = snapshot } From 4d2e378d5d97c961f2de56a0a9cbc9f06f28ff4e Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Wed, 17 Aug 2022 11:00:18 -0400 Subject: [PATCH 8/8] chore: fix trailing space --- packages/driver/src/cypress/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index d6f0cd2f2d16..56ee9ed5fa73 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -348,7 +348,7 @@ export class Log { // if the snapshotting process errors // https://github.com/cypress-io/cypress/issues/15816 if (snapshot) { - // insert at index 'at' or whatever is the next position + // insert at index 'at' or whatever is the next position snapshots[options.at || snapshots.length] = snapshot }