fix: support snapshots of xhr/fetch and other logs generated from the 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>
This commit is contained in:
Bill Glesias
2022-08-17 11:47:52 -04:00
committed by GitHub
parent a05b2b6883
commit 53f0a02858
8 changed files with 203 additions and 26 deletions
+21
View File
@@ -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
+1 -1
View File
@@ -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 = {
@@ -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')
})
})
})
@@ -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>
@@ -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>
@@ -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)
}
+14 -1
View File
@@ -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')
}
+43 -19
View File
@@ -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
@@ -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') || []