-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: [Multi-domain]: Yield subject from switchToDomain
#19936
Conversation
Thanks for taking the time to open a PR!
|
* @param value any object to be serialized. | ||
* @returns the serialized object as a string or undefined, if the object cannot be serialized. | ||
*/ | ||
const serialize = (value: any): string => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AtofStryker, I know that you put together some level of serialization for the errors but this seemed different enough? Perhaps I should rename this subjectSerializer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just saw this comment after review. Yeah I think that is a good idea to distinguish between the two! I have a feeling we are going to have to address serialization pretty soon since it sounds like we are going to have quite a few and might need to see where we can consolidate on.
try { | ||
serializedSubject = JSON.stringify(value, (key, value) => { | ||
// If we encounter any unserializable values we want to abort the whole process. | ||
if (isFunction(value) || isSymbol(value) || value === undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't port undefined
using json, even trying to sub it out for our own custom string would require us to write our own parse function. I could transform undefined
values into null
but that may be confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this is more of a user experience type of thing. I wonder if assigning null
on purpose would tell the end user that null
was intentionally assigned because we couldn't serialize their yield. But that also might be confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we probably need to discuss this with Ben and Brian. We talked about making unserialized values undefined, but not about objects that might have unserializable values within them, so I think we need to define that behavior. I'm not sure if throwing away the whole object is the right way to go or not.
* @returns the deserialized object | ||
*/ | ||
const deserialize = (value: string|undefined): string|object|boolean|number|null|any[] => { | ||
return value ? JSON.parse(value) : value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code is pretty simple but i liked pairing a deserialize function with my serialize function.
@@ -50,7 +50,7 @@ const toSerializedJSON = function (attrs) { | |||
return value() | |||
} | |||
|
|||
if (_.isFunction(value)) { | |||
if (_.isFunction(value) || _.isSymbol(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes an error where we're trying to log out an object containing a symbol
|
||
resolve() | ||
resolve(deserializedSubject) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is where we resolve anything returned
from the switchToDomain function. I'm not sure when it makes sense to synchronously return something from switchToDomain
, but i'm matching the functionality of the then
function blocks.
return command.deferred.resolve() | ||
const deserializedSubject = deserialize(subject) | ||
|
||
return command.deferred.resolve(deserializedSubject) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we're returning the last yielded subject from the last run cy command.
@@ -14,7 +15,11 @@ export const handleCommands = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm | |||
const id = command.get('id') | |||
const name = command.get('name') | |||
|
|||
specBridgeCommunicator.toPrimary('command:end', { id, name }) | |||
let serializedSubject = serialize(cy.state('subject')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be possible to avoid doing this if we aren't the last command in the queue. I'll investigate that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could do this, is this crazy? We're getting the last command in the queue and comparing ids
let serializedSubject = serialize(cy.state('subject')) | |
let serializedSubject: string | undefined = undefined | |
if (cy.queue.at(cy.queue.length - 1).get('id') === id) { | |
serializedSubject = serialize(cy.state('subject')) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we ever need to yield something back that isn't the last command? I don't think it's crazy and it could save on serialization costs if other commands are yielding subjects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a good idea to me. Let's go ahead and add a cy.queue.last()
method or even a cy.queue.isLast(id)
method.
switchToDomain
switchToDomain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few small comments and suggestions. Looking good!
packages/driver/cypress/integration/e2e/multi-domain/multi_domain_spec.ts
Outdated
Show resolved
Hide resolved
}) | ||
}).should('equal', undefined) | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we want to do something similar with Errors here since they aren't serializable through the structured clone algorithm in Firefox. Since we already attempt to serialize errors to objects and back to errors in the primary, I wonder if we want to leverage the util function here in the serializer.
Or we can also just return undefined
if an error is yielded to be consistent with the rest of the unserializable attributes. I'm also not sure why someone would want to yield an error 🤷♂️ .
it('yields undefined if an object contains an error', () => {
cy.switchToDomain('foobar.com', () => {
cy.wrap({
key: new Error('Boom goes the dynamite'),
})
}).should('equal', undefined)
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect an unserializable values to be undefined, not the whole object itself. So I would think the 3 above tests would yield an empty object ({}
) instead of undefined. What happens if there are other serializable values in the object like { key: undefined, otherKey: 'defined' }
?
* @param value a string | ||
* @returns the deserialized object | ||
*/ | ||
const deserialize = (value: string|undefined): string|object|boolean|number|null|any[] => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const deserialize = (value: string|undefined): string|object|boolean|number|null|any[] => { | |
const deserializeSubject = (value: string|undefined): string|object|boolean|number|null|any[] => { |
* @param value any object to be serialized. | ||
* @returns the serialized object as a string or undefined, if the object cannot be serialized. | ||
*/ | ||
const serialize = (value: any): string | undefined => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we now have multiple serializers going on, should we prefix this to make the functions intent clearer?
const serialize = (value: any): string | undefined => { | |
const serializeSubject = (value: any): string | undefined => { |
try { | ||
serializedSubject = JSON.stringify(value, (key, value) => { | ||
// If we encounter any unserializable values we want to abort the whole process. | ||
if (isFunction(value) || isSymbol(value) || value === undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Total nitpick, but more of a readability thing here. I think this also allows for void
operator support, but I wouldn't think that would be applicable here.
if (isFunction(value) || isSymbol(value) || value === undefined) { | |
if (isFunction(value) || isSymbol(value) || isUndefined(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also might need to add isError
here as well.
try { | ||
serializedSubject = JSON.stringify(value, (key, value) => { | ||
// If we encounter any unserializable values we want to abort the whole process. | ||
if (isFunction(value) || isSymbol(value) || value === undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this is more of a user experience type of thing. I wonder if assigning null
on purpose would tell the end user that null
was intentionally assigned because we couldn't serialize their yield. But that also might be confusing.
|
||
specBridgeCommunicator.toPrimary('ran:domain:fn') | ||
specBridgeCommunicator.toPrimary('ran:domain:fn', { subject: serialize(subject) }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does leveraging the serializer
argument in toPrimary
work here?
specBridgeCommunicator.toPrimary('ran:domain:fn', { subject: serialize(subject) }) | |
specBridgeCommunicator.toPrimary('ran:domain:fn', { subject }, serialize) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but i should make it work, i just realized I'm doing double serialization
@@ -14,7 +15,11 @@ export const handleCommands = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm | |||
const id = command.get('id') | |||
const name = command.get('name') | |||
|
|||
specBridgeCommunicator.toPrimary('command:end', { id, name }) | |||
let serializedSubject = serialize(cy.state('subject')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we ever need to yield something back that isn't the last command? I don't think it's crazy and it could save on serialization costs if other commands are yielding subjects.
packages/driver/cypress/integration/e2e/multi-domain/multi_domain_spec.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Bill Glesias <bglesias@gmail.com>
it('yields the cy value even if a return is present', () => { | ||
cy.switchToDomain('foobar.com', () => { | ||
cy | ||
.get('[data-cy="dom-check"]') | ||
.invoke('text') | ||
|
||
return 'text' | ||
}).should('equal', 'From a secondary domain') | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might require some discussion, but I would expect this to behave similar to .then
, where we throw an error in this case, since it's probably not what the user expects the yielded value to be.
cy
.visit('/test.html')
.then(() => {
cy.get('h1').invoke('text')
return 'text'
})
.should('equal', 'text')
results in this error:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, I'll look into it
}) | ||
}).should('equal', undefined) | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect an unserializable values to be undefined, not the whole object itself. So I would think the 3 above tests would yield an empty object ({}
) instead of undefined. What happens if there are other serializable values in the object like { key: undefined, otherKey: 'defined' }
?
@@ -14,7 +15,11 @@ export const handleCommands = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeComm | |||
const id = command.get('id') | |||
const name = command.get('name') | |||
|
|||
specBridgeCommunicator.toPrimary('command:end', { id, name }) | |||
let serializedSubject = serialize(cy.state('subject')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a good idea to me. Let's go ahead and add a cy.queue.last()
method or even a cy.queue.isLast(id)
method.
try { | ||
serializedSubject = JSON.stringify(value, (key, value) => { | ||
// If we encounter any unserializable values we want to abort the whole process. | ||
if (isFunction(value) || isSymbol(value) || value === undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we probably need to discuss this with Ben and Brian. We talked about making unserialized values undefined, but not about objects that might have unserializable values within them, so I think we need to define that behavior. I'm not sure if throwing away the whole object is the right way to go or not.
Co-authored-by: Matt Schile <mschile@gmail.com>
Co-authored-by: Matt Schile <mschile@gmail.com>
packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts
Outdated
Show resolved
Hide resolved
packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts
Outdated
Show resolved
Hide resolved
packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts
Show resolved
Hide resolved
|
||
cy.switchToDomain('foobar.com', () => { | ||
cy.wrap({ | ||
key: new Error('Boom goes the dynamite'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of scope here, but I am also wondering if we should do something similar with test errors. In other words, only try to preprocess the error if the browser can't serialize it through postMessage
. Otherwise, just send it as is as opposed to always preprocessing the error.
packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts
Outdated
Show resolved
Hide resolved
*/ | ||
get (target, prop, receiver) { | ||
// These properties are required to avoid failing prior to attempting to use the subject. | ||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we refactor this to a list on constants like const unserializableSubjects = ['then', 'jquery', ...]
and do something like unserializableSubjects.includes(prop)
since we are working with strings? It might make this a bit easier to read and maintain in the future.
// This handles when a subject is returned synchronously | ||
if (failedToSerializeSubjectOfType) { | ||
return resolve( | ||
failedToSerializeSubject(failedToSerializeSubjectOfType), | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Total nit/preference here. I'm going to apologize for the formatting in this suggestion in advance 😅
// This handles when a subject is returned synchronously | |
if (failedToSerializeSubjectOfType) { | |
return resolve( | |
failedToSerializeSubject(failedToSerializeSubjectOfType), | |
) | |
} | |
// This handles when a subject is returned synchronously | |
const serializedSubject = failedToSerializeSubjectOfType ? | |
failedToSerializeSubject(failedToSerializeSubjectOfType) : | |
subject | |
resolve(serializedSubject) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated with a single line variant fo this suggestion
const { isDom } = $dom | ||
|
||
if (_.isError(value)) { | ||
const serializedError = _.mapValues(clone(value), serializeForPostMessage) | ||
const serializedError = _.mapValues(clone(value), preprocessErrorForPostMessage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still want to call this a serializedError
? I was almost thinking processedError
but that almost sounds more confusing 😅
const serializedError = _.mapValues(clone(value), preprocessErrorForPostMessage) | |
const processedError = _.mapValues(clone(value), preprocessErrorForPostMessage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if we are using this for only errors now and not generic serialization, I wonder if we can gut like 90% of this function and just imply that value is of type Error
or if its undefined
just do nothing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with serializableError
. I do wonder what we could trim out from this function, but haven't dug into what could be in error that needs to be serialized since we do call this function recursively to serialize anything in error.
@@ -1710,6 +1710,32 @@ export default { | |||
|
|||
This is likely because the data argument specified is not serializable. Note that functions and DOM objects cannot be serialized.`, | |||
}, | |||
callback_mixes_sync_and_async: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if something like callback_mixes_sync_and_commands
is more appropriate? Not entirely sure 🤔
packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Bill Glesias <bglesias@gmail.com>
cy | ||
.get('[data-cy="dom-check"]') | ||
}).invoke('text') | ||
.should('equal', 'From a secondary domain') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can probably remove these should
s now since they aren't getting hit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind of like having them there to show how a user might think this test works, but doesn't. Almost like a documentation artifact.
packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts
Outdated
Show resolved
Hide resolved
…y.ts Co-authored-by: Matt Schile <mschile@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
cy | ||
.get('[data-cy="dom-check"]') | ||
}).invoke('text') | ||
.should('equal', 'From a secondary domain') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind of like having them there to show how a user might think this test works, but doesn't. Almost like a documentation artifact.
User facing changelog
n/a
Additional details
With this change we are yielding the last subject returned by the last run cy command or the return statement of synchronous execution. If the subject or return cannot be serialized we return undefined.
How has the user experience changed?
Users may now write tests like this
PR Tasks
cypress-documentation
?type definitions
?cypress.schema.json
?