Skip to content

Commit

Permalink
feat: [Multi-domain]: Yield subject from switchToDomain (#19936)
Browse files Browse the repository at this point in the history
* first pass at yielding a subject

* Remove logs and add comments

* remove duplicate test

* Add debug logging for aborted

* Apply suggestions from code review

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* New strategy for serializing subjects

* handle 'ran:domain:fn' event

* Fix lint

* Now with proxy error handling!

* Break yields tests out into their own file.

* updated test

* Update packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts

Co-authored-by: Matt Schile <mschile@gmail.com>

* add a param for the malformed should

* Apply suggestions from code review

Co-authored-by: Matt Schile <mschile@gmail.com>

* update test to work cross browsers

* fix test strings

* Update packages/driver/src/util/queue.ts

Co-authored-by: Matt Schile <mschile@gmail.com>

* optional chaining !

* Apply suggestions from code review

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* code review changes

* Whoops

* whoops, renamed the wrong test file

* Update packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts

Co-authored-by: Matt Schile <mschile@gmail.com>

Co-authored-by: Bill Glesias <bglesias@gmail.com>
Co-authored-by: Matt Schile <mschile@gmail.com>
  • Loading branch information
3 people authored Feb 3, 2022
1 parent 4070270 commit c58a0ee
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
const { assertLogLength } = require('../../../support/utils')

// @ts-ignore / session support is needed for visiting about:blank between tests
describe('multi-domain yields', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => {
let logs: any = []

beforeEach(() => {
logs = []

cy.on('log:added', (attrs, log) => {
logs.push(log)
})

cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="multi-domain-secondary-link"]').click()
})

it('yields a value', () => {
cy.switchToDomain('foobar.com', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')
}).should('equal', 'From a secondary domain')
})

it('yields the cy value even if a return is present', () => {
cy.switchToDomain('foobar.com', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')

const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('text')
}, 50)
})

return p
}).should('equal', 'From a secondary domain')
})

