Naturally, when testing a project, you need to sign in. And the sign in flow will often be one of the first tests you write. A nearly ubiquitous pattern these days for signing in to the app is to use a third-party single sign-on. Single Sign-On (SSO) is a great technology to make it easy for users to securely log into any app or page that uses a particular service without necessarily having to put in their credentials in over and over, whether they be various offerings from one company or various apps that use the same third-party sign-on. You’ve most likely seen Google’s, Facebook’s, and Github’s, but there are others too.
Of course, you aren’t testing how Google or Facebook wrote their login fields so you can just use their handy API to get past that gatekeeper and into the code your developers wrote. Cypress even provides a handy recipe so you don’t have to reinvent the wheel. But as we all know, sometimes things can’t be that easy. Some SSO providers don’t give you a handy API. That’s fine. You can just use Cypress to imitate a user and cy.type() and cy.click()your way in. After all, that’s what it is best at. But here is where we hit a roadblock. I’m looking at you, Azure AD.
I was working on building an automated test suite using Cypress for a client project we were building. The client was already using Microsoft’s Azure AD for other things and naturally wanted to tie the new app into their ecosystem with SSO. Microsoft’s documentation was pretty good and one of our devs was able to get it up and running without too much problem. Now it was on me to make sure I could still run my tests.
In past projects, I have made use of the third party API to just use a cy.request() to bypass the actual logging in. After all, I wasn’t here to test Google or Facebook’s dev teams. I started looking into how to use Microsoft’s Azure AD SSO for our project only to discover that the way our app was set up, there was no way to sign in bypassing the dialog.
This was less than ideal, but not a deal-breaker. This is what Cypress is good at. I started writing a before hook to get us logged into the app to be able to run the rest of the tests when I ran into a massive roadblock: The dialog I needed to fill out with the username and password was a pop-up, not a modal.
The way Cypress works it is impossible to access a popup. This is because Cypress can only see the DOM of the current document in the window it is testing. It does fine with modals because they already exist in the DOM, whereas popups do not. This proved to be quite a challenge.
I googled and stackoverflowed trying tons of different tips, methods, and ideas but nothing worked. Until I finally found this thread in Cypress’ GitHub repo talking about using Puppeteer to work around it. What was this magical thing called Puppeteer? And could I modify Peiter’s gist to work for my use case? (spoilers: A cool tool from Google, and yes)
Puppeteer is an npm package that allows you to control a headless instance of Chrome. It actually has some of the same functionality as Cypress in regards to clicking and filling out fields and so on with, one important difference: Puppeteer can handle popup windows. So I took the code from the gist and looked to see what I could do with it.
One thing to call out: you may notice that we are making pretty heavy use of async/await here. It’s an awesome feature of ES6 that I strongly advise you learn and use if you aren’t already familiar with it.
The first thing I had to do was modify it a little to handle the different flow for our project:
Instead of multiple parameters, I passed in one object with the properties that I needed. (line 4). For our app we are using email to log in so after the Azure dialog pops up, I grab the input[type=email] but you can just grab whatever makes sense for your flow. On line 33, we use a Puppeteer method called page.content()to grab the contents of the page and put it in a variable.
The if block that starts at line 35 is to handle the fact that sometimes there is an additional confirmation screen with the Azure AD login and sometimes there isn’t. Once all that is done, we go back to our app on line 42. Where, if all went well, we can use puppeteer’s evaluate method to grab whatever data you need from localStorage. Yup, all these gymnastics were just to get a couple of values from local storage so that we can pass them to Cypress.
How you get these values into your tests is up to you, but to get your puppeteer function into Cypress proper is going to require using cy.task(). This is a method that allows you to run node inside Cypress. Essentially, you place a call to your node function inside a callback in your /cypress/plugins/index.js file.
Once that is set up, using your puppeteer function is as simple as calling cy.task(). In this project, I put it in a method on my base page object (the Page Object Model is a topic for another blog post), but you could just call it directly in a before() or beforeEach() clause depending on how your project is set up.
When you do use cy.task(), you’ll call it passing the name of the node function you want as a string for the first argument and an object containing the arguments your function expects as the second argument.
Either way, the likely best place to get the credentials into Cypress.env() is from the /cypress/support/index.js file. But of course, your results may be different depending on how your project is setup.
I used puppeteer to get around the popup for an Azure AD login flow, but something similar can be used anytime a third-party component requires a popup or some other process that Cypress is not set up to handle natively. I hope this was useful and be sure to let us know how you used Cypress and Puppeteer together in your project.