Skip to content

Commit

Permalink
fix: support snapshots of xhr/fetch and other logs generated from the…
Browse files Browse the repository at this point in the history
… primary (#21552)

* add special serialization rules for snapshot prefix

* add failing regression tests

* allow for snapshots to delegate to the active spec bridge if applicable

* test against consoleProps URL which is more consistent than log url

* 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

* Update packages/driver/src/cypress/log.ts

Co-authored-by: Matt Schile <mschile@cypress.io>

* Update packages/driver/src/cypress/log.ts

Co-authored-by: Matt Schile <mschile@cypress.io>

* chore: fix trailing space

Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
Co-authored-by: Matt Schile <mschile@cypress.io>
  • Loading branch information
3 people authored Aug 17, 2022
1 parent a05b2b6 commit 53f0a02
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 26 deletions.
21 changes: 21 additions & 0 deletions packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,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
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/runner/iframe-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
99 changes: 99 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// import to bind shouldWithTimeout into global cy commands
import '../../../support/utils'

describe('cy.origin - snapshots', () => {
const findLog = (logMap: Map<string, any>, displayName: string, url: string) => {
return Array.from(logMap.values()).find((log: any) => {
const props = log.get()

return props.displayName === displayName && (props?.consoleProps?.URL === url || props?.consoleProps()?.URL === url)
})
}
let logs: Map<string, any>

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 }).as('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', () => {
// 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')
cy.wait('@fooBarBaz')
})

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', () => {
// 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')
cy.wait('@fooBarBaz')
})

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', () => {
// 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')
cy.wait('@fooBarBaz')
})
})
})
1 change: 1 addition & 0 deletions packages/driver/cypress/fixtures/primary-origin.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<li><a data-cy="files-form-link" href="http://www.foobar.com:3500/fixtures/files-form.html">http://www.foobar.com:3500/fixtures/files-form.html</a></li>
<li><a data-cy="errors-link" href="http://www.foobar.com:3500/fixtures/errors.html">http://www.foobar.com:3500/fixtures/errors.html</a></li>
<li><a data-cy="screenshots-link" href="http://www.foobar.com:3500/fixtures/screenshots.html">http://www.foobar.com:3500/fixtures/screenshots.html</a></li>
<li><a data-cy="xhr-fetch-requests" href="http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html">http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html</a></li>
<li><a data-cy="integrity-link" href="http://www.foobar.com:3500/fixtures/scripts-with-integrity.html">http://www.foobar.com:3500/fixtures/scripts-with-integrity.html</a></li>
<li><a data-cy="cookie-login">Login with Social</a></li>
<li><a data-cy="cookie-login-https">Login with Social (https)</a></li>
Expand Down
18 changes: 18 additions & 0 deletions packages/driver/cypress/fixtures/xhr-fetch-onload.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<body>
<h1 data-cy="assertion-header">Making XHR and Fetch Requests behind the scenes!</h1>
<script>
function fireXHRAndFetchRequests() {

xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3500/foo.bar.baz.json");
xhr.responseType = "json";
xhr.send();

fetch("http://localhost:3500/foo.bar.baz.json")
}
fireXHRAndFetchRequests()
</script>
</body>
</html>
11 changes: 6 additions & 5 deletions packages/driver/src/cross-origin/communicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down
15 changes: 14 additions & 1 deletion packages/driver/src/cross-origin/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,23 @@ 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)
}
})

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')
}

Expand Down
62 changes: 43 additions & 19 deletions packages/driver/src/cypress/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,20 +341,7 @@ 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
Expand All @@ -367,21 +354,58 @@ export class Log {

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') || []

Expand Down

5 comments on commit 53f0a02

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 53f0a02 Aug 17, 2022

Choose a reason for hiding this comment

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

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

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

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.0/linux-x64/develop-53f0a02858f3df8e2d7ae8f189a69a44ebedd379/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 53f0a02 Aug 17, 2022

Choose a reason for hiding this comment

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

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

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

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.0/linux-arm64/develop-53f0a02858f3df8e2d7ae8f189a69a44ebedd379/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 53f0a02 Aug 17, 2022

Choose a reason for hiding this comment

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

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

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

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.0/darwin-arm64/develop-53f0a02858f3df8e2d7ae8f189a69a44ebedd379/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 53f0a02 Aug 17, 2022

Choose a reason for hiding this comment

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

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

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

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.0/win32-x64/develop-53f0a02858f3df8e2d7ae8f189a69a44ebedd379/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 53f0a02 Aug 17, 2022

Choose a reason for hiding this comment

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

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

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

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.0/darwin-x64/develop-53f0a02858f3df8e2d7ae8f189a69a44ebedd379/cypress.tgz

Please sign in to comment.