codeceptjs / CodeceptJS

Supercharged End 2 End Testing Framework for NodeJS
http://codecept.io
MIT License
4.11k stars 725 forks source link

puppeteer error "Cannot find context with specified id undefined" #914

Closed othree closed 6 years ago

othree commented 6 years ago

What are you trying to achieve?

  1. open a page
  2. click a link by click
  3. see text from new page

What do you get instead?

Got this error and test failed.

Account - basic flow: login, navigation to pages --
 test
 • I am on page "https://github.com/"
 • I click "About"
 • I see "is how people build software"
 ✖ FAILED in 3314ms

-- FAILURES:

  1) Account - basic flow: login, navigation to pages
       test :
     Protocol error (Runtime.callFunctionOn): Cannot find context with specified id undefined

  Scenario Steps:

  - I.see("is how people build software") at Test.Scenario (account-basic_test.js:10:5)
  - I.click("About") at Test.Scenario (account-basic_test.js:9:5)
  - I.amOnPage("https://github.com/") at Test.Scenario (account-basic_test.js:8:5)

  Run with --verbose flag to see NodeJS stacktrace```

But the captured failed screenshot is correct.

> Provide test source code if related

```js
  Scenario('test ', (I) => {
    I.amOnPage('https://github.com/');
    I.click('About');
    I.see('is how people build software');
  });

Details

{
  "tests": "./*_test.js",
  "timeout": 10000,
  "output": "./output",
  "helpers": {
    "puppeteer": {
      "disableScreenshots": true,
      "waitForAction": 200,
      "show": true
    }
  },
  "include": {
    "I": "./steps_file.js"
  },
  "bootstrap": false,
  "mocha": {},
  "name": "web-test-runner"
}

PS. It related to the value of waitForAction. If change it to larger. error will not happen.

More Information

This issue is related to GoogleChrome/puppeteer#1325.
And I think latest comment is correct. The problem is because the click promise resolved when click triggered. But at that moment, navigation is not complete. No context is ready for new action.

DavertMik commented 6 years ago

Is there a way to detect that navigation is complete in Puppeteer?

othree commented 6 years ago

There is a page.waitForNavigation method mentioned in GoogleChrome/puppeteer#1325 , I didn't tested yet. And it will have another problem, some click didn't trigger navigation. Or maybe codecept can expose it to outside.

reubenmiller commented 6 years ago

I've just spent a bit of time looking into this and it is slightly more complex than I thought. Though I found some useful information on the puppeter repo.

It looks like that you have to call the .click and page.waitForNavigation at the same time like so:

await Promise.all([
  page.click('a'),
  page.waitForNavigation()
]);

The reason that it needs to be called at the same time is that apparently .waitForFunction needs to see the new page trigger and the load events occur. So just seeing the page load event is not enough.

So our problem is that we don't know which links will cause a new page to load. And if the above code is used and the link does not change the url, then it will throw a timeout error.

Now we could check the button if it is link like, however that does not cater for the scenario if there are event handlers on it which cause a redirect somewhere. So here are some options.

Option 1: Auto detect url changes and wait if one is detected (a little bit hacky)

However I have been playing around with the idea with using the targetchanged and load events, and recording the url has changed and loaded state information. So the scenario would play out like this:

  1. Click a link using I.click (wait for ~500 ms or enough time for the targetchanged event to trigger).
  2. targetchanged event triggers, and sets a urlChanged flag
  3. (in click function): Wait for the urlChanged flag to be reset (also with timeout breakout)
  4. load event triggers, reset the urlChanged flag.
  5. (in click function): See that the urlChanged flag has been reset and break out of the wait loop.

Option 2: Split the I.click function into navigation and non-navigation functions

Option 3: The user must wait for the page to load themselves

Maybe using I.waitInUrl or something similar to that...

Thoughts?

othree commented 6 years ago

I am using a work around similar to option 2.

    navByClick (text, locator) {
      this.click(text, locator);
      this.wait(2);
    },

Not a good one, but works for my test.

And I think the final solution for this issue need to wait until puppeteer solves 1325. So add a method might become unnecessary in the future. Maybe it will depend on how codeceptjs decide to add a new helper method or not.

othree commented 6 years ago

I have update my work around by extend helper:

/* global codecept_helper */

'use strict';

let Helper = codecept_helper;

class MyPuppeteer extends Helper {
  async navByClick (text, locator) {
    await this.helpers['Puppeteer'].click(text, locator);

    const page = this.helpers['Puppeteer'].page;
    await Promise.race([page.waitForNavigation({waitUntil: 'networkidle0'}), this.helpers['Puppeteer'].wait(4)]);
  }
}

module.exports = MyPuppeteer;

And helper section in codecept.json:

  "helpers": {
    "Puppeteer": {
      "disableScreenshots": false,
      "waitForAction": 800,
      "show": true
    },
    "MyPuppeteer": {
      "require": "./test/my_puppeteer.js"
    }
  },

Promise.race is due to GoogleChrome/puppeteer#1936 ...

DavertMik commented 6 years ago

Should be fixed in #936

RonHD commented 6 years ago

I just ran into the same problem with Puppeteer 1.3.0 (not 0.13.0 as in some comments on #1325). I already had await page.waitForNavigation(); following the await btn.click();. The await Promise.all ... solution in @reubenmiller 's Feb. 7 comment resolved it. I haven't checked if it's necessary, but I still have the additional waitForNavigation() after it.

tomholub commented 6 years ago

I am seeing something similar on 1.3.0-next.1525301631811

(node:10626) UnhandledPromiseRejectionWarning: Error: Protocol error (Runtime.evaluate): Cannot find context with specified id undefined
    at Promise (/home/luke/git/flowcrypt-browser/test/node_modules/puppeteer/lib/Connection.js:200:56)
    at new Promise (<anonymous>)
    at CDPSession.send (/home/luke/git/flowcrypt-browser/test/node_modules/puppeteer/lib/Connection.js:199:12)
    at ExecutionContext.evaluateHandle (/home/luke/git/flowcrypt-browser/test/node_modules/puppeteer/lib/ExecutionContext.js:67:77)
    at _documentPromise._contextPromise.then (/home/luke/git/flowcrypt-browser/test/node_modules/puppeteer/lib/FrameManager.js:346:38)
    at <anonymous>

There is no navigation, just looking for elements in a loop:

for (let i = 0; i < selectors.length; i++) {
    let elements = this._is_xpath(selectors[i]) ? await page.$x(selectors[i]) : await page.$$(selectors[i]);
    // ...
}