mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-29 03:09:53 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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') || []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user