diff --git a/packages/driver/cypress/integration/e2e/multidomain_spec.ts b/packages/driver/cypress/integration/e2e/multidomain_spec.ts index 1763027f8add..23d5cd405cd6 100644 --- a/packages/driver/cypress/integration/e2e/multidomain_spec.ts +++ b/packages/driver/cypress/integration/e2e/multidomain_spec.ts @@ -45,12 +45,13 @@ describe('multidomain', { experimentalSessionSupport: true }, () => { describe('window events', () => { it('form:submitted', (done) => { - expectTextMessage('form:submitted', done) - // @ts-ignore cy.switchToDomain('foobar.com', () => { - Cypress.once('form:submitted', () => { - top!.postMessage({ host: location.host, actual: 'form:submitted' }, '*') + const $form = cy.$$('form') + + Cypress.once('form:submitted', (e) => { + expect(e.target).to.eq($form.get(0)) + done() }) cy.get('form').submit() @@ -181,7 +182,7 @@ describe('multidomain', { experimentalSessionSupport: true }, () => { }) }) - it('allows users to call the "done" callback within the "switchToDomain" context', (done) => { + it('allows users to call the "done" callback within the "switchToDomain" context synchronously', (done) => { // @ts-ignore cy.switchToDomain('foobar.com', () => { cy @@ -193,20 +194,15 @@ describe('multidomain', { experimentalSessionSupport: true }, () => { }) }) - it('runs commands in secondary domain', () => { + it('allows users to call the "done" callback within the "switchToDomain" context asynchronously', (done) => { // @ts-ignore - cy.switchToDomain('foobar.com', () => { + cy.switchToDomain('foobar.com', async () => { cy - .get('[data-cy="dom-check"]') - .invoke('text') - .should('equal', 'From a secondary domain') - }) + .get('[data-cy="cypress-check"]') - cy.log('after switchToDomain') + await setTimeout(() => undefined, 1000) + done() + }) }) - - //TODO: how should we implement errors on the done callback when it is not available? - // would this be more applicable for a system test? - it('throws "done is not defined" if callback is not passed into test but called in "switchToDomain"') }) }) diff --git a/packages/driver/src/cy/multidomain/index.ts b/packages/driver/src/cy/multidomain/index.ts index 97003590743f..903f45572aa2 100644 --- a/packages/driver/src/cy/multidomain/index.ts +++ b/packages/driver/src/cy/multidomain/index.ts @@ -27,6 +27,8 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // the other parts of multidomain switchToDomain (domain, fn) { const done = cy.state('done') + const p = cy.state('promise') + const deferredDone = createDeferred() const invokeDone = (err) => { //TODO: if an error comes back, should we call done immediately? @@ -130,9 +132,9 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // @ts-ignore Cypress.multiDomainCommunicator.on('command:update', updateCommand) - return new Bluebird((resolve) => { + return new Bluebird((resolve, reject) => { // @ts-ignore - Cypress.multiDomainCommunicator.once('run:domain:fn', resolve) + Cypress.multiDomainCommunicator.once('run:domain:fn', (err) => err ? reject(err) : resolve()) // @ts-ignore Cypress.multiDomainCommunicator.once('queue:finished', () => { @@ -141,6 +143,16 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // @ts-ignore Cypress.multiDomainCommunicator.off('command:update', updateCommand) + p.finally(() => { + // if done is to be called from the secondary domain, the 'done:called' event should + // have already been invoked in the switchToDomain function synchronously while the command queue finishes. + // Go ahead and remove the listener + + // TODO: how do we handle setTimeout with a done that occurs in the secondary domain? + // @ts-ignore + Cypress.multiDomainCommunicator.off('done:called', invokeDone) + }) + // By the time the command queue is finished, this promise should be settled as // as done will be invoked within the secondary domain already, if applicable // TODO: how does this work with errors where commands or other items prematurely fail in the secondary domain? @@ -186,13 +198,6 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // the specified domain // @ts-ignore Cypress.multiDomainCommunicator.emit('expect:domain', domain) - }).finally(() => { - // if done is to be called from the secondary domain, the 'done:called' event should - // have already been invoked in the switchToDomain function synchrously while the command queue finishes. - // Go ahead and remove the listener - // TODO: how do we handle async/setTimeout with a done? - // @ts-ignore - Cypress.multiDomainCommunicator.off('done:called', invokeDone) }) }, }) diff --git a/packages/driver/src/multidomain/index.js b/packages/driver/src/multidomain/index.js index a034c48edcd7..15ab30ee4117 100644 --- a/packages/driver/src/multidomain/index.js +++ b/packages/driver/src/multidomain/index.js @@ -10,6 +10,7 @@ import $Commands from '../cypress/commands' import $Log from '../cypress/log' import $Listeners from '../cy/listeners' import { SpecBridgeDomainCommunicator } from './communicator' +import { createDeferred } from '../util/deferred' const specBridgeCommunicator = new SpecBridgeDomainCommunicator() @@ -80,9 +81,24 @@ const setup = () => { Cypress.on('log:added', onLogAdded) Cypress.on('log:changed', onLogChanged) - specBridgeCommunicator.on('run:domain:fn', ({ fn, isDoneFnAvailable = false }) => { + specBridgeCommunicator.on('run:domain:fn', async ({ fn, isDoneFnAvailable = false }) => { + const deferredSwitchToDomain = createDeferred() + + cy.state('switchToDomainDeferred', deferredSwitchToDomain) const evalFn = `(${fn})()` + // await the eval func, whether it is a promise or not + const asyncWrapper = `(async () => { + const deferredSwitchToDomain = cy.state('switchToDomainDeferred') + + try { + await ${evalFn} + deferredSwitchToDomain.resolve() + } catch(e){ + deferredSwitchToDomain.reject(e) + } + })()` + if (isDoneFnAvailable) { // stub out the 'done' function if available in the primary domain // to notify the primary domain if the done() callback is invoked @@ -100,16 +116,23 @@ const setup = () => { const fnDoneWrapper = `(() => { const done = cy.state('done'); - ${evalFn} - })` + ${asyncWrapper} + })()` - window.eval(`(${fnDoneWrapper})()`) + window.eval(fnDoneWrapper) } else { - // TODO: await this if it's a promise, or do whatever cy.then does - window.eval(evalFn) + window.eval(asyncWrapper) } - specBridgeCommunicator.toPrimary('run:domain:fn') + try { + await deferredSwitchToDomain.promise + specBridgeCommunicator.toPrimary('run:domain:fn') + } catch (err) { + specBridgeCommunicator.toPrimary('run:domain:fn', err) + } finally { + cy.state('done', undefined) + cy.state('switchToDomainDeferred', undefined) + } }) specBridgeCommunicator.on('run:command',