it('errors if a cy command is present and it returns a sync value', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 6)
expect(logs[5].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` failed because you are mixing up async and sync code.')

done()
})

cy.switchToDomain('foobar.com', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')

return 'text'
})
})

it('yields synchronously', () => {
cy.switchToDomain('foobar.com', () => {
return 'From a secondary domain'
}).should('equal', 'From a secondary domain')
})

it('yields asynchronously', () => {
cy.switchToDomain('foobar.com', () => {
return new Promise((resolve: (val: string) => any, reject) => {
setTimeout(() => {
resolve('From a secondary domain')
}, 50)
})
}).should('equal', 'From a secondary domain')
})

it('succeeds if subject cannot be serialized and is not accessed synchronously', () => {
cy.switchToDomain('foobar.com', () => {
return {
symbol: Symbol(''),
}
}).then((obj) => {
return 'object not accessed'
}).should('equal', 'object not accessed')
})

it('throws if subject cannot be serialized and is accessed synchronously', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 6)
expect(logs[5].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')

done()
})

cy.switchToDomain('foobar.com', () => {
return {
symbol: Symbol(''),
}
}).then((obj) => {
// This will fail accessing the symbol
// @ts-ignore
return obj.symbol
})
})

it('succeeds if subject cannot be serialized and is not accessed', () => {
cy.switchToDomain('foobar.com', () => {
cy
.get('[data-cy="dom-check"]')
}).then((obj) => {
return 'object not accessed'
}).should('equal', 'object not accessed')
})

it('throws if subject cannot be serialized and is accessed', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')

done()
})

cy.switchToDomain('foobar.com', () => {
cy
.get('[data-cy="dom-check"]')
}).invoke('text')
.should('equal', 'From a secondary domain')
})

it('throws if an object contains a function', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')

done()
})

cy.switchToDomain('foobar.com', () => {
cy.wrap({
key: () => {
return 'whoops'
},
})
}).invoke('key').should('equal', 'whoops')
})

it('throws if an object contains a symbol', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')

done()
})

cy.switchToDomain('foobar.com', () => {
cy.wrap({
key: Symbol('whoops'),
})
}).should('equal', undefined)
})

it('throws if an object is a function', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to functions not being supported by the structured clone algorithm.')

done()
})

cy.switchToDomain('foobar.com', () => {
cy.wrap(() => {
return 'text'
})
}).then((obj) => {
// @ts-ignore
obj()
})
})

it('throws if an object is a symbol', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to symbols not being supported by the structured clone algorithm.')

done()
})

cy.switchToDomain('foobar.com', () => {
cy.wrap(Symbol('symbol'))
}).should('equal', 'symbol')
})

// NOTE: Errors can only be serialized on chromium browsers.
it('yields an error if an object contains an error', (done) => {
const isChromium = Cypress.isBrowser({ family: 'chromium' })

cy.on('fail', (err) => {
if (!isChromium) {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')
}

done()
})

cy.switchToDomain('foobar.com', () => {
cy.wrap({
key: new Error('Boom goes the dynamite'),
})
}).its('key.message')
.should('equal', 'Boom goes the dynamite').then(() => {
done()
})
})

it('yields an object containing valid types', () => {
cy.switchToDomain('foobar.com', () => {
cy.wrap({
array: [
1,
2,
],
undefined,
bool: true,
null: null,
number: 12,
object: {
key: 'key',
},
string: 'string',
})
}).should('deep.equal', {
array: [
1,
2,
],
undefined,
bool: true,
null: null,
number: 12,
object: {
key: 'key',
},
string: 'string',
})
})
})
12 changes: 12 additions & 0 deletions packages/driver/cypress/integration/util/queue_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,16 @@ describe('src/util/queue', () => {
expect(queue.stopped).to.false
})
})

context('.last', () => {
it('returns the last item', () => {
expect(queue.last()).to.deep.equal({ id: '3' })

queue.add({ id: '4' })
expect(queue.last()).to.deep.equal({ id: '4' })

queue.clear()
expect(queue.last()).to.equal(undefined)
})
})
})
5 changes: 3 additions & 2 deletions packages/driver/src/cy/multi-domain/commands_manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PrimaryDomainCommunicator } from '../../multi-domain/communicator'
import { createDeferred, Deferred } from '../../util/deferred'
import { correctStackForCrossDomainError } from './util'
import { failedToSerializeSubject } from './failedSerializeSubjectProxy'

export class CommandsManager {
// these are proxy commands that represent real commands in a
Expand Down Expand Up @@ -58,15 +59,15 @@ export class CommandsManager {
Cypress.action('cy:enqueue:command', attrs)
}

endCommand = ({ id, name, err, logId }) => {
endCommand = ({ id, subject, failedToSerializeSubjectOfType, name, err, logId }) => {
const command = this.commands[id]

if (!command) return

delete this.commands[id]

if (!err) {
return command.deferred.resolve()
return command.deferred.resolve(failedToSerializeSubjectOfType ? failedToSerializeSubject(failedToSerializeSubjectOfType) : subject)
}

// If the command has failed, cast the error back to a proper Error object
Expand Down
66 changes: 66 additions & 0 deletions packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import $errUtils from '../../cypress/error_utils'

// These properties are required to avoid failing prior to attempting to use the subject.
// If Symbol.toStringTag is passed through to the target we will not properly fail the 'cy.invoke' command.
const passThroughProps = [
'then',
Symbol.isConcatSpreadable,
'jquery',
'nodeType',
'window',
'document',
'inspect',
'isSinonProxy',
'_spreadArray',
'selector',
]

/**
* Create a proxy object to fail when accessed or called.
* @param type The type of operand that failed to serialize
* @returns A proxy object that will fail when accessed.
*/
const failedToSerializeSubject = (type: string) => {
let target = {}

// If the failed subject is a function, use a function as the target.
if (type === 'function') {
target = () => {}
}

// Symbol note: The target can't be a symbol, but we can use an object until the symbol is accessed, then provide a different error.

return new Proxy(target, {
/**
* Throw an error if the proxy is called like a function.
* @param target the proxy target
* @param thisArg this
* @param argumentsList args passed.
*/
apply (target, thisArg, argumentsList) {
$errUtils.throwErrByPath('switchToDomain.failed_to_serialize_function')
},

/**
* Throw an error if any properties besides the listed ones are accessed.
* @param target The proxy target
* @param prop The property being accessed
* @param receiver Either the proxy or an object that inherits from the proxy.
* @returns either an error or the result of the allowed get on the target.
*/
get (target, prop, receiver) {
if (passThroughProps.includes(prop)) {
return target[prop]
}

// Provide a slightly different message if the object was meant to be a symbol.
if (type === 'symbol') {
$errUtils.throwErrByPath('switchToDomain.failed_to_serialize_symbol')
} else {
$errUtils.throwErrByPath('switchToDomain.failed_to_serialize_object')
}
},
})
}

export { failedToSerializeSubject }
10 changes: 7 additions & 3 deletions packages/driver/src/cy/multi-domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import $errUtils from '../../cypress/error_utils'
import { CommandsManager } from './commands_manager'
import { LogsManager } from './logs_manager'
import { Validator } from './validator'
import { failedToSerializeSubject } from './failedSerializeSubjectProxy'

export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State, config: Cypress.InternalConfig) {
let timeoutId
Expand Down Expand Up @@ -118,7 +119,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,
logsManager.listen()

return new Bluebird((resolve, reject) => {
communicator.once('ran:domain:fn', (err) => {
communicator.once('ran:domain:fn', ({ subject, failedToSerializeSubjectOfType, err }) => {
sendReadyForDomain()
if (err) {
if (done) {
Expand All @@ -141,9 +142,12 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,
// is common if there are only assertions enqueued in the SD.
if (!commandsManager.hasCommands && !done) {
cleanup()
}

resolve()
// This handles when a subject is returned synchronously
resolve(failedToSerializeSubjectOfType ? failedToSerializeSubject(failedToSerializeSubjectOfType) : subject)
} else {
resolve()
}
})

// If done is NOT passed into switchToDomain, wait for the command queue
Expand Down
Loading

0 comments on commit c58a0ee

Please sign in to comment.