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

Calling puppeteer from cypress #2427

Closed
gregorybleiker opened this issue Sep 3, 2018 · 29 comments
Closed

Calling puppeteer from cypress #2427

gregorybleiker opened this issue Sep 3, 2018 · 29 comments

Comments

@gregorybleiker
Copy link

I'm trying to call puppeteer from cypress to do some login stuff

Current behavior:

There is an exception before any test starts
image

Desired behavior:

be able to use puppeteer in setup code

Steps to reproduce:

include puppeteer in your test project and create a test spec and include

const p = require('puppeteer')

Versions

Cypress 3.1, win10

@brian-mann
Copy link
Member

This isn't a bug.

Puppeteer is a node module that cannot be run in the browser. Move this to your pluginsFile and then use cy.task to call into puppeteer.

@wralitera
Copy link

Hi, What can be the usage of puppeteer into cypress? Puppeteer is a test framework and cypress is an other too.

@debbyca
Copy link

debbyca commented Apr 24, 2019

Have you successfully made it work? I couldn't launch Chrome.

Error: Failed to launch chrome!

TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

at onClose (/Users/debby/node_modules/puppeteer/lib/Launcher.js:342:14)
at Interface.helper.addEventListener (/Users/debby/node_modules/puppeteer/lib/Launcher.js:331:50)
at emitNone (events.js:110:20)
at Interface.emit (events.js:207:7)
at Interface.close (readline.js:366:8)
at Socket.onend (readline.js:146:10)
at emitNone (events.js:110:20)
at Socket.emit (events.js:207:7)
at endReadableNT (_stream_readable.js:1045:12)
at _combinedTickCallback (internal/process/next_tick.js:138:11)
at process._tickCallback (internal/process/next_tick.js:180:9)

@gregorybleiker
Copy link
Author

gregorybleiker commented Apr 24, 2019

@debbyca I launched puppeteer from chrome... however with mixed results... not sure the code is still working. Also this is only an example for asp.net. Might help you as a starting point at least. The puppeteer code for the login page is custom of course. You might also want to check out https://gauge.org/ which takes a bit a different approach.

 on("task", {
    doLogin: (args) => {
      return (async () => {
        expect.setDefaultOptions({ timeout: 10000 });
        let cookie = process.env.ASPCOOKIE;
        if (!cookie) {
          const browser = await puppeteer.launch({ headless: true })
          const page = await browser.newPage()

          await page.goto('https://<your site with login')
          await page.click('a.btn.btn-link')
          await page.waitForNavigation();
          await page.waitFor('[name="loginfmt"]');
          await expect(page).toFill('[name="loginfmt"]', args.username);
          await expect(page).toClick('input.btn-primary')
          //wait for the redirect
          await page.waitForNavigation();
          await page.waitFor('input#passwordInput');
          await expect(page).toFill('input#passwordInput', args.password);
          await expect(page).toClick('#submitButton');
          //wait for the redirect
          await page.waitForNavigation();
          //the stay signed in button
          await page.waitFor('input.btn-primary');
          await expect(page).toClick('input.btn-primary');
          await page.waitForNavigation();
          //cookie consent
          await page.waitFor('button.navbar-btn');
          await expect(page).toClick('button.navbar-btn');

          let cookies = await page.cookies();
          cookie = cookies.find(o => o.name === '.AspNetCore.Cookies')
          process.env.ASPCOOKIE = JSON.stringify(cookie);
        }
        else {
          cookie = JSON.parse(cookie)
        }
        return cookie;
      })();
    }
  });    

and in the cypress test

