diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index ab6ddc5c14c2..ae52808b94fa 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -1679,6 +1679,217 @@ describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function cy.visit('/fixtures/utf8-post.html') }) + // https://github.com/cypress-io/cypress/issues/16327 + context('request url querystring', () => { + // In the code below, cy.window() is used instead of $.get. + // It's because XHR is not sent on Firefox and it's flaky on Chrome. + it('parse query correctly', () => { + cy.intercept({ url: '/users*' }, (req) => { + expect(req.query.someKey).to.deep.equal('someValue') + expect(req.query).to.deep.equal({ someKey: 'someValue' }) + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + context('reconcile changes', () => { + it('by assigning a new query parameter obj', () => { + cy.intercept({ url: '/users*' }, (req) => { + req.query = { + a: 'b', + } + + expect(req.url).to.eq('http://localhost:3500/users?a=b') + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + it('by setting new properties', () => { + cy.intercept({ url: '/users*' }, (req) => { + expect(req.query.a).to.eq('b') + req.query.c = 'd' + + expect(req.url).to.eq('http://localhost:3500/users?a=b&c=d') + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?a=b') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + it('by doing both', () => { + cy.intercept({ url: '/users*' }, (req) => { + req.query = { + a: 'b', + } + + expect(req.query.a).to.eq('b') + req.query.c = 'd' + + expect(req.url).to.eq('http://localhost:3500/users?a=b&c=d') + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + it('by deleting query member', () => { + cy.intercept({ url: '/users*' }, (req) => { + req.query = { + a: 'b', + c: 'd', + } + + delete req.query.c + + expect(req.url).to.eq('http://localhost:3500/users?a=b') + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + context('by setting new url', () => { + it('absolute path', () => { + cy.intercept({ url: '/users*' }, (req) => { + req.url = 'http://localhost:3500/users?a=b' + + expect(req.query).to.deep.eq({ a: 'b' }) + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + it('relative path', () => { + cy.intercept({ url: '/users*' }, (req) => { + req.url = '/users?a=b' + + expect(req.query).to.deep.eq({ a: 'b' }) + expect(req.url).to.eq('http://localhost:3500/users?a=b') + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + it('empty string', () => { + cy.intercept({ url: '/users*' }, (req) => { + req.url = '' + + expect(req.query).to.deep.eq({}) + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + }) + + context('throwing errors correctly', () => { + it('defineproperty', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('`defineProperty()` is not allowed.') + + done() + }) + + cy.intercept({ url: '/users*' }, (req) => { + Object.defineProperty(req.query, 'key', { + enumerable: false, + configurable: false, + writable: false, + value: 'static', + }) + + expect(req.query).to.deep.eq({}) + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + + it('setPrototypeOf', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('`setPrototypeOf()` is not allowed.') + + done() + }) + + cy.intercept({ url: '/users*' }, (req) => { + Object.setPrototypeOf(req.query, null) + + expect(req.query).to.deep.eq({}) + }).as('getUrl') + + cy.window().then((win) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/users?someKey=someValue') + xhr.send() + }) + + cy.wait('@getUrl') + }) + }) + }) + }) + context('request events', function () { context('can end response', () => { for (const eventName of ['before:response', 'response']) { diff --git a/packages/driver/src/cy/net-stubbing/events/before-request.ts b/packages/driver/src/cy/net-stubbing/events/before-request.ts index f0911e4bf90b..a2b2f5c61918 100644 --- a/packages/driver/src/cy/net-stubbing/events/before-request.ts +++ b/packages/driver/src/cy/net-stubbing/events/before-request.ts @@ -119,8 +119,90 @@ export const onBeforeRequest: HandlerFn = (Cypre let resolved = false let handlerCompleted = false + const createQueryObject = () => { + try { + if (/^(?:[a-z]+:)?\/\//i.test(req.url) === false) { + const { protocol, hostname, port } = window.location + + req.url = `${protocol}//${hostname}${port ? `:${port}` : ''}${req.url}` + } + + const url = new URL(req.url) + const urlSearchParams = new URLSearchParams(url.search) + const result = {} + + for (let pair of urlSearchParams.entries()) { + result[pair[0]] = pair[1] + } + + return result + } catch { // avoid when url is "". + return {} + } + } + + const updateUrlParams = (paramsObj) => { + const url = new URL(req.url) + const urlSearchParams = new URLSearchParams(paramsObj) + + url.search = urlSearchParams.toString() + req.url = url.toString() + } + + const createQueryProxy = (obj) => { + return new Proxy(obj, { + set (target, key, value) { + target[key] = value + + updateUrlParams(target) + + return true + }, + + deleteProperty (target, key) { + delete target[key] + + updateUrlParams(target) + + return true + }, + + defineProperty () { + $errUtils.throwErrByPath('net_stubbing.request_handling.defineproperty_is_not_allowed') + + return false + }, + + setPrototypeOf () { + $errUtils.throwErrByPath('net_stubbing.request_handling.setprototypeof_is_not_allowed') + + return false + }, + }) + } + + let queryObj = createQueryObject() + let queryProxy = createQueryProxy(queryObj) + const userReq: CyHttpMessages.IncomingHttpRequest = { ...req, + get query () { + return queryProxy + }, + set query (userQuery) { + updateUrlParams(userQuery) + queryProxy = createQueryProxy(userQuery) + }, + get url () { + return req.url + }, + set url (userUrl) { + req.url = userUrl + + // reset query variables + queryObj = createQueryObject() + queryProxy = createQueryProxy(queryObj) + }, on (eventName, handler) { if (!validEvents.includes(eventName)) { return $errUtils.throwErrByPath('net_stubbing.request_handling.unknown_event', { diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 16814947e632..cd3bba2d3adb 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -1001,6 +1001,8 @@ module.exports = { You passed: ${format(eventName)}`, 10) }, event_needs_handler: `\`req.on()\` requires the second parameter to be a function.`, + defineproperty_is_not_allowed: `\`defineProperty()\` is not allowed.`, + setprototypeof_is_not_allowed: `\`setPrototypeOf()\` is not allowed.`, }, request_error: { network_error: ({ innerErr, req, route }) => { diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index d5df49498e75..cbd2222b29ca 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -131,6 +131,10 @@ export namespace CyHttpMessages { * Request URL. */ url: string + /** + * URL query string as object. + */ + query: Record /** * The HTTP version used in the request. Read only. */