mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-07 23:40:21 -05:00
feat: patch fetch and xhr inside cy.origin to get resourceType and credential Level (#23822)
* chore: modify xhr-fetch-requests to handle onload and prep for use in patches tests * feat: add patches for fetch and xmlhttprequest * chore: short circuit fetch and xmlHttpRequests if conditions aren't met * chore: refactor xmlHttpRequest and fetch patches into individual files and add some basic types * chore: fix typo
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
describe('src/cross-origin/patches', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
})
|
||||
|
||||
context('submit', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
})
|
||||
|
||||
it('correctly submits a form when the target is _top for HTMLFormElement', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.get('form').then(($form) => {
|
||||
@@ -18,6 +18,11 @@ describe('src/cross-origin/patches', () => {
|
||||
})
|
||||
|
||||
context('setAttribute', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
})
|
||||
|
||||
it('renames integrity to cypress-stripped-integrity for HTMLScriptElement', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.window().then((win: Window) => {
|
||||
@@ -52,4 +57,533 @@ describe('src/cross-origin/patches', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('fetch', () => {
|
||||
describe('from the AUT', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/test-request').as('testRequest')
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
})
|
||||
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="xhr-fetch-requests"]').click()
|
||||
})
|
||||
|
||||
describe('patches fetch in the AUT when going cross origin and sends credential status to server socket', () => {
|
||||
[undefined, 'same-origin', 'omit', 'include'].forEach((credentialOption) => {
|
||||
describe(`for credential option ${credentialOption || 'default'}`, () => {
|
||||
const postfixedSelector = !credentialOption || credentialOption === 'same-origin' ? '' : `-${credentialOption}`
|
||||
const assertCredentialStatus = credentialOption || 'same-origin'
|
||||
|
||||
it('with a url string', () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
postfixedSelector,
|
||||
assertCredentialStatus,
|
||||
},
|
||||
},
|
||||
({ postfixedSelector, assertCredentialStatus }) => {
|
||||
cy.get(`[data-cy="trigger-fetch${postfixedSelector}"]`).click()
|
||||
cy.wait('@testRequest')
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: assertCredentialStatus,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('with a request object', () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
postfixedSelector,
|
||||
assertCredentialStatus,
|
||||
},
|
||||
},
|
||||
({ postfixedSelector, assertCredentialStatus }) => {
|
||||
cy.get(`[data-cy="trigger-fetch-with-request-object${postfixedSelector}"]`).click()
|
||||
cy.wait('@testRequest')
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: assertCredentialStatus,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('with a url object', () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
postfixedSelector,
|
||||
assertCredentialStatus,
|
||||
},
|
||||
},
|
||||
({ postfixedSelector, assertCredentialStatus }) => {
|
||||
cy.get(`[data-cy="trigger-fetch-with-url-object${postfixedSelector}"]`).click()
|
||||
cy.wait('@testRequest')
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: assertCredentialStatus,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fails gracefully if fetch is called with Bad arguments and we don\'t single to the socket (must match the fetch api spec), but fetch request still proceeds', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.contain('404')
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="trigger-fetch-with-bad-options"]`).click()
|
||||
})
|
||||
})
|
||||
|
||||
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.contain('CORS ERROR')
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://app.foobar.com:3500/test-request',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: 'include',
|
||||
})
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="trigger-fetch-with-preflight"]`).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('from the spec bridge', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/test-request').as('testRequest')
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
})
|
||||
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="xhr-fetch-requests"]').click()
|
||||
})
|
||||
|
||||
describe('patches fetch in the AUT when going cross origin and sends credential status to server socket', () => {
|
||||
[undefined, 'same-origin', 'omit', 'include'].forEach((credentialOption) => {
|
||||
const assertCredentialStatus = credentialOption || 'same-origin'
|
||||
|
||||
// NOTE: Even if the request fails, this should be popped off the queue in the proxy and not be an issue moving forward.
|
||||
// only thing we should be concerned with is urls that go over the socket but somehow do NOT make a request.
|
||||
// This MIGHT be an issue for preflight requests failing if the browser fails the request and the request doesn't actually make it through the proxy
|
||||
describe(`for credential option ${credentialOption || 'default'}`, () => {
|
||||
it('with a url string', () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
credentialOption,
|
||||
assertCredentialStatus,
|
||||
},
|
||||
}, ({ credentialOption, assertCredentialStatus }) => {
|
||||
cy.then(() => {
|
||||
if (credentialOption) {
|
||||
return fetch('http://www.foobar.com:3500/test-request-credentials', {
|
||||
credentials: credentialOption as RequestCredentials,
|
||||
})
|
||||
}
|
||||
|
||||
return fetch('http://www.foobar.com:3500/test-request-credentials')
|
||||
})
|
||||
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request-credentials',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: assertCredentialStatus,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('with a request object', () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
credentialOption,
|
||||
assertCredentialStatus,
|
||||
},
|
||||
}, ({ credentialOption, assertCredentialStatus }) => {
|
||||
cy.then(() => {
|
||||
let req
|
||||
|
||||
if (credentialOption) {
|
||||
req = new Request('http://www.foobar.com:3500/test-request-credentials', {
|
||||
credentials: credentialOption as RequestCredentials,
|
||||
})
|
||||
} else {
|
||||
req = new Request('http://www.foobar.com:3500/test-request-credentials')
|
||||
}
|
||||
|
||||
return fetch(req)
|
||||
})
|
||||
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request-credentials',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: assertCredentialStatus,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('with a url object', () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
credentialOption,
|
||||
assertCredentialStatus,
|
||||
},
|
||||
}, ({ credentialOption, assertCredentialStatus }) => {
|
||||
cy.then(() => {
|
||||
let urlObj = new URL('/test-request-credentials', 'http://www.foobar.com:3500')
|
||||
|
||||
if (credentialOption) {
|
||||
return fetch(urlObj, {
|
||||
credentials: credentialOption as RequestCredentials,
|
||||
})
|
||||
}
|
||||
|
||||
return fetch(urlObj)
|
||||
})
|
||||
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request-credentials',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: assertCredentialStatus,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fails gracefully if fetch is called with Bad arguments and we don\'t single to the socket (must match the fetch api spec), but fetch request still proceeds', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.contain('404')
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="trigger-fetch-with-bad-options"]`).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.then(() => {
|
||||
let url = new URL('/test-request', 'http://app.foobar.com:3500').toString()
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'foo': 'bar',
|
||||
},
|
||||
}).catch(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://app.foobar.com:3500/test-request',
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: 'include',
|
||||
})
|
||||
|
||||
resolve()
|
||||
}).then(() => {
|
||||
// if this fetch does not fail, fail the test
|
||||
reject()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does not patch fetch in the spec window or the AUT if the AUT is on the primary', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
cy.visit('fixtures/xhr-fetch-requests.html')
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.location.href = 'http://www.foobar.com:3500/fixtures/secondary-origin.html'
|
||||
})
|
||||
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
win.location.href = 'http://localhost:3500/fixtures/xhr-fetch-requests.html'
|
||||
})
|
||||
})
|
||||
|
||||
// expect spec to NOT be patched in primary
|
||||
cy.then(async () => {
|
||||
await fetch('/test-request')
|
||||
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
})
|
||||
|
||||
// expect AUT to NOT be patched in primary
|
||||
cy.window().then(async (win) => {
|
||||
await win.fetch('/test-request')
|
||||
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('xmlHttpRequest', () => {
|
||||
describe('from the AUT', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/test-request').as('testRequest')
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
})
|
||||
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="xhr-fetch-requests"]').click()
|
||||
})
|
||||
|
||||
describe('patches xmlHttpRequest in the AUT when going cross origin and sends credential status to server socket', () => {
|
||||
[false, true].forEach((withCredentials) => {
|
||||
it(`for withCredentials option ${withCredentials}`, () => {
|
||||
const postfixedSelector = withCredentials ? '-with-credentials' : ''
|
||||
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
postfixedSelector,
|
||||
withCredentials,
|
||||
},
|
||||
},
|
||||
({ postfixedSelector, withCredentials = false }) => {
|
||||
cy.get(`[data-cy="trigger-xml-http-request${postfixedSelector}"]`).click()
|
||||
cy.wait('@testRequest')
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request',
|
||||
resourceType: 'xhr',
|
||||
credentialStatus: withCredentials,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fails gracefully if xhr is called with Bad arguments and we don\'t signal to the socket (must match the legit URL), but xhr request still proceeds', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.contain('404')
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="trigger-xml-http-request-with-bad-options"]`).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.contain('CORS ERROR')
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://app.foobar.com:3500/test-request',
|
||||
resourceType: 'xhr',
|
||||
credentialStatus: true,
|
||||
})
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="trigger-xml-http-request-with-preflight"]`).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('from the spec bridge', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/test-request').as('testRequest')
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
})
|
||||
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="xhr-fetch-requests"]').click()
|
||||
})
|
||||
|
||||
describe('patches xmlHttpRequest in the spec bridge', () => {
|
||||
[false, true].forEach((withCredentials) => {
|
||||
it(`for withCredentials option ${withCredentials}`, () => {
|
||||
cy.origin('http://www.foobar.com:3500', {
|
||||
args: {
|
||||
withCredentials,
|
||||
},
|
||||
},
|
||||
({ withCredentials = false }) => {
|
||||
cy.then(() => {
|
||||
let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString()
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.open('GET', url)
|
||||
xhr.withCredentials = withCredentials
|
||||
xhr.onload = function () {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
|
||||
xhr.onerror = function () {
|
||||
reject(xhr.response)
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
})
|
||||
})
|
||||
|
||||
cy.then(() => {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://www.foobar.com:3500/test-request-credentials',
|
||||
resourceType: 'xhr',
|
||||
credentialStatus: withCredentials,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fails gracefully if xmlHttpRequest is called with bad arguments and we don\'t signal to the socket (must match the legit URL), but xhr request still proceeds', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.contain('404')
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
cy.get(`[data-cy="trigger-xml-http-request-with-bad-options"]`).click()
|
||||
})
|
||||
})
|
||||
|
||||
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
|
||||
cy.origin('http://www.foobar.com:3500',
|
||||
() => {
|
||||
cy.then(() => {
|
||||
let url = new URL('/test-request', 'http://app.foobar.com:3500').toString()
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.open('GET', url)
|
||||
xhr.withCredentials = true
|
||||
xhr.onload = function () {
|
||||
// if this request passes, fail the test
|
||||
reject(xhr.response)
|
||||
}
|
||||
|
||||
xhr.onerror = function () {
|
||||
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
|
||||
url: 'http://app.foobar.com:3500/test-request',
|
||||
resourceType: 'xhr',
|
||||
credentialStatus: true,
|
||||
})
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does not patch xmlHttpRequest in the spec window or the AUT if the AUT is on the primary', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
cy.visit('fixtures/xhr-fetch-requests.html')
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.location.href = 'http://www.foobar.com:3500/fixtures/secondary-origin.html'
|
||||
})
|
||||
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
win.location.href = 'http://localhost:3500/fixtures/xhr-fetch-requests.html'
|
||||
})
|
||||
})
|
||||
|
||||
// expect spec to NOT be patched in primary
|
||||
cy.then(async () => {
|
||||
let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString()
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.open('GET', url)
|
||||
xhr.onload = function () {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
|
||||
xhr.onerror = function () {
|
||||
reject(xhr.response)
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
})
|
||||
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
})
|
||||
|
||||
// expect AUT to NOT be patched in primary
|
||||
cy.window().then(async (win) => {
|
||||
let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString()
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let xhr = new win.XMLHttpRequest()
|
||||
|
||||
xhr.open('GET', url)
|
||||
xhr.onload = function () {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
|
||||
xhr.onerror = function () {
|
||||
reject(xhr.response)
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
})
|
||||
|
||||
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('cy.origin - snapshots', () => {
|
||||
})
|
||||
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="xhr-fetch-requests-onload"]').click()
|
||||
})
|
||||
|
||||
// TODO: the xhr event is showing up twice in the log, which is wrong and causing flake. skipping until: https://github.com/cypress-io/cypress/issues/23840 is addressed.
|
||||
@@ -47,7 +48,7 @@ describe('cy.origin - snapshots', () => {
|
||||
|
||||
// TODO: Since we have two events, one of them does not have a request snapshot
|
||||
|
||||
expect(snapshots[1].querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!')
|
||||
expect(snapshots[1].querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes if fireOnload is true!')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,7 +71,7 @@ describe('cy.origin - snapshots', () => {
|
||||
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!')
|
||||
expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes if fireOnload is true!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<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="xhr-fetch-requests-onload" href="http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html?fireOnload=true">http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html onLoad</a></li>
|
||||
<li><a data-cy="xhr-fetch-requests" href="http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html">http://www.foobar.com:3500/fixtures/xhr-fetch-requests.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>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1 data-cy="assertion-header">Making XHR and Fetch Requests behind the scenes if fireOnload is true!</h1>
|
||||
<button data-cy="trigger-fetch" onclick="triggerFetch('/test-request')"> trigger fetch </button>
|
||||
<button data-cy="trigger-fetch-with-request-object" onclick="triggerFetchWithRequestObject('/test-request')"> trigger fetch with Request Object</button>
|
||||
<button data-cy="trigger-fetch-with-url-object" onclick="triggerFetchWithUrlObject('/test-request')" > trigger fetch with URL Object</button>
|
||||
<button data-cy="trigger-fetch-omit" onclick="triggerFetch('/test-request', 'omit')"> trigger fetch w/ omit credentials </button>
|
||||
<button data-cy="trigger-fetch-with-request-object-omit" onclick="triggerFetchWithRequestObject('/test-request', 'omit')"> trigger fetch with Request Object w/ omit credentials</button>
|
||||
<button data-cy="trigger-fetch-with-url-object-omit" onclick="triggerFetchWithUrlObject('/test-request', 'omit')" > trigger fetch with URL Object w/ omit credentials</button>
|
||||
<button data-cy="trigger-fetch-include" onclick="triggerFetch('/test-request', 'include')"> trigger fetch w/ include credentials </button>
|
||||
<button data-cy="trigger-fetch-with-request-object-include" onclick="triggerFetchWithRequestObject('/test-request', 'include')"> trigger fetch with Request Object w/ include credentials</button>
|
||||
<button data-cy="trigger-fetch-with-url-object-include" onclick="triggerFetchWithUrlObject('/test-request', 'include')" > trigger fetch with URL Object w/ include credentials</button>
|
||||
<button data-cy="trigger-fetch-with-bad-options" onclick="triggerFetch(null)">trigger fetch with bad option</button>
|
||||
<button data-cy="trigger-fetch-with-preflight" onclick="triggerFailingFetchPreflight('/test-request')">trigger fetch w/ preflight</button>
|
||||
<button data-cy="trigger-xml-http-request" onclick="triggerXmlHttpRequest('/test-request')">trigger xmlHttpRequest</button>
|
||||
<button data-cy="trigger-xml-http-request-with-credentials" onclick="triggerXmlHttpRequest('/test-request', true)">trigger xmlHttpRequest w/ credentials</button>
|
||||
<button data-cy="trigger-xml-http-request-with-bad-options" onclick="triggerXmlHttpRequest(null)">trigger xmlHttpRequest w/ bad options</button>
|
||||
<button data-cy="trigger-xml-http-request-with-preflight" onclick="triggerFailingXmlHttpRequestPreflight('/test-request')">trigger xmlHttpRequest w/ preflight</button>
|
||||
<script>
|
||||
function triggerFetch(requestOrUrlObjOrString, credentials){
|
||||
let fetchReq
|
||||
if(credentials){
|
||||
fetchReq = fetch(requestOrUrlObjOrString, {
|
||||
credentials
|
||||
})
|
||||
} else {
|
||||
fetchReq = fetch(requestOrUrlObjOrString)
|
||||
}
|
||||
return fetchReq.then(function(response) {
|
||||
// throw errors in our application to test when fetch fails
|
||||
if (!response.ok) {
|
||||
throw Error(response.status);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
function triggerFetchWithRequestObject(urlString, credentials){
|
||||
let req = new Request(urlString)
|
||||
if(credentials){
|
||||
// credentials must either match options passed into fetch or must exist on the Request object itself
|
||||
req = new Request(urlString, {
|
||||
credentials
|
||||
})
|
||||
}
|
||||
return triggerFetch(req)
|
||||
}
|
||||
|
||||
function triggerFetchWithUrlObject(urlString, credentials){
|
||||
return triggerFetch(new URL(urlString, window.location.origin), credentials)
|
||||
}
|
||||
|
||||
function triggerFailingFetchPreflight(relativeUrl){
|
||||
let url = new URL(relativeUrl, 'http://app.foobar.com:3500').toString()
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'foo': 'bar'
|
||||
},
|
||||
credentials: 'include'
|
||||
}).catch(() => {
|
||||
throw new Error('CORS ERROR');
|
||||
}).then(() => {
|
||||
throw new Error('request succeeded when it shouldn\'t have')
|
||||
})
|
||||
}
|
||||
|
||||
function triggerXmlHttpRequest(relativeUrl, withCredentials = false){
|
||||
const url = new URL(relativeUrl, window.location.origin)
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url);
|
||||
xhr.withCredentials = withCredentials
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function triggerFailingXmlHttpRequestPreflight(relativeUrl){
|
||||
// might need a cross origin req here
|
||||
let url = new URL(relativeUrl, 'http://app.foobar.com:3500').toString()
|
||||
|
||||
let xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.open('GET', url)
|
||||
// adding headers to trigger a preflight request. @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
|
||||
xhr.setRequestHeader('foo', 'bar')
|
||||
// since the plugin server sets the Access-Control-Allow-Origin to * (wildcard), this request will fail as a CORS errors. The
|
||||
xhr.withCredentials = true
|
||||
|
||||
xhr.onerror = function () {
|
||||
throw new Error('CORS ERROR')
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
function fireXHRAndFetchRequests() {
|
||||
if(window.location.search.includes('fireOnload=true')){
|
||||
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>
|
||||
@@ -21,6 +21,8 @@ import { handleTestEvents } from './events/test'
|
||||
import { handleMiscEvents } from './events/misc'
|
||||
import { handleUnsupportedAPIs } from './unsupported_apis'
|
||||
import { patchFormElementSubmit } from './patches/submit'
|
||||
import { patchFetch } from './patches/fetch'
|
||||
import { patchXmlHttpRequest } from './patches/xmlHttpRequest'
|
||||
import $Mocha from '../cypress/mocha'
|
||||
import * as cors from '@packages/network/lib/cors'
|
||||
|
||||
@@ -173,6 +175,15 @@ const attachToWindow = (autWindow: Window) => {
|
||||
|
||||
cy.overrides.wrapNativeMethods(autWindow)
|
||||
|
||||
// place after override incase fetch is polyfilled in the AUT injection
|
||||
// this can be in the beforeLoad code as we only want to patch fetch/xmlHttpRequest
|
||||
// when the cy.origin block is active to track credential use
|
||||
patchFetch(Cypress, autWindow)
|
||||
patchXmlHttpRequest(Cypress, autWindow)
|
||||
// also patch it in the spec bridge as well
|
||||
patchFetch(Cypress, window)
|
||||
patchXmlHttpRequest(Cypress, window)
|
||||
|
||||
// TODO: DRY this up with the mostly-the-same code in src/cypress/cy.js
|
||||
// https://github.com/cypress-io/cypress/issues/20972
|
||||
bindToListeners(autWindow, {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { captureFullRequestUrl } from './utils'
|
||||
|
||||
export const patchFetch = (Cypress: Cypress.Cypress, window) => {
|
||||
// if fetch is available in the browser, or is polyfilled by whatwg fetch
|
||||
// intercept method calls and add cypress headers to determine cookie applications in the proxy
|
||||
// for simulated top. @see https://github.github.io/fetch/ for default options
|
||||
if (!Cypress.config('experimentalSessionAndOrigin') || !window.fetch) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch
|
||||
|
||||
window.fetch = function (...args) {
|
||||
try {
|
||||
let url: string | undefined = undefined
|
||||
let credentials: string | undefined = undefined
|
||||
|
||||
const resource = args[0]
|
||||
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters for fetch resource options. We will only support Request, URL, and strings
|
||||
if (resource instanceof window.Request) {
|
||||
({ url, credentials } = resource)
|
||||
} else if (resource instanceof window.URL) {
|
||||
// should be a no-op for URL
|
||||
url = resource.toString()
|
||||
|
||||
;({ credentials } = args[1] || {})
|
||||
} else if (Cypress._.isString(resource)) {
|
||||
url = captureFullRequestUrl(resource, window)
|
||||
|
||||
;({ credentials } = args[1] || {})
|
||||
}
|
||||
|
||||
credentials = credentials || 'same-origin'
|
||||
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
|
||||
// if the option isn't set, we can imply the default as we know the resource type in the proxy
|
||||
if (url) {
|
||||
// @ts-expect-error
|
||||
Cypress.backend('request:sent:with:credentials', {
|
||||
// TODO: might need to go off more information here or at least make collisions less likely
|
||||
url,
|
||||
resourceType: 'fetch',
|
||||
credentialStatus: credentials,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
// if our internal logic errors for whatever reason, do NOT block the end user and continue the request
|
||||
return originalFetch.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const captureFullRequestUrl = (relativeOrAbsoluteUrlString: string, window: Window) => {
|
||||
// need to pass the window here by reference to generate the correct absolute URL if needed. Spec Bridge does NOT contain sub domain
|
||||
let url
|
||||
|
||||
try {
|
||||
url = new URL(relativeOrAbsoluteUrlString).toString()
|
||||
} catch (err1) {
|
||||
try {
|
||||
// likely a relative path, construct the full url
|
||||
url = new URL(relativeOrAbsoluteUrlString, window.location.origin).toString()
|
||||
} catch (err2) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { captureFullRequestUrl } from './utils'
|
||||
|
||||
export const patchXmlHttpRequest = (Cypress: Cypress.Cypress, window: Window) => {
|
||||
// intercept method calls and add cypress headers to determine cookie applications in the proxy
|
||||
// for simulated top
|
||||
|
||||
if (!Cypress.config('experimentalSessionAndOrigin')) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalXmlHttpRequestOpen = window.XMLHttpRequest.prototype.open
|
||||
const originalXmlHttpRequestSend = window.XMLHttpRequest.prototype.send
|
||||
|
||||
window.XMLHttpRequest.prototype.open = function (...args) {
|
||||
try {
|
||||
// since the send method does NOT have access to the arguments passed into open or have the request information,
|
||||
// we need to store a reference here to what we need in the send method
|
||||
this._url = captureFullRequestUrl(args[1], window)
|
||||
} finally {
|
||||
return originalXmlHttpRequestOpen.apply(this, args as any)
|
||||
}
|
||||
}
|
||||
|
||||
window.XMLHttpRequest.prototype.send = function (...args) {
|
||||
try {
|
||||
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
|
||||
// if the option isn't set, we can imply the default as we know the resource type in the proxy
|
||||
if (this._url) {
|
||||
// @ts-expect-error
|
||||
Cypress.backend('request:sent:with:credentials', {
|
||||
// TODO: might need to go off more information here or at least make collisions less likely
|
||||
url: this._url,
|
||||
resourceType: 'xhr',
|
||||
credentialStatus: this.withCredentials,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
// if our internal logic errors for whatever reason, do NOT block the end user and continue the request
|
||||
return originalXmlHttpRequestSend.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +484,9 @@ export class SocketBase {
|
||||
}
|
||||
case 'cross:origin:automation:cookies:received':
|
||||
return this.localBus.emit('cross:origin:automation:cookies:received')
|
||||
case 'request:sent:with:credentials':
|
||||
// NOTE: this is currently a no-op until the server logic is implemented
|
||||
return this.localBus.emit('request:sent:with:credentials', args[0])
|
||||
default:
|
||||
throw new Error(`You requested a backend event we cannot handle: ${eventName}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user