-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: support snapshots of xhr/fetch and other logs generated from the primary #21552
Changes from all commits
313566a
cc0f144
d90da5c
c7ecf28
85e0036
32b25f9
957cdb0
dd85921
43881fd
0ad5d74
c48fac4
4d2e378
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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') | ||
}) | ||
}) | ||
}) |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) { | ||
mjhenkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: lets specify @packages/runner since the driver has a Runner class as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry this was confusing...more like, we should specific which runner. it seems like it's this runner: but our code base also has these runners: |
||
// 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)) { | ||
mjhenkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
data = preprocessSnapshotForSerialization(data as any) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to somehow tie this to the command group the log is part of? Like add the originPolicy to the command group and use it for any events generated within that group? @emilyrohrbough, is something like that possible (or better)? |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In what scenarios is this not true? If we are in a cy.origin block but have begun navigating to another origin? Does it make a difference if it's the top origin or another cross origin? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mainly the error case/operator error. If a user navigates to an origin that doesn't match the spec bridge origin, like cy.get('[href="https://www.foobar.com"]').click()
cy.origin('https://www.barbaz.com', () => {
// wrong origin, don't snapshot this
}) I added a test here to make sure this fails gracefully. |
||
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') || [] | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be one of those cases where adding a promise-based API to the communicator would be useful. Then it could be something like:
I'm not suggesting adding it in this PR, but wanted to call it out as an example. Maybe when we have another use-case, we could add that and refactor this one.