Recently, I was asked to write a solid test for an online demo e-commerce shop https://demo.prestashop.com. The site loads a temporary demo shop. We want to write a Cypress test that picks an item and adds it to the cart and goes to the checkout.
Let's start. First, visit the page.
1 2 3
it('goes to the checkout with one item', () => { cy.visit('/') })
You see the loader element at the start. It is outside the iframe itself, let's wait for it to disappear.
1 2 3 4
it('goes to the checkout with one item', () => { cy.visit('/') cy.get('#loadingMessage', { timeout: 15_000 }).should('not.be.visible') })
It might take a while to spin up a new shop instance, thus I set the maximum timeout of 15 seconds.
it('goes to the checkout with one item', () => { cy.visit('/') cy.get('#loadingMessage', { timeout: 15_000 }).should('not.be.visible') cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('.header-top') .should('be.visible') .contains('a', 'Clothes') .click() cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('.breadcrumb') .should('include.text', 'Clothes') cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') // we know the iframe body is there // but we need a jQuery object to be able to use cy.contains // otherwise we see "subject.each" is not a function error .then(cy.wrap) .contains('.category-sub-menu a', 'Men') .click() cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('.breadcrumb') .should('include.text', 'Men') })
Every time we click on the link or button, we check that the page has finished updating.
click on the "Clothes" link
confirm the page shows the breadcrumb "Clothes"
click on the "Men" link
confirm the page shows the breadcrumb "Men"
Note: I copy/paste the code to access the iframe's contents, we can clean it up later.
Add item to the cart
We see at least one item of clothing. Let's click on it and add it to the cart. We expect to see the following:
After clicking on the "Add to Cart" button we should see the cart modal fade in.
// click on the first product item cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('.products .product') .should('have.length.greaterThan', 0) .first() .click()
// click on the "Add to cart" button cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('#main .product-container') .should('be.visible') .within(() => { cy.contains('.product-add-to-cart', 'Add to cart') cy.contains('button', 'Add to cart').should('be.visible').click() })
// the cart view appears cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('#blockcart-modal') .should('be.visible')
Hmm, the test clicked on the "Add to Cart" button, it the cart is empty and the test fails. What is happening? It works if we add a cy.wait before clicking on the "Add to Cart" button:
1 2 3 4 5 6 7 8 9 10 11
// click on the "Add to cart" button cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('#main .product-container') .should('be.visible') // ANTI-PATTERN: add a hard-coded wait .wait(200) .within(() => { cy.contains('.product-add-to-cart', 'Add to cart') cy.contains('button', 'Add to cart').should('be.visible').click() })
I would like to avoid hard-coded waits. Let's open DevTools to understand why the item does not "register" the click from the test.
Do you see the waterfall of small JavaScript scripts the page is downloading when we go to the Item view? It takes time to load all of them, one of them registers the "click" event listener on the "Add to Cart" button. The test clicks too fast, the event listener hasn't been downloaded or registered yet, so it does nothing. We need to wait for the JS resources to finish before clicking.
A good way to do this is by using my plugin cypress-network-idle. Let's wait for all JS resources before clicking.
// click on the first product item cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('.products .product') .should('have.length.greaterThan', 0) .first() .click()
// click on the "Add to cart" button cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('#main .product-container') .should('be.visible') .within(() => { cy.waitForNetworkIdle('@js', 40) cy.contains('.product-add-to-cart', 'Add to cart') cy.contains('button', 'Add to cart').should('be.visible').click() }) ... })
We start spying on *.js network calls before the item component appears. Once it appears we wait for JS resources to stop for at least 40ms.
We don't know which particular JS script reacts to the button click. If we knew, we could have targeted it better. But for now, we wait for all of them:
Burn it
Good. Now we can finish the test and run it multiple times in a row to confirm that it is solid. Following Cypress Flaky Tests Exercises
Cypress._.times(10, (k) => { it(`goes to the checkout with one item ${k + 1} / 10`, () => { cy.visit('/') cy.get('#loadingMessage', { timeout: 15_000 }).should('not.be.visible') ...
cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('#checkout-personal-information-step') .should('be.visible') }) })
Seems solid.
Simplify the code
Our testing code is very verbose when accessing the iframe. We have two query chains essentially to access the elements inside the iframe:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// BODY -> cy.find cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') .find('.header-top') .should('be.visible') .contains('a', 'Clothes') .click() // BODY -> cy.then(cy.wrap).contains cy.get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') // we know the iframe body is there // but we need a jQuery object to be able to use cy.contains // otherwise we see "subject.each" is not a function error .then(cy.wrap) .contains('.category-sub-menu a', 'Men') .click()
Note: Cypress throws an error if we directly attach cy.contains to the BODY element like this: .its('0.contentDocument.body').contains. Thus we have to use cy.then(cy.wrap) which breaks the retries. Luckily we can first confirm the element is there using cy.find and then look at the text.
constgetIframeBody = (wrap = false) => { if (wrap) { return ( cy .get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') // we know the iframe body is there // but we need a jQuery object to be able to use cy.contains // otherwise we see "subject.each" is not a function error .then(cy.wrap) ) } else { return cy .get('iframe[title="Frame of demo shop"]') .its('0.contentDocument.body') } }
it('goes to the checkout with one item', () => { cy.visit('/') cy.get('#loadingMessage', { timeout: 15_000 }).should('not.be.visible') getIframeBody() .find('.header-top') .should('be.visible') .contains('a', 'Clothes') .click() getIframeBody().find('.breadcrumb').should('include.text', 'Clothes') getIframeBody(true).contains('.category-sub-menu a', 'Men').click() getIframeBody().find('.breadcrumb').should('include.text', 'Men')
constiframeContains = (selector, text) => iframeBody() // we know the iframe body is there // but we need a jQuery object to be able to use cy.contains // otherwise we see "subject.each" is not a function error .then(cy.wrap) .contains(selector, text)
it('goes to the checkout with one item', () => { cy.visit('/') cy.get('#loadingMessage', { timeout: 15_000 }).should('not.be.visible') iframeFind('.header-top') .should('be.visible') .contains('a', 'Clothes') .click() iframeFind('.breadcrumb').should('include.text', 'Clothes') iframeContains('.category-sub-menu a', 'Men').click() iframeFind('.breadcrumb').should('include.text', 'Men')
// click on the first product item iframeFind('.products .product') .should('have.length.greaterThan', 0) .first() .click()
// click on the "Add to cart" button iframeFind('#main .product-container') .should('be.visible') .within(() => { cy.waitForNetworkIdle('@js', 40) cy.contains('.product-add-to-cart', 'Add to cart') cy.contains('button', 'Add to cart').should('be.visible').click() })
iframeFind('#blockcart-modal') .should('be.visible') .contains('a', 'Proceed to checkout') .click()
iframeFind('.cart-grid') .should('be.visible') .contains('a', 'Proceed to checkout') .click()