describe('Homepage', () => {

  beforeEach(() => {
    cy.task("doLogin", { username: Cypress.env("username"), password: Cypress.env("password") }).then(cookie => {
      console.log(cookie.domain)
      cy.setCookie(cookie.name, cookie.value,
        { domain: cookie.domain, secure: true, sameSite: 'Lax' });
      cy.setCookie(cookie.name, cookie.value,
        { domain: '<your domain>' });
    });
  });

@debbyca
Copy link

debbyca commented Apr 24, 2019

@gregorybleiker Thanks a lot! I made it work after following your example.

@blackorion
Copy link

blackorion commented Jun 25, 2019

Is there a way to connect to the same browser instance that cypress uses from puppeteer?

The only way I found to link these together is to add a remote port for a browser launch to cypress plugins file:

on('before:browser:launch', (browser = {}, args) => {
  if ( browser.name === 'chrome' || browser.name === 'chromium' )
    args.push('--remote-debugging-port=9222');
  ...

then in a task you can use puppeteer.connect({ browserURL: 'http://localhost....' })

but I'm not sure that's the best way to manage these

@peterfiorito
Copy link

peterfiorito commented Jun 29, 2019

@blackorion yes, I did something similar. The only "issue" would be running cypress on CI, as you would need to run chrome headless, something that cypress doesn't support yet.

In case anyone reaches this looking for a way to do this:
On plugins/index.js

  on('before:browser:launch', (browser = {}, args) => {
    if (browser.name === 'chrome' || browser.name === 'chromium') {
      args.push('--remote-debugging-port=9222');
    }
    return args;
  });
  on('task', {
    doSomething: args => (async () => {
      const browser = await puppeteer.connect({
      browserURL: 'http://localhost:9222',
  });

Then of course in your spec you would call at some point: cy.task('doSomething').then(() => {}

Alternatives are needed regarding running cypress in CI, but the options appear to be slim: Chrome doesn't support running extensions (cypress) when headless and cypress doesn't support headless chrome yet.
You'll probably need to look at a docker container that has what you need: https://github.com/cypress-io/cypress-docker-images.

Anyway, my 2 cts for anyone passing by.

@Tormenta74
Copy link

Tormenta74 commented Jul 16, 2019

@peterfiorito I have been trying your approach without much success. In my app there are some components that do not react as expected when clicked with the normal Cypress mechanism, so I tried to integrate Puppeteer for those particular interactions. However, when I invoke the task, it seems as if the contents of my page are not available to Puppeteer: I can do things like

const browser = await puppeteer.connect({browserURL: "http://localhost:9222"});
const pages = await browser.pages();
const page = pages[0];
await page.screenshot({path: 'screenshot.png'});
const contents = await page.content();
await page.waitForSelector("div.container");

where div.container is part of the Cypress interface, but the custom elements of my application are nowhere to be seen, even though the screenshots I can produce with page.screenshot show that the page is rendering correctly.

@peterfiorito
Copy link

@Tormenta74 I assume that you already found the workaround, sorry for the delay but this is my personal github and I use another account for my work.
You did nail the first part of it, which is, you need to control the first browser page, which is the cypress tab where the test is running, but you are missing the second bit.
If you check the instance of chrome that is used by cypress, you will find that it is basically 2 iframes. One holds the cypress panel/interface and the other one is your actual project/website where you want to be doing the puppeteer commands.
It should look something like so:

      const elementHandle = await page.$(
        'iframe[src="https://localwebsite.com/index"]',
      );
      const frame = await elementHandle.contentFrame();
     // now target stuff inside the frame (which is actually your page)
    // target a selector inside your page that you want to do something with
      await frame.$("selector");

Hope that makes sense.

@Tormenta74
Copy link

@peterfiorito I totally missed the iframes when I was inspecting the cypress page. That clue was it for me, thanks a lot!

@blackorion
Copy link

@peterfiorito it seems like OOTB solution is coming soon. Including headless Chrome according to the notes

@JayeshRGujar
Copy link

JayeshRGujar commented Sep 17, 2019

@peterfiorito Is there any sample working repo for calling puppeteer form cypress.

@peterfiorito
Copy link

@JayeshRGujar I did this while working for a company, so the repo where the full working example lives is private. I did outline how to do the whole setup in the comments. If you have any particular questions just ask, I am sure that we can figure it out.
I can try to make a working repo as demo, but to be honest it would take more time to get the setup going that what it actually takes to make the connection between cypress and puppeteer.
As a side note, I've used this for all of the out of window flows they had, from paypal payments to facebook logins/registrations (using cypress + puppeteer + facebook developer API) with very good results. No flakiness and consistent test runs.

@jhernandezinzunzaTechtonic

@JayeshRGujar I did this while working for a company, so the repo where the full working example lives is private. I did outline how to do the whole setup in the comments. If you have any particular questions just ask, I am sure that we can figure it out.
I can try to make a working repo as demo, but to be honest it would take more time to get the setup going that what it actually takes to make the connection between cypress and puppeteer.
As a side note, I've used this for all of the out of window flows they had, from paypal payments to facebook logins/registrations (using cypress + puppeteer + facebook developer API) with very good results. No flakiness and consistent test runs.

I would love to see some examples of this!

@borecz
Copy link

borecz commented Dec 20, 2019

@JayeshRGujar I did this while working for a company, so the repo where the full working example lives is private. I did outline how to do the whole setup in the comments. If you have any particular questions just ask, I am sure that we can figure it out.
I can try to make a working repo as demo, but to be honest it would take more time to get the setup going that what it actually takes to make the connection between cypress and puppeteer.
As a side note, I've used this for all of the out of window flows they had, from paypal payments to facebook logins/registrations (using cypress + puppeteer + facebook developer API) with very good results. No flakiness and consistent test runs.

I would love to see some examples of this!

Yep, me too!

@peterfiorito
Copy link

@borecz I updated the quick repo you shared with me so that it connects to the running cypress instance.
I don't know if the project is public (it seemed to be only a quick example repo), but maybe you can share it here (as required by @JayeshRGujar and @jhernandezinzunzaTechtonic). To be honest, it doesn't have anything special or different to what I described in the earlier posts, everything needed to hook up both tools is here; the rest is more about reading the docs and working with the specifics of the project you are testing.

Cypress > 3.5

For anyone getting here, this thread is from 2018 and the solution as posted was BEFORE 3.5. So bear in mind that a couple of months, let alone a year is a long time in tech/dev and a lot of releases will probably happen.
The new versions of cypress require a slightly different approach, as you won't be able to change the --remote-debugging-port.
For cypress > 3.5 you need to get the port of the created instance and use whatever cypress has set to it, something like this would work:
Quick example:
cypress/plugins/index.js

    const { myService, setDebuggingPortMyService } = require('./myService');
    if (browser.name === 'chrome' || browser.name === 'chromium') {
      const existing = args.find(
        (arg) => arg.slice(0, 23) === '--remote-debugging-port',
      );
     // Here you will need to persist the port to your plugins, whatever they may be
     setDebuggingPortMyService(existing.split('='));
    return args;
    }
    on('task', { MyService });

myService.js

const puppeteer = require('puppeteer');

module.exports = {
  debuggingPort: '',
  setDebuggingPortMyService(port) {
    [, debuggingPort] = port;
    return null;
  },
  async myService({ currentPage }) {
    const browser = await puppeteer.connect({
      browserURL: `http://localhost:${debuggingPort}`,
    });
  }
}

For now, this approach works, but as the project evolves it's very likely you will need to make updates to it.
That's kind of it. For the rest, you just will need to read through the docs of both cypress and puppeteer and get creative regarding the most efficient way of testing you particular project.

@ilianabe
Copy link

ilianabe commented Apr 2, 2020

@peterfiorito
Hi, I followed the instructions above and now I am able to log in to my 3rd party provider by setting up the cookie on page.
The only big problem is that I am logged in on puppeteer browser but my main test runs on cypress browser. So when the task ends, I can't move forward to my app as a logged user.

What you mention above is for connecting the two browsers? I couldn't follow up and understand what you're doing.
Thanks a lot.

@peterfiorito
Copy link

@ilianabe I would say that is very specific to your test/use case.
What is described here in general is just about hooking puppeteer into a running cypress instance. What you do with it in each phase is really up to you and what you are testing.
In general, when I did this, was to run something specific for which I needed control of other windows/popups/3rd parties. For eg. login in to my app via a facebook account.
So, cypress would navigate to my login page, fill in some details or something and then click on the facebook login button. This will trigger a popup, that is how the facebook api works, so I would need something that gave me control of the whole browser instance and not just a window of it. I would fire a cy.task inside that test run and puppeteer would take over, actually click the button an generate a reference to the popup so that I could also control it, start filling everything needed, etc.
Once the popup registration was done, the cy.task would return, effectively finishing the task, and cypress would be in control again.

Hope that makes sense.

@peterfiorito
Copy link

peterfiorito commented Apr 5, 2020

btw, since I was pulled back to this issue again, there is a minor adjustment needed if you upgrade to cypress @4.2.0, as per the new docs

Cypress v4.2.0

  on('before:browser:launch', (browser = { headless: true }, launchOptions) => {
    if (browser.name !== 'electron' || browser.name === 'chromium') {
      const existing = launchOptions.args.find(
        (arg) => arg.slice(0, 23) === '--remote-debugging-port',
      );
      // Here you will need to persist the port to your plugins, whatever they may be
     setDebuggingPortMyService(existing.split('='));
    }
    return launchOptions;
  });

Mainly, just that now we have launchOptions as a param, and you will need to call args.find from it and return the launchOptions instead of args as we used to do.
Other than that, still working as expected, and overall a painless upgrade.

@gregorybleiker
Copy link
Author

I have been thinking about this problem lately. Maybe it would be better to try and mock all the calls to the auth service (if you're using oidc for instance everything that is specified in well-known/openid-configuration), return a token in the stubs that is signed by a testing certificate and make sure that this testing certificate is also returned with a stub. This should allow you to run the tests without calls to an actual auth provider (which is the whole reason for running pupeteer).
What do others here think about this? Would anybody be interested in this code if I actually take the time to try this out?

@borecz
Copy link

borecz commented Apr 28, 2020

So here I have my solution but it feels incomplete, so any help in the matter will be appreciated.
Instead of Puppeteer the experiment is with PlayWright (but nothing changes)

index.js

const { playwright } = require('../config/playwright')

let debuggingPort

  on('before:browser:launch', (browser = {}, launchOptions) => {
    if (browser.family === 'chromium' && browser.name !== 'electron') {
      // auto open devtools
      launchOptions.args.push('--auto-open-devtools-for-tabs')
      const existing = launchOptions.args.find(
        arg => arg.slice(0, 23) === '--remote-debugging-port',
      )
      debuggingPort = existing.split('=')[1]
    }
})

  on('task', {
    async play() {
      console.log('Debugging port is: ' + debuggingPort)
      return await playwright(debuggingPort)
    },
  })

playwright.js

const { chromium, firefox, webkit, devices } = require('playwright')

exports.playwright = async function playwright(debuggingPort) {
  const browser = await chromium.launch({
    headless: false,
    devtools: true,
    args: [`--remote-debugging-port=${debuggingPort}`],
  })
  const page = await browser.newPage()
  await page.goto('http://aboutyou.de/' + debuggingPort)
  await await page.waitFor(10000)
  // other actions...
  await browser.close()
  return null
}

.spec

  it('Playwright at work', () => {
    cy.log('This should be after split... in the spec file!' + Cypress.env().test)
    cy.log('Attempt to change' + Cypress.env('whichport'))
    cy.task('play')
  })

The question here is I see another browser instance spinning off, is it expected behaviour?
I was thinking if I login using this instance with PlayWright, then I will not be automatically logged in well on Cypress instance...

@peterfiorito

execution here: https://share.getcloudapp.com/BluZQDX0

@peterfiorito
Copy link

@borecz yeah that is not what we aimed at all in this thread/issue.
The aim here is to have a running instance of cypress, connect puppeteer to it only when you need a task that goes out of window.
If you set it up as the instructions in the comments you would get that, but of course, that is for puppeteer and cypress, no idea about playwright as I am not familiar with it.

@peterfiorito
Copy link

I have been thinking about this problem lately. Maybe it would be better to try and mock all the calls to the auth service (if you're using oidc for instance everything that is specified in well-known/openid-configuration), return a token in the stubs that is signed by a testing certificate and make sure that this testing certificate is also returned with a stub. This should allow you to run the tests without calls to an actual auth provider (which is the whole reason for running pupeteer).
What do others here think about this? Would anybody be interested in this code if I actually take the time to try this out?

It depends on the requirements and how much flexibility you get around them. In my case, this was done for a company and they had specific requirements about how this was to be done... I did suggest workarounds, but they didn't really cover what the company needed at the time.

@taylorjdawson
Copy link

@peterfiorito Were you able to get this running on CI?

@peterfiorito
Copy link

@peterfiorito Were you able to get this running on CI?

Yep, we had it running on a gocd pipeline

@gmkumar08
Copy link

@peterfiorito Bootstrap-like alerts are not being displayed when I run my tests in Cypress with multiple 'it' blocks but when I copy all the code in a single 'it' block, the alerts are displayed just fine. I wrote a sample script in NightWatch.js to see if the problem exists in other frameworks but it works fine. So, my best guess is it has something to do with the way cookies are handled by Cypress. Can I use this approach to let puppeteer take care of login and cookie handling entirely instead of Cypress? Any thoughts/suggestions would be greatly appreciated.

Note: I tried all approaches to resolve this issue like using preserveOnce and whitelisting the entire cookie. I even tried cypress-localstorage-commands to see if saving and restoring the local storage would resolve this issue but no luck.

@peterfiorito
Copy link

@gmkumar08 I guess you could, but it seems like a lot of work for just getting the cookies to work as expected.
If you say that the test is working when it's a single case, then why not set up the tests in a way that the cookie needed is available?
I understand it may not be doable, but anyway... to sum it up: you can do something like what you are mentioning, it just seems like overkill.

@swatidhanu1985
Copy link

Hi @peterfiorito I read the whole thread as I am doing exact same thing, Trying to configure Puppeteer (10.2) in Cypress(8.2). Reason is I am trying to automate authentication flow using bank id in application. Norway/ Sweden BankId loads inside our application in iframe. Cypress dont work well with these iframe for click and type event.

So trying to configure puppeteer for that iframe event handling. I managed to configure puppeteer (Thanks for your earlier info).

Now my objective is to get control of same page running in cypress when puppeteer task is invoked from cypress test.

So I see there are 3 iframes inside each other and I want to go to last iframe where there is button which I want to click.
ButI get an error as below

`const puppeteer = require('puppeteer');

module.exports = {
async loginNorgeBankId (debuggingPort) {
console.log('Hi.....in my service.....');
const width = 600; const height = 800;
const browser = await puppeteer.connect({
browserURL: http://localhost:${debuggingPort},
headless: false,
slowmo: 250,
defaultViewport: { width: width, height: height }
});

const pages = await browser.pages();
const page = pages[0];
await page.setViewport({ width: width, height: height });
await page.setUserAgent('TEST');

// fetch cypress iframe
const elementHandle = await page.$(
  'iframe[src="https://<applicatio-server-url>"]');
const cypressIframe = await elementHandle.contentFrame();

await cypressIframe.waitForSelector('iframe[name="XXX"]', { timeout: 5000 });
const iFrameContainer = await cypressIframe.$('iframe[name="XXX"]');
const frame = await iFrameContainer.contentFrame();

const child = await frame.$('iframe[title="BankID"]');
const childFrame = await child.contentFrame();

console.log('clicking button in 3rd iframe....');

await childFrame.click('.document-list__document__icon .document-list__document__icon__container img');

}
};
`
image

I put console.log but they are also not printed when this code get invoked. So not sure where exactly it is breaking.

Kindly help incase you are aware about this issue.

Thanks

@jennifer-shehane
Copy link
Member

We've released an official Puppeteer plugin, to make puppeteer calls from within Cypress: https://github.com/cypress-io/cypress/tree/develop/npm/puppeteer

Please leave any feedback here: #28410

@cypress-io cypress-io locked and limited conversation to collaborators Dec 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests