onbeforeunload not handled correctly in firefox-webdriver
Hi there,
it seems that there is code that is supposed to handle this condition, but it doesn't work for me - i.e. hangs indefinitely. Also, if the onbeforeunload handler is triggered in a different (background) window, it seems that this condition is not handled at all, but instead the window just gets a close event and doesn't close.
Here's my reproducer:
# https://github.com/elliterate/capybara.py
import capybara
from capybara.dsl import page
from conftest import find_firefox
HEADLESS = True
HEADLESS = False
@capybara.register_driver("selenium")
def init_selenium_driver(app):
from selenium.webdriver.firefox.options import Options
options = Options()
options.binary_location = find_firefox()
options.headless = HEADLESS
# otherwise marionette automatically disables beforeunload event handling
# still requires interaction to trigger
options.set_preference("dom.disable_beforeunload", False)
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
capabilities = DesiredCapabilities.FIREFOX.copy()
capabilities["marionette"] = True
from capybara.selenium.driver import Driver
return Driver(app, browser="firefox", options=options, desired_capabilities=capabilities,
# cannot set these after the fact, so we set them here
clear_local_storage=True,
clear_session_storage=True,
)
capybara.default_driver = "selenium"
capybara.default_max_wait_time = 5
def test_isolation(flask_uri, ask_to_leave_script):
page.visit(flask_uri)
# onbeforeunload dialog
# bug in capybara, ask to leave script is only handled in current window, other windows just get closed and then hang
# Even though it is handled in the code, that doesn't work for firefox. (?)
page.execute_script(ask_to_leave_script)
# page interaction, so onbeforeunload is actually triggered
page.fill_in('input_label', value='fnord')
# bug in capybara: background windows don't even have code to handle dialogs like onbeforeunload
with page.window(page.open_new_window()):
page.visit(flask_uri)
page.execute_script(ask_to_leave_script)
# page interaction, so onbeforeunload is actually triggered
page.fill_in('input_label', value='fnord')
page.reset() # hangs on the onbeforeunload handlers
assert len(page.windows) == 1
assert page.current_url == 'about:blank'
I realize I missed the conftest.py file:
import subprocess
import pytest
import re
import atexit
from subprocess import run
def find_firefox():
paths = run(['mdfind', 'kMDItemFSName == Firefox.app'], capture_output=True).stdout.splitlines()
assert len(paths) > 0
return paths[0].strip().decode() + '/Contents/MacOS/firefox'
@pytest.fixture(scope='session')
def flask_uri():
with subprocess.Popen(['flask', 'run', '--reload'], stderr=subprocess.PIPE, encoding='utf8') as process:
flask_url = None
still_starting = True
while still_starting:
output = process.stderr.readline()
match = re.match(r'^.* Running on (http://[\d\.\:]+/).*$', output)
still_starting = match is None
flask_url = match.group(1)
def kill():
process.terminate()
process.wait()
atexit.register(kill)
yield flask_url
kill()
@pytest.fixture
def ask_to_leave_script():
return '''
// interestingly Firefox webdriver doesn't show thes dialogs at all, even though this code works in normal Firefox
window.addEventListener('beforeunload', function (e) {
// Cancel the event
e.preventDefault(); // mozilla will now always show dialog
// Chrome requires returnValue to be set
e.returnValue = 'Fnord';
});
'''
# Selenium style xpath matcher
@pytest.fixture
def xpath():
class XPath:
def __getattr__(self, name):
from xpath import html
from xpath.renderer import to_xpath
def callable(*args, **kwargs):
return to_xpath(getattr(html, name)(*args, **kwargs))
return callable
return XPath()
def assert_is_png(path):
assert_is_file(path, '.png', b'PNG image data', b'8-bit/color RGBA, non-interlaced')
def assert_is_file(path, expected_suffix, *expected_file_outputs):
assert path.exists() and path.is_file()
assert path.stat().st_size > 1000
assert path.suffix == expected_suffix
import subprocess
output = subprocess.check_output(['file', path])
for expected_output in expected_file_outputs:
assert expected_output in output
from contextlib import contextmanager
from datetime import datetime
@contextmanager
def assert_no_slower_than(seconds=1):
before = datetime.now()
yield
after = datetime.now()
assert (after - before).total_seconds() < seconds
And the flask app in question:
import flask
app = flask.Flask(__name__)
@app.get('/')
def index():
return flask.redirect('/selector_playground')
@app.get("/dynamic_disclose")
def dynamic_disclosure():
return '''
<div id=container>
<div id=outer>
Container
<div id=inner>
</div>
</div>
</div>
<button onclick=trigger()>Trigger</button>
<script>
function trigger() {
div = document.querySelectorAll('#container')[0]
setTimeout(function() {
div.innerHTML = "<div id=outer><div id=inner>fnord</div></div>"
}, 1000)
}
</script>
'''
@app.get('/form')
def form():
return '''
<form>
<label for=first_name>First name:</label><input id=first_name>
<label>Last name:<input id=last_name></label>
<input id=email placeholder=your@email>
</form>
'''
@app.get('/selector_playground')
def selector_playground():
return '''
<form>
<label for=input_id id=label>input_label</label>
<input id=input_id class=input_class name=input_name value=input_value
title=input_title placeholder=input_placeholder aria-label=input_aria_label>
<div id=div_id>div_text</div>
</form>
'''
I believe the unload handling in Capybara is meant to be best-effort and not exhaustive. That is, I'm pretty sure the Ruby library makes no effort to deal with extra windows that may have been left open by one's tests.
If you really want to use and test an unload handler, you could copy the cleanup logic into your own test teardown function.
:-) Well I sure can deal with this manually - and especially here where I'm comparing how different browser automation frameworks deal with this.
Im reporting the bug cause I noticed that there was code, but it didn't work. Would you be open to a pull request that fixes this (for the current window?)
What about background windows, it seems sensible that the same logic is applied to closing them as the main window (why have a different code path) - I haven't looked into capybara yet, but do you know a reason why different logic should apply for background windows?