cypress icon indicating copy to clipboard operation
cypress copied to clipboard

Electron browser hangs/errors on apps with window.onbeforeunload alerts

Open chheller opened this issue 6 years ago • 45 comments

Current behavior:

When running a test using the Electron browser on a webpage that contains a window.onbeforeunload script and trying to navigate away, Cypress will fail to load the next page. The test will eventually fail after a PageLoadTimeout exception. This occurs when using any kind of navigation such as cy.reload(), cy.visit(), or interacting with an element that contains an href linking to somewhere else.

Test run with Electron 59:

Desired behavior:

Test run with Chrome 67:

Steps to reproduce:

describe('Electron 59 alert prompt bug', function() {
  it('Should navigate away from the page', function() {
    cy.on('window:confirm', function() {
      return true;
    });
    cy.visit('http://localhost:8080/');
    cy.visit('http://localhost:8080/form');
  });
  it('Should reload the page', function() {
    cy.visit('http://localhost:8080/');
    cy.reload();
  });
});

The page under test needs to contain a window.onbeforeunload function which alters the returnValue. To reproduce, I use http-server to serve a barebones HTML file

<!DOCTYPE html>

<script>
  window.onbeforeunload = function (e) {
    e.returnValue = 'Dialog';
    return;
  }
</script>

Versions

Cypress: 3.0.2 MacOS: High Sierra 10.13.5 Browsers: Chrome 67, Electron 59

chheller avatar Jul 10 '18 15:07 chheller

I've just hit this exact same problem. For now I've wrapped my onBeforeUnload binding in a check for cypress which feels a bit dirty.

 if (!window.Cypress) {
    $window.onbeforeunload = function (e) { ... }
}

marmite22 avatar Aug 21 '18 13:08 marmite22

I was able to work around the issue with the following code in the test:

cy.window().then((win) =>  {
	win.onbeforeunload = null;
})
cy.visit(<the next page goes here>)

bimimicah avatar Oct 04 '18 20:10 bimimicah

Reproducible example: https://github.com/cypress-io/cypress-test-tiny/pull/27

jennifer-shehane avatar Nov 10 '18 00:11 jennifer-shehane

Just chiming in here to say we have an issue within our tests because Cypress doesn't properly handle the window.onbeforeunload event in Electron w/ Chromium 59, but works perfectly fine using Chrome 70 running the test runner. In our Jenkins env using docker, it also experiences the same issue with the cypress/base:10 image.

My guess is that the version of Chromium used by Electron (59) that ships with Cypress currently (3.1.0) has some major bugs and issues. I see #2559 is acknowledged, any ETA when that might get rolled out? I imagine this would fix a swath of issues being worked around due to an old version of Chromium.

Ya'll doin' great work though, I really appreciate Cypress :)

Cypress: 3.1.0 macOS: 10.14.1 Browsers: Electron 59, Chrome 70

brandonb927 avatar Nov 14 '18 17:11 brandonb927

Started encountering this problem in our CI build. For now going to workaround by monkey patching window.addEventListener:

Cypress.on('window:load', function(window) {
    const original = window.addEventListener;
    window.addEventListener = function() {
        if (arguments && arguments[0] === 'beforeunload') {
            return;
        }
        return original.apply(this, arguments);
    };
});

Obi-Dann avatar Dec 07 '18 00:12 Obi-Dann

If you're using window.onbeforeunload=function() {...} instead of window.addEventListener('beforeunload', function() {...}, you can use this workaround:

Cypress.on('window:load', function(window) {
  Object.defineProperty(window, 'onbeforeunload', {
    get: function() {},
    set: function() {}
  });
});

Fonger avatar Mar 16 '19 14:03 Fonger

Regarding dannsam's approach, you may want to change 'window:load' to 'window:before:load' if you are adding the beforeunload listener in some code executed on load.

fritz-c avatar May 23 '19 07:05 fritz-c

Thank you @dannsam and @fritz-c , your workaround did it for me. I have encountered this issue using the autosave plugin for CKEditor 5, since it tries to make sure the last call was completed or something.

szymach avatar Jun 20 '19 17:06 szymach

I have pushed another example https://github.com/cypress-io/cypress-test-tiny/tree/test-unload that shows the problem in Electron 61 (Chrome passes, despite showing an error). The website in question is http://webdriverjsdemo.github.io/leave/

and has the following script

window.onbeforeunload = function(){
  return 'Are you sure you want to leave?';
};

The error displayed in the browser console:

Screen Shot 2019-07-06 at 6 05 20 PM

This error has docs in https://www.chromestatus.com/feature/5082396709879808 and probably is not disabled in Electron

Funnily, you cannot close the Electron browser in GUI mode - it just keeps showing the error

