diff --git a/packages/driver/cypress/integration/commands/navigation_spec.js b/packages/driver/cypress/integration/commands/navigation_spec.js index c2f1933dfc23..3c43bb3cb743 100644 --- a/packages/driver/cypress/integration/commands/navigation_spec.js +++ b/packages/driver/cypress/integration/commands/navigation_spec.js @@ -825,7 +825,7 @@ describe('src/cy/commands/navigation', () => { onLoad, }) .then((win) => { - expect(win.bar).to.not.exist + expect(win.foo).to.equal('bar') expect(onLoad).not.to.have.been.called }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts index 6550b6f2d378..92f7880a11d4 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts @@ -5,39 +5,6 @@ context('multi-domain misc', { experimentalSessionSupport: true }, () => { cy.get('a[data-cy="dom-link"]').click() }) - it('verifies number of cy commands', () => { - // @ts-ignore - const actualCommands = Object.keys(cy.commandFns) - const expectedCommands = [ - 'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select', - 'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then', - 'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile', - 'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit', - 'focused', 'get', 'contains', 'root', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not', - 'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', - 'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'switchToDomain', - ] - const addedCommands = Cypress._.difference(actualCommands, expectedCommands) - const removedCommands = Cypress._.difference(expectedCommands, actualCommands) - - if (addedCommands.length && removedCommands.length) { - throw new Error(`Commands have been added to and removed from Cypress. - - The following command(s) were added: ${addedCommands.join(', ')} - The following command(s) were removed: ${removedCommands.join(', ')} - - Update this test accordingly.`) - } - - if (addedCommands.length) { - throw new Error(`The following command(s) have been added to Cypress: ${addedCommands.join(', ')}. Please add the command(s) to this test.`) - } - - if (removedCommands.length) { - throw new Error(`The following command(s) have been removed from Cypress: ${removedCommands.join(', ')}. Please remove the command(s) from this test.`) - } - }) - it('.end()', () => { cy.switchToDomain('foobar.com', () => { cy.get('#button').end().should('be.null') @@ -104,3 +71,37 @@ context('multi-domain misc', { experimentalSessionSupport: true }, () => { }) }) }) + +it('verifies number of cy commands', () => { + // @ts-ignore + // remove 'getAll' command since it's a custom command we add for our own testing and not an actual cy command + const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => command === 'getAll') + const expectedCommands = [ + 'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select', + 'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then', + 'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile', + 'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit', + 'focused', 'get', 'contains', 'root', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not', + 'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', + 'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'switchToDomain', + ] + const addedCommands = Cypress._.difference(actualCommands, expectedCommands) + const removedCommands = Cypress._.difference(expectedCommands, actualCommands) + + if (addedCommands.length && removedCommands.length) { + throw new Error(`Commands have been added to and removed from Cypress. + + The following command(s) were added: ${addedCommands.join(', ')} + The following command(s) were removed: ${removedCommands.join(', ')} + + Update this test accordingly.`) + } + + if (addedCommands.length) { + throw new Error(`The following command(s) have been added to Cypress: ${addedCommands.join(', ')}. Please add tests for the command(s) in multi-domain and add the command(s) to this test.`) + } + + if (removedCommands.length) { + throw new Error(`The following command(s) have been removed from Cypress: ${removedCommands.join(', ')}. Please remove the command(s) from this test.`) + } +}) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts index f2ce0572cc8f..6816489bfbfb 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts @@ -1,26 +1,24 @@ -// @ts-ignore / session support is needed for visiting about:blank between tests -context('multi-domain navigation', { experimentalSessionSupport: true }, () => { - beforeEach(() => { +// @ts-ignore +context('multi-domain navigation', { experimentalMultiDomain: true }, () => { + it('.go()', () => { cy.visit('/fixtures/multi-domain.html') - cy.get('a[data-cy="dom-link"]').click() - }) + cy.get('a[data-cy="multi-domain-secondary-link"]').click() - // FIXME: CypressError: Timed out after waiting `undefinedms` for your remote page to load. - // Your page did not fire its `load` event within`undefinedms`. - // You can try increasing the `pageLoadTimeout` value in `undefined` to wait longer. - // Browsers will not fire the `load` event until all stylesheets and scripts are done downloading. - // When this `load` event occurs, Cypress will continue running commands. - // at timedOutWaitingForPageLoad(webpack:///../driver/src/cy/commands/navigation.ts?:59:72) - // at eval (webpack:///../driver/src/cy/commands/navigation.ts?:1143:16) - it.skip('.go()', () => { cy.switchToDomain('foobar.com', () => { - cy.visit('/fixtures/generic.html') + cy.visit('http://www.foobar.com:3500/fixtures/dom.html') + cy.go('back') + cy.location('pathname').should('include', 'multi-domain-secondary.html') + + cy.go('forward') cy.location('pathname').should('include', 'dom.html') }) }) it('.reload()', () => { + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="dom-link"]').click() + cy.switchToDomain('foobar.com', () => { cy.get(':checkbox[name="colors"][value="blue"]').check().should('be.checked') cy.reload() @@ -28,16 +26,293 @@ context('multi-domain navigation', { experimentalSessionSupport: true }, () => { }) }) - // FIXME: CypressError: Timed out after waiting `undefinedms` for your remote page to load. - // Your page did not fire its `load` event within `undefinedms`. - // You can try increasing the `pageLoadTimeout` value in `undefined` to wait longer. - // Browsers will not fire the `load` event until all stylesheets and scripts are done downloading. - // When this `load` event occurs, Cypress will continue running commands. - // at timedOutWaitingForPageLoad (webpack:///../driver/src/cy/commands/navigation.ts?:59:72) - // at eval (webpack:///../driver/src/cy/commands/navigation.ts?:1143:16) - it.skip('.visit()', () => { + context('.visit()', () => { + it('calls the correct load handlers', () => { + const primaryCyBeforeLoadSpy = cy.spy() + const primaryCyLoadSpy = cy.spy() + const primaryVisitBeforeLoadSpy = cy.spy() + const primaryVisitLoadSpy = cy.spy() + + cy.on('window:before:load', primaryCyBeforeLoadSpy) + cy.on('window:load', primaryCyLoadSpy) + + cy.visit('/fixtures/multi-domain.html', { + onBeforeLoad: primaryVisitBeforeLoadSpy, + onLoad: primaryVisitLoadSpy, + }).then(() => { + expect(primaryCyBeforeLoadSpy).to.be.calledOnce + expect(primaryCyLoadSpy).to.be.calledTwice // twice because it's also called for 'about:blank' + expect(primaryVisitBeforeLoadSpy).to.be.calledOnce + expect(primaryVisitLoadSpy).to.be.calledOnce + }) + + cy.switchToDomain('foobar.com', () => { + const secondaryCyBeforeLoadSpy = cy.spy() + const secondaryCyLoadSpy = cy.spy() + const secondaryVisitBeforeLoadSpy = cy.spy() + const secondaryVisitLoadSpy = cy.spy() + + cy.on('window:before:load', secondaryCyBeforeLoadSpy) + cy.on('window:load', secondaryCyLoadSpy) + + cy.visit('http://www.foobar.com:3500/fixtures/dom.html', { + onBeforeLoad: secondaryVisitBeforeLoadSpy, + onLoad: secondaryVisitLoadSpy, + }).then(() => { + expect(secondaryCyBeforeLoadSpy).to.be.calledOnce + expect(secondaryCyLoadSpy).to.be.calledOnce + expect(secondaryVisitBeforeLoadSpy).to.be.calledOnce + expect(secondaryVisitLoadSpy).to.be.calledOnce + }) + }).then(() => { + expect(primaryCyBeforeLoadSpy).to.be.calledOnce + expect(primaryCyLoadSpy).to.be.calledTwice + expect(primaryVisitBeforeLoadSpy).to.be.calledOnce + expect(primaryVisitLoadSpy).to.be.calledOnce + }) + }) + + it('supports visiting primary first', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + }) + }) + + it('supports skipping visiting primary first', () => { + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + }) + }) + + // TODO: we don't support nested domains yet... + it.skip('supports nesting a third domain', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + + cy.switchToDomain('idp.com', () => { + cy.visit('http://www.idp.com:3500/fixtures/dom.html') + }) + }) + }) + + it('supports navigating to secondary through button and then visiting', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + + cy.switchToDomain('foobar.com', () => { + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + + cy.visit('http://www.foobar.com:3500/fixtures/dom.html') + }) + }) + + // TODO: add support for relative links within secondary + it.skip('supports relative urls within secondary', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + + cy.switchToDomain('foobar.com', () => { + cy.visit('/fixtures/dom.html') + }) + }) + + it('supports hash change within secondary', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html#hashchange') + + cy.location('hash').should('equal', '#hashchange') + }) + }) + + it('navigates back to primary', () => { + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + }) + + cy.visit('/fixtures/multi-domain.html') + + cy.get('a[data-cy="multi-domain-secondary-link"]').should('have.text', 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + cy.location('href').should('equal', 'http://localhost:3500/fixtures/multi-domain.html') + }) + + it('errors when visiting a new domain within switchToDomain', (done) => { + cy.on('fail', (e) => { + expect(e.message).to.include('failed trying to load:\n\nhttp://www.idp.com:3500/fixtures/dom.html') + expect(e.message).to.include('failed because you are attempting to visit a URL that is of a different origin') + + done() + }) + + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + + cy.switchToDomain('foobar.com', () => { + // this call should error since we can't visit a cross-domain + cy.visit('http://www.idp.com:3500/fixtures/dom.html') + }) + }) + + it('supports the query string option', () => { + cy.visit('/fixtures/multi-domain.html') + + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html', { qs: { foo: 'bar' } }) + + cy.location('search').should('equal', '?foo=bar') + }) + }) + + it('can send a POST request', () => { + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/post-only', { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + bar: 'baz', + }), + }) + + cy.contains('it worked!').contains('{"bar":"baz"}') + }) + }) + + it('succeeds when the AUT window in the secondary is undefined', () => { + // manually remove the spec bridge iframe to ensure Cypress.state('window') is not already set + window.top?.document.getElementById('Spec\ Bridge:\ foobar.com')?.remove() + + cy.visit('/fixtures/multi-domain.html') + + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + }) + }) + + it('succeeds when the secondary is already defined but the AUT is still on the primary', () => { + // setup the secondary to be on the secondary domain + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + }) + + // update the AUT to be on the primary domain + cy.visit('/fixtures/multi-domain.html') + + // verify there aren't any issues when the AUT is on primary but the spec bridge is on secondary (cross-origin) + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') + + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain') + }) + }) + + it('does not navigate to about:blank in secondary if already visited in primary', () => { + Cypress.state('hasVisitedAboutBlank', true) + + cy.switchToDomain('foobar.com', () => { + const urlChangedSpy = cy.spy(Cypress, 'emit').log(false).withArgs('url:changed') + const aboutBlankSpy = cy.spy(Cypress.specBridgeCommunicator, 'toPrimary').log(false).withArgs('visit:about:blank') + + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html').then(() => { + expect(urlChangedSpy).to.have.been.calledOnce + expect(urlChangedSpy.firstCall).to.be.calledWith( + 'url:changed', + 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html', + ) + + expect(aboutBlankSpy).to.not.have.been.called + }) + }) + }) + + it('navigates to about:blank in secondary if not already visited in primary', () => { + Cypress.state('hasVisitedAboutBlank', false) + + cy.switchToDomain('foobar.com', () => { + const urlChangedSpy = cy.spy(Cypress, 'emit').log(false).withArgs('url:changed') + const aboutBlankSpy = cy.spy(Cypress.specBridgeCommunicator, 'toPrimary').log(false).withArgs('visit:about:blank') + + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html').then(() => { + expect(urlChangedSpy).to.have.been.calledOnce + expect(urlChangedSpy.firstCall).to.be.calledWith( + 'url:changed', + 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html', + ) + + expect(aboutBlankSpy).to.have.been.calledOnce + }) + }) + + cy.then(() => { + expect(Cypress.state('hasVisitedAboutBlank')).to.equal(true) + }) + }) + + // TODO: un-skip once multiple remote states are supported + it.skip('supports auth options and adding auth to subsequent requests', () => { + cy.switchToDomain('foobar.com', () => { + cy.visit('http://www.foobar.com:3500/basic_auth', { + auth: { + username: 'cypress', + password: 'password123', + }, + }) + + cy.get('body').should('have.text', 'basic auth worked') + + cy.window().then({ timeout: 60000 }, (win) => { + return new Cypress.Promise(((resolve, reject) => { + const xhr = new win.XMLHttpRequest() + + xhr.open('GET', '/basic_auth') + xhr.onload = function () { + try { + expect(this.responseText).to.include('basic auth worked') + + return resolve(win) + } catch (err) { + return reject(err) + } + } + + return xhr.send() + })) + }) + }) + }) + }) + + it('supports navigating through changing the window.location.href', () => { + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + cy.switchToDomain('foobar.com', () => { - cy.visit('/fixtures/generic.html') + cy.window().then((win) => { + win.location.href = 'http://www.foobar.com:3500/fixtures/dom.html' + }) + + cy.location('pathname').should('equal', '/fixtures/dom.html') }) }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts index dca7acb32bff..00adab54855c 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts @@ -5,44 +5,36 @@ describe('multi-domain Cypress API', { experimentalSessionSupport: true }, () => cy.get('a[data-cy="multi-domain-secondary-link"]').click() }) - // FIXME: Commands adding/overwriting should be condensed into one test with two switchToDomain tests - // once multiple switchToDomain calls are supported context('Commands', () => { - context('add', () => { - it('adds a custom command', () => { - cy.switchToDomain('foobar.com', () => { - // @ts-ignore - Cypress.Commands.add('foo', () => 'bar') - - // @ts-ignore - cy.foo().should('equal', 'bar') - }) + it('adds a custom command', () => { + cy.switchToDomain('foobar.com', () => { + // @ts-ignore + Cypress.Commands.add('foo', () => 'bar') + + // @ts-ignore + cy.foo().should('equal', 'bar') }) - it('persists defined commands through spec bridge', () => { - cy.switchToDomain('foobar.com', () => { - // @ts-ignore - cy.foo().should('equal', 'bar') - }) + // persists added command through spec bridge + cy.switchToDomain('foobar.com', () => { + // @ts-ignore + cy.foo().should('equal', 'bar') }) }) - context('overwrite', () => { - it('overwrites an existing command in the spec bridge', () => { - cy.switchToDomain('foobar.com', () => { - // @ts-ignore - Cypress.Commands.overwrite('foo', () => 'baz') + it('overwrites an existing command in the spec bridge', () => { + cy.switchToDomain('foobar.com', () => { + // @ts-ignore + Cypress.Commands.overwrite('foo', () => 'baz') - // @ts-ignore - cy.foo().should('equal', 'baz') - }) + // @ts-ignore + cy.foo().should('equal', 'baz') }) - it('persists overwritten command through spec bridge', () => { - cy.switchToDomain('foobar.com', () => { - // @ts-ignore - cy.foo().should('equal', 'baz') - }) + // persists overwritten command through spec bridge + cy.switchToDomain('foobar.com', () => { + // @ts-ignore + cy.foo().should('equal', 'baz') }) }) }) @@ -60,8 +52,6 @@ describe('multi-domain Cypress API', { experimentalSessionSupport: true }, () => }) }) - // FIXME: Commands adding/overwriting should be condensed into one test with two switchToDomain tests - // once multiple switchToDomain calls are supported it('allows a user to configure defaults', () => { cy.switchToDomain('foobar.com', () => { const multiDomainKeyboardDefaults = Cypress.Keyboard.defaults({ @@ -72,9 +62,8 @@ describe('multi-domain Cypress API', { experimentalSessionSupport: true }, () => keystrokeDelay: 60, }) }) - }) - it('persists default configuration changes through spec bridge', () => { + // persists default configuration changes through spec bridge cy.switchToDomain('foobar.com', () => { const multiDomainKeyboardDefaults = Cypress.Keyboard.defaults({}) @@ -106,8 +95,6 @@ describe('multi-domain Cypress API', { experimentalSessionSupport: true }, () => }) }) - // FIXME: Commands adding/overwriting should be condensed into one test with two switchToDomain tests - // once multiple switchToDomain calls are supported it('allows a user to configure defaults', () => { cy.switchToDomain('foobar.com', () => { const multiDomainScreenshotDefaults = Cypress.Screenshot.defaults({ @@ -120,9 +107,8 @@ describe('multi-domain Cypress API', { experimentalSessionSupport: true }, () => overwrite: true, }) }) - }) - it('persists default configuration changes through spec bridge', () => { + // persists default configuration changes through spec bridge cy.switchToDomain('foobar.com', () => { const multiDomainScreenshotDefaults = Cypress.Screenshot.defaults({}) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts index 9a98c563724c..abb72a8860f9 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts @@ -21,11 +21,10 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { .should('equal', 'Window Before Load Called') }) - // TODO enable once we can re-visit the primary domain. - // cy.visit('/fixtures/multi-domain.html') + cy.visit('/fixtures/multi-domain.html') - // cy.window().its('testPrimaryDomainBeforeLoad').should('be.true') - // cy.window().its('testSecondaryWindowBeforeLoad').should('be.undefined') + cy.window().its('testPrimaryDomainBeforeLoad').should('be.true') + cy.window().its('testSecondaryWindowBeforeLoad').should('be.undefined') }) describe('post window load events', () => { @@ -54,14 +53,13 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { cy.switchToDomain('foobar.com', () => { const afterWindowBeforeUnload = new Promise((resolve) => { Cypress.once('window:before:unload', () => { - expect(location.host).to.equal('foobar.com') + expect(location.host).to.equal('foobar.com:3500') resolve() }) }) - cy.window().then((window) => { - window.location.href = '/fixtures/multi-domain.html' - }) + // TODO: update to use relative path once available + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain.html') cy.wrap(afterWindowBeforeUnload) }) @@ -71,14 +69,13 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { cy.switchToDomain('foobar.com', () => { const afterWindowUnload = new Promise((resolve) => { Cypress.once('window:unload', () => { - expect(location.host).to.equal('foobar.com') + expect(location.host).to.equal('foobar.com:3500') resolve() }) }) - cy.window().then((window) => { - window.location.href = '/fixtures/multi-domain.html' - }) + // TODO: update to use relative path once available + cy.visit('http://www.foobar.com:3500/fixtures/multi-domain.html') cy.wrap(afterWindowUnload) }) @@ -88,7 +85,7 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { cy.switchToDomain('foobar.com', () => { const afterWindowAlert = new Promise((resolve) => { Cypress.once('window:alert', (text) => { - expect(location.host).to.equal('foobar.com') + expect(location.host).to.equal('foobar.com:3500') expect(`window:alert ${text}`).to.equal('window:alert the alert text') resolve() }) @@ -103,7 +100,7 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { cy.switchToDomain('foobar.com', () => { const afterWindowConfirm = new Promise((resolve) => { Cypress.once('window:confirm', (text) => { - expect(location.host).to.equal('foobar.com') + expect(location.host).to.equal('foobar.com:3500') expect(`window:confirm ${text}`).to.equal('window:confirm the confirm text') resolve() }) @@ -118,7 +115,7 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { cy.switchToDomain('foobar.com', () => { const afterWindowConfirmed = new Promise((resolve) => { Cypress.once('window:confirmed', (text, returnedFalse) => { - expect(location.host).to.equal('foobar.com') + expect(location.host).to.equal('foobar.com:3500') expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - true') resolve() }) @@ -138,7 +135,7 @@ describe('multi-domain', { experimentalSessionSupport: true }, () => { cy.switchToDomain('foobar.com', () => { const afterWindowConfirmed = new Promise((resolve) => { Cypress.once('window:confirmed', (text, returnedFalse) => { - expect(location.host).to.equal('foobar.com') + expect(location.host).to.equal('foobar.com:3500') expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - false') resolve() }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts index b1a7e58bd609..a642460f33d5 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts @@ -6,7 +6,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true }, }) describe('sync errors', () => { - it('fails the current test/command if sync errors are thrown from the switchToDomain callback', () => { + it('fails the current test/command if sync errors are thrown from the switchToDomain callback', (done) => { const uncaughtExceptionSpy = cy.spy() const r = cy.state('runnable') @@ -25,6 +25,8 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true }, // lastly, make sure the `uncaught:exception' handler is NOT called in the primary expect(uncaughtExceptionSpy).not.to.be.called expect(runnable).to.be.equal(r) + + done() }) cy.switchToDomain('foobar.com', () => { @@ -49,7 +51,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true }, }) }) - it('returns true from cy.on(uncaught:exception), resulting in cy:fail to be called in the primary', () => { + it('returns true from cy.on(uncaught:exception), resulting in cy:fail to be called in the primary', (done) => { cy.on('fail', (err) => { expect(err.name).to.eq('Error') expect(err.message).to.include('sync error') @@ -57,6 +59,8 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true }, expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application') // @ts-ignore expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application']) + + done() }) cy.switchToDomain('foobar.com', () => { @@ -99,7 +103,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true }, }) }) - it('fails the current test/command if async errors are thrown from the secondary domain AUT', () => { + it('fails the current test/command if async errors are thrown from the secondary domain AUT', (done) => { const uncaughtExceptionSpy = cy.spy() const r = cy.state('runnable') @@ -115,6 +119,8 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true }, expect(uncaughtExceptionSpy).not.to.be.called expect(runnable).to.be.equal(r) + + done() }) cy.switchToDomain('foobar.com', () => { diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index ad03991d7abe..691304d20ebb 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -14,7 +14,6 @@ const debug = debugFn('cypress:driver:navigation') let id = null let previousDomainVisited: boolean = false -let hasVisitedAboutBlank: boolean = false let currentlyVisitingAboutBlank: boolean = false let knownCommandCausedInstability: boolean = false @@ -34,7 +33,7 @@ const reset = (test: any = {}) => { // make sure we reset that we haven't // visited about blank again - hasVisitedAboutBlank = false + Cypress.state('hasVisitedAboutBlank', false) currentlyVisitingAboutBlank = false @@ -96,6 +95,14 @@ const specifyFileByRelativePath = (url, log) => { } const aboutBlank = (cy, win) => { + if (Cypress.state('isMultiDomain')) { + return new Promise((resolve) => { + Cypress.specBridgeCommunicator.once('visit:about:blank:end', resolve) + + Cypress.specBridgeCommunicator.toPrimary('visit:about:blank') + }) + } + return new Promise((resolve) => { cy.once('window:load', resolve) @@ -431,18 +438,24 @@ const stabilityChanged = (Cypress, state, config, stable) => { } } +// filter the options to only the REQUEST_URL_OPTS options, normalize the timeout +// value to the responseTimeout, and add the isMultiDomain value. +// // there are really two timeout values - pageLoadTimeout // and the underlying responseTimeout. for the purposes -// of resolving resolving the url, we only care about +// of resolving the url, we only care about // responseTimeout - since pageLoadTimeout is a driver // and browser concern. therefore we normalize the options // object and send 'responseTimeout' as options.timeout // for the backend. -const normalizeTimeoutOptions = (options) => { +const normalizeOptions = (options) => { return _ .chain(options) .pick(REQUEST_URL_OPTS) - .extend({ timeout: options.responseTimeout }) + .extend({ + timeout: options.responseTimeout, + isMultiDomain: !!Cypress.state('isMultiDomain'), + }) .value() } @@ -481,6 +494,14 @@ export default (Commands, Cypress, cy, state, config) => { formSubmitted(Cypress, e) }) + Cypress.multiDomainCommunicator.on('visit:about:blank', (_, domain) => { + currentlyVisitingAboutBlank = true + aboutBlank(cy, Cypress.state('window')).then(() => { + currentlyVisitingAboutBlank = false + Cypress.multiDomainCommunicator.toSpecBridge(domain, 'visit:about:blank:end') + }) + }) + const visitFailedByErr = (err, url, fn) => { err.url = url @@ -493,7 +514,7 @@ export default (Commands, Cypress, cy, state, config) => { return Cypress.backend( 'resolve:url', url, - normalizeTimeoutOptions(options), + normalizeOptions(options), ) .then((resp: any = {}) => { if (!resp.isOkStatusCode) { @@ -540,6 +561,10 @@ export default (Commands, Cypress, cy, state, config) => { } catch (e) {} // eslint-disable-line no-empty }) + Cypress.multiDomainCommunicator.on('visit:url', ({ url }) => { + $utils.iframeSrc(Cypress.$autIframe, url) + }) + Commands.addAll({ reload (...args) { let forceReload @@ -894,6 +919,12 @@ export default (Commands, Cypress, cy, state, config) => { knownCommandCausedInstability = true + // if this is multi-domain, we need to tell the primary to change + // the AUT iframe since we don't have access to it + if (Cypress.state('isMultiDomain')) { + return Cypress.specBridgeCommunicator.toPrimary('visit:url', { url }) + } + return $utils.iframeSrc($autIframe, url) }) } @@ -969,24 +1000,36 @@ export default (Commands, Cypress, cy, state, config) => { return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log) } - const current = $Location.create(win.location.href) - - // if all that is changing is the hash then we know - // the browser won't actually make a new http request - // for this, and so we need to resolve onLoad immediately - // and bypass the actual visit resolution stuff - if (bothUrlsMatchAndOneHasHash(current, remote)) { - // https://github.com/cypress-io/cypress/issues/1311 - if (current.hash === remote.hash) { - consoleProps['Note'] = 'Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire.' + // in multi-domain, the window may not have been set yet if nothing has been loaded in the secondary domain, + // it's also possible for a new test to start and for a cross-domain failure to occur if the win is set but + // the AUT hasn't yet navigated to the secondary domain + if (win) { + try { + const current = $Location.create(win.location.href) + + // if all that is changing is the hash then we know + // the browser won't actually make a new http request + // for this, and so we need to resolve onLoad immediately + // and bypass the actual visit resolution stuff + if (bothUrlsMatchAndOneHasHash(current, remote)) { + // https://github.com/cypress-io/cypress/issues/1311 + if (current.hash === remote.hash) { + consoleProps['Note'] = 'Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire.' + + return onLoad({ runOnLoadCallback: false }) + } - return onLoad({ runOnLoadCallback: false }) + return changeIframeSrc(remote.href, 'hashchange') + .then(() => { + return onLoad({}) + }) + } + } catch (e) { + // if this is a cross-domain error, skip it + if (e.name !== 'SecurityError') { + throw e + } } - - return changeIframeSrc(remote.href, 'hashchange') - .then(() => { - return onLoad({}) - }) } if (existingHash) { @@ -1035,7 +1078,6 @@ export default (Commands, Cypress, cy, state, config) => { // if the origin currently matches // then go ahead and change the iframe's src // and we're good to go - // if origin is existing.origin if (remote.originPolicy === existing.originPolicy) { previousDomainVisited = remote.origin @@ -1047,6 +1089,14 @@ export default (Commands, Cypress, cy, state, config) => { }) } + // if we are in multi-domain and the origin policies weren't the same, + // we need to throw an error since the user tried to visit a new + // domain which isn't allowed within a multi-domain block + if (Cypress.state('isMultiDomain') && win) { + // TODO: need a better error message + return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log) + } + // if we've already visited a new origin // then die else we'd be in a terrible endless loop if (previousDomainVisited) { @@ -1162,8 +1212,8 @@ export default (Commands, Cypress, cy, state, config) => { // so that we nuke the previous state. subsequent // visits will not navigate to about:blank so that // our history entries are intact - if (!hasVisitedAboutBlank) { - hasVisitedAboutBlank = true + if (!Cypress.state('hasVisitedAboutBlank')) { + Cypress.state('hasVisitedAboutBlank', true) currentlyVisitingAboutBlank = true return aboutBlank(cy, win) diff --git a/packages/driver/src/cy/multi-domain/index.ts b/packages/driver/src/cy/multi-domain/index.ts index d8321aede0c5..bdf4f3b84daf 100644 --- a/packages/driver/src/cy/multi-domain/index.ts +++ b/packages/driver/src/cy/multi-domain/index.ts @@ -99,9 +99,10 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, _resolve({ subject, unserializableSubjectType }) } - communicator.once('sync:config', ({ config, env }) => { + communicator.once('sync:globals', ({ config, env, state }) => { syncConfigToCurrentDomain(config) syncEnvToCurrentDomain(env) + Cypress.state(state) }) communicator.once('ran:domain:fn', (details) => { @@ -159,9 +160,11 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, runnable: serializeRunnable(Cypress.state('runnable')), duringUserTestExecution: Cypress.state('duringUserTestExecution'), hookId: state('hookId'), + hasVisitedAboutBlank: state('hasVisitedAboutBlank'), }, config: preprocessConfig(Cypress.config()), env: preprocessEnv(Cypress.env()), + isStable: state('isStable'), }) } catch (err: any) { const wrappedErr = $errUtils.errByPath('switchToDomain.run_domain_fn_errored', { diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index f5ff64d26385..ee446504a345 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -39,7 +39,7 @@ import ProxyLogging from './cypress/proxy-logging' import * as $Events from './cypress/events' import $Keyboard from './cy/keyboard' import * as resolvers from './cypress/resolvers' -import { PrimaryDomainCommunicator } from './multi-domain/communicator' +import { PrimaryDomainCommunicator, SpecBridgeDomainCommunicator } from './multi-domain/communicator' const debug = debugFn('cypress:driver:cypress') @@ -68,6 +68,7 @@ class $Cypress { this.$autIframe = null this.onSpecReady = null this.multiDomainCommunicator = new PrimaryDomainCommunicator() + this.specBridgeCommunicator = new SpecBridgeDomainCommunicator() this.events = $Events.extend(this) this.$ = jqueryProxyFn.bind(this) diff --git a/packages/driver/src/multi-domain/communicator.ts b/packages/driver/src/multi-domain/communicator.ts index f708e93f2b76..5ded23dc49df 100644 --- a/packages/driver/src/multi-domain/communicator.ts +++ b/packages/driver/src/multi-domain/communicator.ts @@ -166,10 +166,13 @@ export class SpecBridgeDomainCommunicator extends EventEmitter { } } - private syncConfigEnvToPrimary = () => { - this.toPrimary('sync:config', { + private syncGlobalsToPrimary = () => { + this.toPrimary('sync:globals', { config: preprocessConfig(Cypress.config()), env: preprocessEnv(Cypress.env()), + state: { + hasVisitedAboutBlank: Cypress.state('hasVisitedAboutBlank'), + }, }) } @@ -195,9 +198,9 @@ export class SpecBridgeDomainCommunicator extends EventEmitter { * @param {string} event - the name of the event to be sent. * @param {any} data - any meta data to be sent with the event. */ - toPrimary (event: string, data?: any, options = { syncConfig: false }) { + toPrimary (event: string, data?: any, options = { syncGlobals: false }) { debug('<= to Primary ', event, data, window.specBridgeDomain) - if (options.syncConfig) this.syncConfigEnvToPrimary() + if (options.syncGlobals) this.syncGlobalsToPrimary() this.handleSubjectAndErr(data, (data: any) => { this.windowReference.top.postMessage({ diff --git a/packages/driver/src/multi-domain/cypress.ts b/packages/driver/src/multi-domain/cypress.ts index 9a201798f0fe..87dabf62428e 100644 --- a/packages/driver/src/multi-domain/cypress.ts +++ b/packages/driver/src/multi-domain/cypress.ts @@ -44,6 +44,8 @@ const setup = (cypressConfig: Cypress.Config, env: Cypress.ObjectLike) => { testingType: 'e2e', }) as Cypress.Cypress + Cypress.specBridgeCommunicator.initialize(window) + // @ts-ignore const cy = window.cy = new $Cy(window, Cypress, Cypress.Cookies, Cypress.state, Cypress.config, false) diff --git a/packages/driver/src/multi-domain/domain_fn.ts b/packages/driver/src/multi-domain/domain_fn.ts index 8ac7734df09e..8d14a59bee2d 100644 --- a/packages/driver/src/multi-domain/domain_fn.ts +++ b/packages/driver/src/multi-domain/domain_fn.ts @@ -12,6 +12,7 @@ interface RunDomainFnOptions { fn: string skipConfigValidation: boolean state: {} + isStable: boolean } interface serializedRunnable { @@ -74,9 +75,7 @@ export const handleDomainFn = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm // Set the state ctx to the runnable ctx to ensure they remain in sync cy.state('ctx', cy.state('runnable').ctx) - // Stability is always false when we start as the page will always be - // loading at this point - cy.isStable(false, 'multi-domain-start') + cy.state('isMultiDomain', true) } const setRunnableStateToPassed = () => { @@ -86,12 +85,15 @@ export const handleDomainFn = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm } specBridgeCommunicator.on('run:domain:fn', async (options: RunDomainFnOptions) => { - const { config, data, env, fn, state, skipConfigValidation } = options + const { config, data, env, fn, state, skipConfigValidation, isStable } = options let queueFinished = false reset(state) + // Stability is sync'd with the primary stability + cy.isStable(isStable, 'multi:domain:fn') + // @ts-ignore window.__cySkipValidateConfig = skipConfigValidation || false @@ -110,7 +112,7 @@ export const handleDomainFn = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm } cy.stop() - specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncConfig: true }) + specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncGlobals: true }) }) try { @@ -134,10 +136,10 @@ export const handleDomainFn = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm subject, finished: !hasCommands, }, { - // Only sync the config if there are no commands in queue + // Only sync the globals if there are no commands in queue // (for instance, only assertions exist in the callback) // since it means the callback is finished at this point - syncConfig: !hasCommands, + syncGlobals: !hasCommands, }) if (!hasCommands) { @@ -149,7 +151,7 @@ export const handleDomainFn = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm } } catch (err) { setRunnableStateToPassed() - specBridgeCommunicator.toPrimary('ran:domain:fn', { err }, { syncConfig: true }) + specBridgeCommunicator.toPrimary('ran:domain:fn', { err }, { syncGlobals: true }) return } @@ -161,7 +163,7 @@ export const handleDomainFn = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm specBridgeCommunicator.toPrimary('queue:finished', { subject: cy.state('subject'), }, { - syncConfig: true, + syncGlobals: true, }) }) }) diff --git a/packages/driver/src/multi-domain/events/misc.ts b/packages/driver/src/multi-domain/events/misc.ts index 0cdd1027e8f7..81ba90bba29b 100644 --- a/packages/driver/src/multi-domain/events/misc.ts +++ b/packages/driver/src/multi-domain/events/misc.ts @@ -1,19 +1,15 @@ import type { $Cy } from '../../cypress/cy' import type { SpecBridgeDomainCommunicator } from '../communicator' -let viewportChangedCallbackFn - export const handleMiscEvents = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeCommunicator: SpecBridgeDomainCommunicator) => { - Cypress.on('viewport:changed', (viewport, fn) => { - viewportChangedCallbackFn = fn + Cypress.on('viewport:changed', (viewport, callbackFn) => { + specBridgeCommunicator.once('viewport:changed:end', () => { + callbackFn() + }) specBridgeCommunicator.toPrimary('viewport:changed', viewport) }) - specBridgeCommunicator.on('viewport:changed:end', () => { - if (viewportChangedCallbackFn) viewportChangedCallbackFn() - }) - // TODO: Should state syncing be built into cy.state instead of being explicitly called? specBridgeCommunicator.on('sync:state', (state) => { cy.state(state) diff --git a/packages/driver/src/multi-domain/events/screenshots.ts b/packages/driver/src/multi-domain/events/screenshots.ts index 1f942303a419..fcf812cbd66a 100644 --- a/packages/driver/src/multi-domain/events/screenshots.ts +++ b/packages/driver/src/multi-domain/events/screenshots.ts @@ -1,18 +1,14 @@ import type { SpecBridgeDomainCommunicator } from '../communicator' export const handleScreenshots = (Cypress: Cypress.Cypress, specBridgeCommunicator: SpecBridgeDomainCommunicator) => { - let beforeScreenshotCallback - - Cypress.on('before:screenshot', (config, cb) => { - beforeScreenshotCallback = cb + Cypress.on('before:screenshot', (config, callbackFn) => { + specBridgeCommunicator.once('before:screenshot:end', () => { + callbackFn() + }) specBridgeCommunicator.toPrimary('before:screenshot', config) }) - specBridgeCommunicator.on('before:screenshot:end', () => { - if (beforeScreenshotCallback) beforeScreenshotCallback() - }) - Cypress.on('after:screenshot', (config) => { specBridgeCommunicator.toPrimary('after:screenshot', config) }) diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index c287eccda2ed..b4a16585be13 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -51,6 +51,7 @@ declare namespace Cypress { events: Events emit: ((event: string, payload?: any) => void) multiDomainCommunicator: import('../src/multi-domain/communicator').PrimaryDomainCommunicator + specBridgeCommunicator: import('../src/multi-domain/communicator').SpecBridgeDomainCommunicator mocha: $Mocha } diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts index a5dae39a5dab..de288d016dcb 100644 --- a/packages/proxy/lib/http/request-middleware.ts +++ b/packages/proxy/lib/http/request-middleware.ts @@ -81,7 +81,7 @@ const MaybeEndRequestWithBufferedResponse: RequestMiddleware = function () { if (buffer) { this.debug('ending request with buffered response') - this.res.wantsInjection = 'full' + this.res.wantsInjection = buffer.isMultiDomain ? 'fullMultiDomain' : 'full' return this.onResponse(buffer.response, buffer.stream) } diff --git a/packages/proxy/lib/http/util/buffers.ts b/packages/proxy/lib/http/util/buffers.ts index b1ad4b63a6ee..88eb8d9a9666 100644 --- a/packages/proxy/lib/http/util/buffers.ts +++ b/packages/proxy/lib/http/util/buffers.ts @@ -12,6 +12,7 @@ export type HttpBuffer = { response: IncomingMessage stream: Readable url: string + isMultiDomain: boolean } const stripPort = (url) => { diff --git a/packages/proxy/test/unit/http/helpers.ts b/packages/proxy/test/unit/http/helpers.ts index 70b1720fdbf5..a901078a68a9 100644 --- a/packages/proxy/test/unit/http/helpers.ts +++ b/packages/proxy/test/unit/http/helpers.ts @@ -2,6 +2,7 @@ import { HttpMiddleware, _runStage } from '../../../lib/http' export function testMiddleware (middleware: HttpMiddleware[], ctx = {}) { const fullCtx = { + debug: () => {}, req: {}, res: {}, config: {}, diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts index 4e2e705b0545..8527afa113e8 100644 --- a/packages/proxy/test/unit/http/request-middleware.spec.ts +++ b/packages/proxy/test/unit/http/request-middleware.spec.ts @@ -2,7 +2,8 @@ import _ from 'lodash' import RequestMiddleware from '../../../lib/http/request-middleware' import { expect } from 'chai' import { testMiddleware } from './helpers' -import { CypressIncomingRequest } from '../../../lib' +import { CypressIncomingRequest, CypressOutgoingResponse } from '../../../lib' +import { HttpBuffer, HttpBuffers } from '../../../lib/http/util/buffers' describe('http/request-middleware', () => { it('exports the members in the correct order', () => { @@ -54,4 +55,68 @@ describe('http/request-middleware', () => { }) }) }) + + describe('MaybeEndRequestWithBufferedResponse', () => { + const { MaybeEndRequestWithBufferedResponse } = RequestMiddleware + + it('sets wantsInjection to full when a request is buffered', async () => { + const buffers = new HttpBuffers() + const buffer = { url: 'https://www.cypress.io/', isMultiDomain: false } as HttpBuffer + + buffers.set(buffer) + + const ctx = { + buffers, + req: { + proxiedUrl: 'https://www.cypress.io/', + }, + res: {} as Partial, + } + + await testMiddleware([MaybeEndRequestWithBufferedResponse], ctx) + .then(() => { + expect(ctx.res.wantsInjection).to.equal('full') + }) + }) + + it('sets wantsInjection to fullMultiDomain when a multi-domain request is buffered', async () => { + const buffers = new HttpBuffers() + const buffer = { url: 'https://www.cypress.io/', isMultiDomain: true } as HttpBuffer + + buffers.set(buffer) + + const ctx = { + buffers, + req: { + proxiedUrl: 'https://www.cypress.io/', + }, + res: {} as Partial, + } + + await testMiddleware([MaybeEndRequestWithBufferedResponse], ctx) + .then(() => { + expect(ctx.res.wantsInjection).to.equal('fullMultiDomain') + }) + }) + + it('wantsInjection is not set when the request is not buffered', async () => { + const buffers = new HttpBuffers() + const buffer = { url: 'https://www.cypress.io/', isMultiDomain: true } as HttpBuffer + + buffers.set(buffer) + + const ctx = { + buffers, + req: { + proxiedUrl: 'https://www.not-cypress.io/', + }, + res: {} as Partial, + } + + await testMiddleware([MaybeEndRequestWithBufferedResponse], ctx) + .then(() => { + expect(ctx.res.wantsInjection).to.be.undefined + }) + }) + }) }) diff --git a/packages/runner/src/iframe/iframes.jsx b/packages/runner/src/iframe/iframes.jsx index 6a7ad83033f4..6e159c417ef2 100644 --- a/packages/runner/src/iframe/iframes.jsx +++ b/packages/runner/src/iframe/iframes.jsx @@ -186,7 +186,8 @@ export default class Iframes extends Component { // container since it needs to match the size of the top window for screenshots $container: $(document.body), className: 'spec-bridge-iframe', - src: `//${domain}/${this.props.config.namespace}/multi-domain-iframes/${encodeURIComponent(domain)}`, + // TODO: Update this to the correct origin once we decide on string vs object + src: `//${domain}:3500/${this.props.config.namespace}/multi-domain-iframes/${encodeURIComponent(domain)}`, }) } diff --git a/packages/server/lib/server-e2e.ts b/packages/server/lib/server-e2e.ts index f37e8557c4f9..2c9234a26a52 100644 --- a/packages/server/lib/server-e2e.ts +++ b/packages/server/lib/server-e2e.ts @@ -304,8 +304,8 @@ export class ServerE2E extends ServerBase { // and set the domain vs not if (isOk && details.isHtml) { // reset the domain to the new url if we're not - // handling a local file - if (!handlingLocalFile) { + // handling a local file and we aren't in multi-domain + if (!handlingLocalFile && !options.isMultiDomain) { this._onDomainSet(newUrl, options) } @@ -321,6 +321,7 @@ export class ServerE2E extends ServerBase { details, originalUrl, response: incomingRes, + isMultiDomain: options.isMultiDomain, }) } else { // TODO: move this logic to the driver too for diff --git a/packages/server/test/integration/server_spec.js b/packages/server/test/integration/server_spec.js index a495d8c9b8a5..a5398efba68a 100644 --- a/packages/server/test/integration/server_spec.js +++ b/packages/server/test/integration/server_spec.js @@ -907,6 +907,56 @@ describe('Server', () => { }) }) }) + + it('can serve a multi-domain request', function () { + nock('http://www.cypress.io/') + .get('/') + .reply(200, 'content', { + 'Content-Type': 'text/html', + }) + + expect(this.server._getRemoteState()).to.deep.eq({ + auth: undefined, + props: null, + origin: 'http://localhost:2000', + strategy: 'file', + visiting: undefined, + domainName: 'localhost', + fileServer: this.fileServer, + }) + + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest, { isMultiDomain: true }) + .then((obj = {}) => { + expectToEqDetails(obj, { + isOkStatusCode: true, + isHtml: true, + contentType: 'text/html', + url: 'http://www.cypress.io/', + originalUrl: 'http://www.cypress.io/', + status: 200, + statusText: 'OK', + cookies: [], + redirects: [], + }) + + // Verify the multi-domain request was buffered + const buffer = this.buffers.take('http://www.cypress.io/') + + expect(buffer).to.not.be.empty + expect(buffer.isMultiDomain).to.be.true + + // Verify the remote state was not updated with the multi-domain request + expect(this.server._getRemoteState()).to.deep.eq({ + auth: undefined, + props: null, + origin: 'http://localhost:2000', + strategy: 'file', + visiting: false, + domainName: 'localhost', + fileServer: this.fileServer, + }) + }) + }) }) describe('both', () => {