it('fills the login form', () => { cy.visit('/') // slowly type the username and the password values // "username" and "password" LoginPage.getUsername().type('username', { delay: 200 }) LoginPage.getPassword().type('password', { delay: 100 }) // confirm the username and the password input elements // have the expected values we typed LoginPage.getUsername().should('have.value', 'username') LoginPage.getPassword().should('have.value', 'password') })
If the popup always appears, and we simply do not know the precise timing, we could always wait for it and close using the normal Cypress commands.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
it('waits for the modal to click close', () => { cy.visit('/') // if the modal always appears, let's just wait for it // and click the "close modal" element cy.get('#close-modal').click() // then type the username and the password values // "username" and "password" LoginPage.getUsername().type('username', { delay: 200 }) LoginPage.getPassword().type('password', { delay: 100 }) // confirm the username and the password input elements // have the expected values we typed LoginPage.getUsername().should('have.value', 'username') LoginPage.getPassword().should('have.value', 'password') })
Did you see that tiny flash? That was the modal element appearing and then disappearing. We can see it better by hovering or clicking on the "CLICK" command in the Command Log column
Non-deterministic modal
What if the modal appears only sometimes? We cannot use cy.get('#close-modal').click() because it will fail if the modal never exists. We could wait for N seconds and then check.
1 2 3 4 5 6 7 8 9 10 11
cy.wait(5000) cy.get('#close-modal') // disable the built-in existence assertion .should(Cypress._.noop) .then($el => { // only click on the close button // if the popup element exists if ($el.length) { cy.wrap($el).click() } })
Tip: I have disabled the built-in element existence assertion using .should(Cypress._.noop) assertion. For more, watch the video Built-in Existence Assertion.
Unfortunately the above approach slows down your tests.
Use MutationObserver
Let's use a solution that does not rely on Cypress commands. Instead, let's insert a MutationObserver and use "regular" JavaScript to hide the popup element as soon as it appears. The popup is attached right to the <body> element of the page.
As soon as the element with id=modal is added to the body we can set its display state to hide it.
it('closes a random popup', () => { // visit the login page "/" // https://on.cypress.io/visit // which yields the window object // create a new MutationObserver object to observe // child list changes on the "window.document.body" node // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver cy.visit('/').then((window) => { const body = window.document.body constcallback = (mutations: MutationRecord[]) => { // if you see a new Node with id "modal" has been added // set its style to "display: none" to hide it if (mutations.length && mutations[0].addedNodes.length) { // @ts-ignore TS2339 if (mutations[0].addedNodes[0].id === 'modal') { console.log('added modal') // @ts-ignore TS2339 mutations[0].addedNodes[0].style.display = 'none' } } } const observer = newMutationObserver(callback) observer.observe(body, { childList: true }) }) // slowly type the username and the password values // "username" and "password" LoginPage.getUsername().type('username', { delay: 200 }) LoginPage.getPassword().type('password', { delay: 100 }) // confirm the username and the password input elements // have the expected values we typed LoginPage.getUsername().should('have.value', 'username') LoginPage.getPassword().should('have.value', 'password') })
Here is the test in action
The element is hidden even before it appears on the screen. Nice! If the random is never created by the application, no worries, the test just continues.
Every window
If the test goes from page to page, a new window object is created each time. You can inject your code into every window by subscribing to the window:load event:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
cy.on('window:load', (window) => { const body = window.document.body constcallback = (mutations: MutationRecord[]) => { // if you see a new Node with id "modal" has been added // set its style to "display: none" to hide it if (mutations.length && mutations[0].addedNodes.length) { // @ts-ignore TS2339 if (mutations[0].addedNodes[0].id === 'modal') { console.log('added modal') // @ts-ignore TS2339 mutations[0].addedNodes[0].style.display = 'none' } } } const observer = newMutationObserver(callback) observer.observe(body, { childList: true }) }) cy.visit('/')
Confirm the popup was handled
How do we know if the popup was hidden? Let's say the popup always appears. We can set a property on the window object to use the built-in retry-ability and check for it.
// enable only if the modal always appears during testing it('closes a random popup with window property confirmation', () => { // subscribe to the "window:load" event // create a new MutationObserver object to observe // child list changes on the "window.document.body" node // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver cy.on('window:load', (window) => { const body = window.document.body constcallback = (mutations: MutationRecord[]) => { // if you see a new Node with id "modal" has been added // set its style to "display: none" to hide it if (mutations.length && mutations[0].addedNodes.length) { // @ts-ignore TS2339 if (mutations[0].addedNodes[0].id === 'modal') { // @ts-ignore TS2339 window.popupHandled = true // @ts-ignore TS2339 mutations[0].addedNodes[0].style.display = 'none' } } } const observer = newMutationObserver(callback) observer.observe(body, { childList: true }) })
// visit the login page "/" // https://on.cypress.io/visit cy.visit('/') // slowly type the username and the password values // "username" and "password" LoginPage.getUsername().type('username', { delay: 200 }) LoginPage.getPassword().type('password', { delay: 100 }) // confirm the username and the password input elements // have the expected values we typed LoginPage.getUsername().should('have.value', 'username') LoginPage.getPassword().should('have.value', 'password') // confirm the popup was added and hidden cy.window().should('have.property', 'popupHandled', true) })
Instead of adding a property to the window object, we can use a local object and add a property there:
// enable only if the modal always appears during testing it('closes a random popup with window property confirmation', () => { // subscribe to the "window:load" event // create a new MutationObserver object to observe // child list changes on the "window.document.body" node // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver const o = { popupHandled: false, }
cy.on('window:load', (window) => { const body = window.document.body constcallback = (mutations: MutationRecord[]) => { // if you see a new Node with id "modal" has been added // set its style to "display: none" to hide it if (mutations.length && mutations[0].addedNodes.length) { // @ts-ignore TS2339 if (mutations[0].addedNodes[0].id === 'modal') { o.popupHandled = true // @ts-ignore TS2339 mutations[0].addedNodes[0].style.display = 'none' } } } const observer = newMutationObserver(callback) observer.observe(body, { childList: true }) })
// visit the login page "/" // https://on.cypress.io/visit cy.visit('/') // slowly type the username and the password values // "username" and "password" LoginPage.getUsername().type('username', { delay: 200 }) LoginPage.getPassword().type('password', { delay: 100 }) // confirm the username and the password input elements // have the expected values we typed LoginPage.getUsername().should('have.value', 'username') LoginPage.getPassword().should('have.value', 'password') // confirm the popup was added and hidden (retries) cy.wrap(o).should('have.property', 'popupHandled', true) })
Again, we use the retry-ability in the command and assertion:
1 2 3 4
const o = { ... } // code sets a property inside object "o" at some point // confirm the popup was added and hidden (retries) cy.wrap(o).should('have.property', 'popupHandled', true)
Use a spy confirmation
Instead of the window or a local object, we can use a spy function to keep track of events and then confirm they have happened.
// enable only if the modal always appears during testing it('closes a random popup with spy confirmation', () => { // if we know the modal always appears, we can // create a spy and give it an alias "modalClosed" // https://on.cypress.io/spy // https://on.cypress.io/as const modalClosed = cy.spy().as('modalClosed')
// repeat the same setup as in the previous test cy.visit('/').then((window) => { const body = window.document.body constcallback = (mutations: MutationRecord[]) => { if (mutations.length && mutations[0].addedNodes.length) { // @ts-ignore TS2339 if (mutations[0].addedNodes[0].id === 'modal') { console.log('added modal') // @ts-ignore TS2339 mutations[0].addedNodes[0].style.display = 'none' // after hiding the modal element using "display: none" // call the spy function we created above modalClosed() } } } const observer = newMutationObserver(callback) observer.observe(body, { childList: true }) })
// slowly type the username and the password values // "username" and "password" LoginPage.getUsername().type('username', { delay: 200 }) LoginPage.getPassword().type('password', { delay: 100 }) // confirm the username and the password input elements // have the expected values we typed LoginPage.getUsername().should('have.value', 'username') LoginPage.getPassword().should('have.value', 'password')
cy.log('**confirm the modal was hidden**') // confirm the spy "modalClosed" was called once (retries) cy.get('@modalClosed').should('have.been.calledOnce') })
The const modalClosed = cy.spy().as('modalClosed') creates a spy modalClosed. It is just a function - but it keeps a record of every call made, thus we can use it later to confirm it was really called (and even check its arguments): cy.get('@modalClosed').should('have.been.calledOnce').
Here is the spy code in action:
I really like this approach since I can see the made calls in the Command Log.