Skip to content
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

chore: [Multi-domain]: Support multiple multi-domain commands in a single test #20354

Merged
merged 7 commits into from
Feb 24, 2022

Conversation

mjhenkes
Copy link
Member

User facing changelog

n/a

Additional details

This PR introduces the concept of running multiple multi-domain command in a single test. To accomplish this, I have added logic to:

  • Find an appropriate bridge when a multi-domain AUT is loaded
  • Send bridge messages to a specific domain
  • Send bridge messages to all domains
  • Identify which domain sent a message to primary

What this does not fix:

  • Setting readyForMultiDomain to false
  • Anticipating a domain that never loads

How has the user experience changed?

PR Tasks

  • Have tests been added/updated?
  • Has the original issue (or this PR, if no issue exists) been tagged with a release in ZenHub? (user-facing changes only)
  • Has a PR for user-facing changes been opened in cypress-documentation?
  • Have API changes been updated in the type definitions?
  • Have new configuration options been added to the cypress.schema.json?

@mjhenkes mjhenkes added the topic: cy.origin Problems or enhancements related to cy.origin command label Feb 24, 2022
@mjhenkes mjhenkes requested a review from a team as a code owner February 24, 2022 16:25
@mjhenkes mjhenkes self-assigned this Feb 24, 2022
@cypress-bot
Copy link
Contributor

cypress-bot bot commented Feb 24, 2022

Thanks for taking the time to open a PR!