Screen Shot 2019-07-06 at 6 05 38 PM

Running cypress run --headed shows the error that happens on CI - the test keeps running

Screen Shot 2019-07-06 at 6 08 12 PM

bahmutov avatar Jul 06 '19 22:07 bahmutov

There is also a situation where, where after visiting the site above, this will exhibit as a Page Load timeout.

Failing test code below:

describe('page', () => {
  it('works', () => {
    cy.visit('http://webdriverjsdemo.github.io/leave/')
    cy.contains('Go Home')
      .click()
  })

  it('works 2', () => {
    cy.visit('http://webdriverjsdemo.github.io/leave/')
      .contains('Go Home')
      .click()
  })
})
Screen Shot 2019-11-04 at 5 03 12 PM Screen Shot 2019-11-04 at 5 00 17 PM

Electron will not display this error in the Console when this is happening also (if you have not manually interacted with the page)

[Intervention] Blocked attempt to show a 'beforeunload' confirmation panel for a frame that never had a user gesture since its load. https://www.chromestatus.com/feature/5082396709879808

Workaround

There are 2 possible workarounds.

  1. Run your tests in Chrome using the --chrome flag during cypress run or choosing Chrome during cypress open.

OR

  1. Add the code below to your support/index.js file. This will prevent the prompts from occurring - and not hang Electron.
Cypress.on('window:before:load', function (win) {
  const original = win.EventTarget.prototype.addEventListener

  win.EventTarget.prototype.addEventListener = function () {
    if (arguments && arguments[0] === 'beforeunload') {
      return
    }
    return original.apply(this, arguments)
  }

  Object.defineProperty(win, 'onbeforeunload', {
    get: function () { },
    set: function () { }
  })
})

jennifer-shehane avatar Nov 04 '19 22:11 jennifer-shehane

The code for this is done in cypress-io/cypress#5603, but has yet to be released. We'll update this issue and reference the changelog when it's released.

cypress-bot[bot] avatar Nov 07 '19 22:11 cypress-bot[bot]

Released in 3.6.1.

cypress-bot[bot] avatar Nov 08 '19 23:11 cypress-bot[bot]

Is this confirmed fix? Still experiencing this after upgrading to 3.6.1.

hvuong-sigma avatar Nov 22 '19 20:11 hvuong-sigma

@hvuong-sigma Yes, please open a new issue with a reproducible example.

jennifer-shehane avatar Dec 24 '19 08:12 jennifer-shehane

I am reopening this issue. While this was partially addressed in https://github.com/cypress-io/cypress/pull/5603 in 3.6.1 release by fixing the hanging while running in Electron (aka - the process would never exit), this did not actually fix the error portion of the issue.

Problem

Any application that has a defined onbeforeunload handler that triggers an alert causes Cypress page load to time out within the Electron browser (not Chrome). Typically this is only noticed in CI since Electron is the chosen browser by default. Resulting in the error below:

Screen Shot 2020-01-30 at 12 20 23 PM

To reproduce

Example 1

index.html

<html>
<body>
  hello world
</body>
<script>
  window.onbeforeunload = () => confirm("test");
</script>
</html>

spec.js

it('should accept the confirm and successfully reload', function () {
  cy.on('window:confirm', () => true) // this is unnecessary as it is the default behavior
  cy.visit(`index.html`)
  cy.reload()
})

Example 2

it('works', () => {
  cy.visit('http://webdriverjsdemo.github.io/leave/')
  cy.contains('Go Home')
    .click()
})

Workaround

There are 2 possible workarounds.

  1. Run your tests in Chrome using the --browser flag during cypress run or choosing Chrome during cypress open.

OR

  1. Add the code below to your support/index.js file. This will prevent the prompts from occurring - and not hang Electron.
Cypress.on('window:before:load', function (win) {
  const original = win.EventTarget.prototype.addEventListener

  win.EventTarget.prototype.addEventListener = function () {
    if (arguments && arguments[0] === 'beforeunload') {
      return
    }
    return original.apply(this, arguments)
  }

  Object.defineProperty(win, 'onbeforeunload', {
    get: function () { },
    set: function () { }
  })
})

jennifer-shehane avatar Jan 30 '20 05:01 jennifer-shehane

The window:before:load solution worked for me.

I had an aha moment when I realized that the project that I work on shows a popup to confirm that the user will release her lock on that piece of content.

juampynr avatar May 13 '20 13:05 juampynr

This is not fixed as part of upcoming Electron 9 upgrade.

jennifer-shehane avatar Jun 29 '20 07:06 jennifer-shehane

Hey I is anyone else still seeing this issue when using a custom protocol? Like this zoommtg://us04web.zoom.us/join?action=join&confno=...

I am getting this error

image

Russ93 avatar Sep 12 '20 04:09 Russ93

Looks like a different issue to me, @Russ93 — consider opening a new issue ☺️

valscion avatar Sep 14 '20 06:09 valscion

Still the same issue occurs. Accessing an URL and throws one pop-up since one file couldn't be loaded. After that, even after closing the pop-up the tool shows 'Loading' progress and keeps waiting for the page to load and throws the below error. image

kbaala88 avatar Nov 17 '20 11:11 kbaala88

Yes, this hasn't been fixed yet. See https://github.com/cypress-io/cypress/issues/2118#issuecomment-580095512 where there exists a workaround.

valscion avatar Nov 17 '20 11:11 valscion

Thanks for the quick response. Tried the below workaround, however still not working.

Cypress.on('window:before:load', function (win) {
  const original = win.EventTarget.prototype.addEventListener

  win.EventTarget.prototype.addEventListener = function () {
    if (arguments && arguments[0] === 'beforeunload') {
      return
    }
    return original.apply(this, arguments)
  }

  Object.defineProperty(win, 'onbeforeunload', {
    get: function () { },
    set: function () { }
  })
})

kbaala88 avatar Nov 17 '20 12:11 kbaala88

Please suggest for any other workarounds and I shall try it!! Thanks in advance!!!

kbaala88 avatar Nov 17 '20 12:11 kbaala88

Team any idea if this will get fixed any time soon? This blocker happens in chrome browser too (Both headless & normal).

kbaala88 avatar Jan 29 '21 18:01 kbaala88

Confirmed the behavior is problematic IF there are some user interaction, see reproduction in https://github.com/cypress-io/cypress-test-tiny/tree/onbeforeunload

This is similar to what I have observed in https://github.com/cypress-io/cypress/issues/2118#issuecomment-508957157

Spec

/// <reference types="cypress" />
describe('Test window.confirm', () => {
  it('should accept the confirm and successfully reload', function () {
    cy.visit('/')
      .wait(1000)
    cy.reload()
      .wait(1000)
    cy.reload()
      .wait(1000)
    cy.wait(1000) // let the video finish
  });
});

App

window.onbeforeunload = function (e) {
  console.log('app window.onbeforeunload')
  e.returnValue = 'Dialog';
  return;
}

In Electron (note no confirm dialog, silently times out)

https://user-images.githubusercontent.com/2212006/107830600-347ca400-6d5a-11eb-8810-e4ea5fcf5fcf.mp4

In Chrome 90 the confirmation dialog pops up

https://user-images.githubusercontent.com/2212006/107830618-3d6d7580-6d5a-11eb-92b5-9c65468607e0.mp4

If I do not touch the confirm dialog, load times out

Screen Shot 2021-02-12 at 5 41 52 PM

Interestingly, from the DevTools console removing these listeners does not seem to remove the app's listener

getEventListeners(window).beforeunload
  .forEach(({ type, listener, useCapture }) => {
    console.log('removing', type, listener, useCapture)
    window.removeEventListener(type, listener, useCapture)
  })

Second interesting point is that I could not make it hang in cypress run mode running with Electron or Chrome

npx cypress run --browser chrome
npx cypress run --browser chrome --headless

bahmutov avatar Feb 12 '21 22:02 bahmutov

Explanation and workarounds

I looked at this problem again, and it is due to the browser trying to show the confirmation popup. I describe the problem and my workarounds in the blog post https://glebbahmutov.com/blog/onbeforeunload/ with all code in https://github.com/bahmutov/onbeforeunload-example

bahmutov avatar Feb 13 '21 21:02 bahmutov

bahmutov - Thanks for the updates. Still, there is no luck and every time the single particular page loads(Chrome/Electron) , cypress couldn't proceed and fails consistently with the details shown in snapshot. Unfortunately this occurs in very second page , hence blocking the entire flow. Any help, Thanks!

Capture

kbaala88 avatar Feb 14 '21 19:02 kbaala88

Can you share the code and the tests please? Without a reproducible example there is not much we can do

bahmutov avatar Feb 14 '21 19:02 bahmutov

@bahmutov : Unfortunately couldn't share the test or app code. For this issue, got one cypress support call scheduled upcoming week. But the error behavior is very consistent and the application couldnt cross the "2nd Page" of the flow and is a blocker. Seeing this error in console, 'This event initially fires when your application fires its 'beforeunload' event and completes when your application fires its 'load' event after the next page loads.'

Is there a way to skip the test step and proceed to the next one or forcefully load the page and move on? Thanks in advance!!!

kbaala88 avatar Feb 17 '21 12:02 kbaala88

I am having the same issue with Circle CI and locally. Any progress?

melibe23 avatar Apr 15 '21 10:04 melibe23