hookId: state('hookId'),
},
communicator.once('bridge:ready', (_data, bridgeReadyDomain) => {
if (bridgeReadyDomain === domain) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can verify the bridge is ready for the correct domain now, not sure if this is ever not the same or if we should do something more drastic, but it's theoretically possible.

communicator.once('bridge:ready', (_data, bridgeReadyDomain) => {
if (bridgeReadyDomain === domain) {
// now that the spec bridge is ready, instantiate Cypress with the current app config and environment variables for initial sync when creating the instance
communicator.toSpecBridge(domain, 'initialize:cypress', {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell the domain we just created to initialize cypress

// once the secondary domain page loads, send along the
// user-specified callback to run in that domain
try {
communicator.toSpecBridge(domain, 'run:domain:fn', {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just run the domain in question

@@ -534,7 +534,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// not utilized
try {
this.Cypress.action('app:window:load', this.state('window'))
this.Cypress.multiDomainCommunicator.toSpecBridge('window:load', { url: this.getRemoteLocation('href') })
this.Cypress.multiDomainCommunicator.toAllSpecBridges('window:load', { url: this.getRemoteLocation('href') })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any domain could be anticipating a window load, so tell them all

@@ -1527,7 +1527,7 @@ export default {
}

cy.state('duringUserTestExecution', false)
Cypress.multiDomainCommunicator.toSpecBridge('sync:state', { 'duringUserTestExecution': false })
Cypress.multiDomainCommunicator.toAllSpecBridges('sync:state', { 'duringUserTestExecution': false })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep all domains in sync

private windowReference
private crossDomainDriverWindow
private windowReference: Window | undefined
private crossDomainDriverWindows: {[key: string]: Window} = {}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now a map of domains to windows

@@ -105,9 +105,21 @@ export class PrimaryDomainCommunicator 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.
*/
toSpecBridge (event: string, data?: any) {
toAllSpecBridges (event: string, data?: any) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opinions on function naming? this is kinda awkward, but also clear

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could go with some sort of broadcast naming.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking some sort of broadcast, but wasn't sure if that was more clear or just a more enjoyable word to say.

@@ -522,13 +522,13 @@ export const eventManager = {
Cypress.emit('internal:window:load', { type: 'cross:domain', url })
})

Cypress.multiDomainCommunicator.on('expect:domain', (domain) => {
Cypress.on('expect:domain', (domain) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this because it made more sense to me that cypress is sending the message than the multi-domain communicator

@@ -513,7 +513,7 @@ export const eventManager = {
})

Cypress.on('test:before:run', (...args) => {
Cypress.multiDomainCommunicator.toSpecBridge('test:before:run', ...args)
Cypress.multiDomainCommunicator.toAllSpecBridges('test:before:run', ...args)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all domains should be notified before the test runs

if (options.syncConfig) this.syncConfigEnvToPrimary()

this.handleSubjectAndErr(data, (data: any) => {
this.windowReference.top.postMessage({
event: `${CROSS_DOMAIN_PREFIX}${event}`,
data,
domain: location.hostname,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now appending the domain to ALL messages sent to the primary.

}

this.emit(messageName, data.data)
this.emit(messageName, data.data, data.domain)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Domain the message originated from is now attached to all emitted messages

@@ -105,9 +105,21 @@ export class PrimaryDomainCommunicator 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.
*/
toSpecBridge (event: string, data?: any) {
toAllSpecBridges (event: string, data?: any) {
debug('=> to all spec bridges', event, data)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these debug messages on eventing, I find that when i'm debugging multi-domain I'm always setting these to see what communication is happening.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny story: to enable these debug messages you have to add a local storage variable localStorage.debug = 'cypress:*', but with session enabled, we clear local storage after each test. 🤦‍♂️

const callback = () => {
Cypress.multiDomainCommunicator.toSpecBridge('before:screenshot:end')
Cypress.multiDomainCommunicator.toSpecBridge(domain, 'before:screenshot:end')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we get the originating domain we handily also know where to send the message back

if (!Cypress) {
throw new Error('Something went terribly wrong and we cannot proceed. We expected to find the global \
Cypress in the spec bridge window but it is missing. This should never happen and likely is a bug. Please open an issue.')
Cypress in the spec bridge window but it is missing.')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could legitimately happen if a click causes several redirects and finally lands on the specified domain. That shouldn't be an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do we even want this error? Seems odd to say something went terribly wrong for an expected scenario.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will want to remove this error in the long run. Not finding cypress may or may not be a problem depending if this is the last domain loaded and I don't think we can know that. Even if it is the last domain loaded this error isn't bubbled up to the primary domain to display and the test times out.

I think the onus lies on the primary domain and ideally, the command that interacts with the dom to timeout if the correct domain hasn't loaded in an appropriate amount of time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, I somehow missed the comment you added above the line


try {
// If Cypress is defined and we haven't gotten a cross domain error we have found the correct bridge.
if (frame.Cypress) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enough, I don't think there will be other frames on the same domain that also have cypress defined, it should just be the bridge.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think it's safe to assume.

}
} catch (error) {
// Catch DOMException: Blocked a frame from accessing a cross-origin frame.
if (error.name !== 'SecurityError') {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrowing the exception we're catching as much as possible.

@cypress
Copy link

cypress bot commented Feb 24, 2022



Test summary

4611 0 79 1Flakiness 2


Run details

Project cypress
Status Passed
Commit 8207496
Started Feb 24, 2022 10:25 PM
Ended Feb 24, 2022 10:37 PM
Duration 11:48 💡
OS Linux Debian - 10.10
Browser Electron 94

View run in Cypress Dashboard ➡️


Flakiness

commands/net_stubbing_spec.ts Flakiness
1 network stubbing > intercepting response > can 'delay' a proxy response using Promise.delay
cypress/proxy-logging_spec.ts Flakiness
1 Proxy Logging > request logging > xhr log has response body/status code

This comment has been generated by cypress-bot as a result of this project's GitHub integration settings. You can manage this integration in this project's settings in the Cypress Dashboard

Copy link
Contributor

@chrisbreiding chrisbreiding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind updating the FIXME tests here, now that multiple switchToDomains are supported.

packages/driver/src/cy/multi-domain/index.ts Outdated Show resolved Hide resolved
packages/driver/src/cy/multi-domain/index.ts Outdated Show resolved Hide resolved

try {
// If Cypress is defined and we haven't gotten a cross domain error we have found the correct bridge.
if (frame.Cypress) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think it's safe to assume.

packages/runner/injection/multi-domain.js Outdated Show resolved Hide resolved
@@ -105,9 +105,21 @@ export class PrimaryDomainCommunicator 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.
*/
toSpecBridge (event: string, data?: any) {
toAllSpecBridges (event: string, data?: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me.

if (!Cypress) {
throw new Error('Something went terribly wrong and we cannot proceed. We expected to find the global \
Cypress in the spec bridge window but it is missing. This should never happen and likely is a bug. Please open an issue.')
Cypress in the spec bridge window but it is missing.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do we even want this error? Seems odd to say something went terribly wrong for an expected scenario.

if (options.syncConfig) this.syncConfigEnvToPrimary()

this.handleSubjectAndErr(data, (data: any) => {
this.windowReference.top.postMessage({
event: `${CROSS_DOMAIN_PREFIX}${event}`,
data,
domain: location.hostname,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The host name is converted into punycode, which will not match the domain specified in the command. To circumvent this and avoid adding a dependency to convert punycode to unicode I have attached the unicode domain name to the window via the server.

@@ -7,6 +7,7 @@
<body>
<script type="text/javascript">
document.domain = '{{domain}}';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the browser converts document.domain to punycode by default, how helpful, but doesn't provide any API's to convert it back to unicode. So I attach the domain to window below.

@mjhenkes mjhenkes merged commit a0c6f00 into feature-multidomain Feb 24, 2022
@mjhenkes mjhenkes deleted the md-multi-multi-domains branch February 24, 2022 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: cy.origin Problems or enhancements related to cy.origin command
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants