diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index a01c72788..6e17ebd71 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -37,13 +37,14 @@ import pytest import py.path # pylint: disable=no-name-in-module import helpers.stubs as stubsmod +import helpers.utils from qutebrowser.config import config, configdata, configtypes, configexc from qutebrowser.utils import objreg, standarddir from qutebrowser.browser.webkit import cookies from qutebrowser.misc import savemanager, sql from qutebrowser.keyinput import modeman -from PyQt5.QtCore import pyqtSignal, QEvent, QSize, Qt, QObject +from PyQt5.QtCore import QEvent, QSize, Qt from PyQt5.QtGui import QKeyEvent from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout from PyQt5.QtNetwork import QNetworkCookieJar @@ -78,34 +79,9 @@ class WinRegistryHelper: del objreg.window_registry[win_id] -class CallbackChecker(QObject): - - """Check if a value provided by a callback is the expected one.""" - - got_result = pyqtSignal(object) - UNSET = object() - - def __init__(self, qtbot, parent=None): - super().__init__(parent) - self._qtbot = qtbot - self._result = self.UNSET - - def callback(self, result): - """Callback which can be passed to runJavaScript.""" - self._result = result - self.got_result.emit(result) - - def check(self, expected): - """Wait until the JS result arrived and compare it.""" - if self._result is self.UNSET: - with self._qtbot.waitSignal(self.got_result, timeout=2000): - pass - assert self._result == expected - - @pytest.fixture def callback_checker(qtbot): - return CallbackChecker(qtbot) + return helpers.utils.CallbackChecker(qtbot) class FakeStatusBar(QWidget): diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index e6e3d37c8..82c07fbd2 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -27,6 +27,8 @@ import contextlib import pytest +from PyQt5.QtCore import QObject, pyqtSignal + from qutebrowser.utils import qtutils @@ -176,3 +178,28 @@ def abs_datapath(): @contextlib.contextmanager def nop_contextmanager(): yield + + +class CallbackChecker(QObject): + + """Check if a value provided by a callback is the expected one.""" + + got_result = pyqtSignal(object) + UNSET = object() + + def __init__(self, qtbot, parent=None): + super().__init__(parent) + self._qtbot = qtbot + self._result = self.UNSET + + def callback(self, result): + """Callback which can be passed to runJavaScript.""" + self._result = result + self.got_result.emit(result) + + def check(self, expected): + """Wait until the JS result arrived and compare it.""" + if self._result is self.UNSET: + with self._qtbot.waitSignal(self.got_result, timeout=2000): + pass + assert self._result == expected diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index b9f013ecf..8490a5362 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -26,6 +26,12 @@ import logging import pytest import jinja2 +from PyQt5.QtCore import QUrl + +import helpers.utils +import qutebrowser.utils.debug +from qutebrowser.utils import utils + try: from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebPage @@ -34,7 +40,14 @@ except ImportError: QWebSettings = None QWebPage = None -from qutebrowser.utils import utils +try: + from PyQt5.QtWebEngineWidgets import (QWebEnginePage, + QWebEngineSettings, + QWebEngineScript) +except ImportError: + QWebEnginePage = None + QWebEngineSettings = None + QWebEngineScript = None if QWebPage is None: @@ -68,9 +81,96 @@ else: """Fail tests on js console messages as they're used for errors.""" pytest.fail("js console ({}:{}): {}".format(source, line, msg)) +if QWebEnginePage is None: + TestWebEnginePage = None +else: + class TestWebEnginePage(QWebEnginePage): + + """QWebEnginePage which overrides javascript logging methods. + + Attributes: + _logger: The logger used for alerts. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self._logger = logging.getLogger('js-tests') + + def javaScriptAlert(self, _frame, msg): + """Log javascript alerts.""" + self._logger.info("js alert: {}".format(msg)) + + def javaScriptConfirm(self, _frame, msg): + """Fail tests on js confirm() as that should never happen.""" + pytest.fail("js confirm: {}".format(msg)) + + def javaScriptPrompt(self, _frame, msg, _default): + """Fail tests on js prompt() as that should never happen.""" + pytest.fail("js prompt: {}".format(msg)) + + def javaScriptConsoleMessage(self, level, msg, line, source): + """Fail tests on js console messages as they're used for errors.""" + pytest.fail("[{}] js console ({}:{}): {}".format( + qutebrowser.utils.debug.qenum_key( + QWebEnginePage, level), source, line, msg)) + class JSTester: + """Common subclass providing basic functionality for all JS testers. + + Attributes: + webview: The webview which is used. + _qtbot: The QtBot fixture from pytest-qt. + _jinja_env: The jinja2 environment used to get templates. + """ + + def __init__(self, webview, qtbot): + self.webview = webview + self._qtbot = qtbot + loader = jinja2.FileSystemLoader(os.path.dirname(__file__)) + self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) + + def load(self, path, **kwargs): + """Load and display the given jinja test data. + + Args: + path: The path to the test file, relative to the javascript/ + folder. + **kwargs: Passed to jinja's template.render(). + """ + template = self._jinja_env.get_template(path) + with self._qtbot.waitSignal(self.webview.loadFinished, + timeout=2000) as blocker: + self.webview.setHtml(template.render(**kwargs)) + assert blocker.args == [True] + + def load_file(self, path: str, force: bool = False): + """Load a file from disk. + + Args: + path: The string path from disk to load (relative to this file) + force: Whether to force loading even if the file is invalid. + """ + self.load_url(QUrl.fromLocalFile( + os.path.join(os.path.dirname(__file__), path)), force) + + def load_url(self, url: QUrl, force: bool = False): + """Load a given QUrl. + + Args: + url: The QUrl to load. + force: Whether to force loading even if the file is invalid. + """ + with self._qtbot.waitSignal(self.webview.loadFinished, + timeout=2000) as blocker: + self.webview.load(url) + if not force: + assert blocker.args == [True] + + +class JSWebKitTester(JSTester): + """Object returned by js_tester which provides test data and a webview. Attributes: @@ -80,11 +180,8 @@ class JSTester: """ def __init__(self, webview, qtbot): - self.webview = webview + super().__init__(webview, qtbot) self.webview.setPage(TestWebPage(self.webview)) - self._qtbot = qtbot - loader = jinja2.FileSystemLoader(os.path.dirname(__file__)) - self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) def scroll_anchor(self, name): """Scroll the main frame to the given anchor.""" @@ -94,19 +191,6 @@ class JSTester: new_pos = page.mainFrame().scrollPosition() assert old_pos != new_pos - def load(self, path, **kwargs): - """Load and display the given test data. - - Args: - path: The path to the test file, relative to the javascript/ - folder. - **kwargs: Passed to jinja's template.render(). - """ - template = self._jinja_env.get_template(path) - with self._qtbot.waitSignal(self.webview.loadFinished) as blocker: - self.webview.setHtml(template.render(**kwargs)) - assert blocker.args == [True] - def run_file(self, filename): """Run a javascript file. @@ -134,7 +218,57 @@ class JSTester: return self.webview.page().mainFrame().evaluateJavaScript(source) +class JSWebEngineTester(JSTester): + + """Object returned by js_tester_webengine which provides a webview. + + Attributes: + webview: The webview which is used. + _qtbot: The QtBot fixture from pytest-qt. + _jinja_env: The jinja2 environment used to get templates. + """ + + def __init__(self, webview, qtbot): + super().__init__(webview, qtbot) + self.webview.setPage(TestWebEnginePage(self.webview)) + + def run_file(self, filename: str, expected) -> None: + """Run a javascript file. + + Args: + filename: The javascript filename, relative to + qutebrowser/javascript. + expected: The value expected return from the javascript execution + """ + source = utils.read_file(os.path.join('javascript', filename)) + self.run(source, expected) + + def run(self, source: str, expected, world=None) -> None: + """Run the given javascript source. + + Args: + source: The source to run as a string. + expected: The value expected return from the javascript execution + world: The scope the javascript will run in + """ + if world is None: + world = QWebEngineScript.ApplicationWorld + + callback_checker = helpers.utils.CallbackChecker(self._qtbot) + assert self.webview.settings().testAttribute( + QWebEngineSettings.JavascriptEnabled) + self.webview.page().runJavaScript(source, world, + callback_checker.callback) + callback_checker.check(expected) + + @pytest.fixture -def js_tester(webview, qtbot): - """Fixture to test javascript snippets.""" - return JSTester(webview, qtbot) +def js_tester_webkit(webview, qtbot): + """Fixture to test javascript snippets in webkit.""" + return JSWebKitTester(webview, qtbot) + + +@pytest.fixture +def js_tester_webengine(callback_checker, webengineview, qtbot): + """Fixture to test javascript snippets in webengine.""" + return JSWebEngineTester(webengineview, qtbot) diff --git a/tests/unit/javascript/position_caret/test_position_caret.py b/tests/unit/javascript/position_caret/test_position_caret.py index 7be62e3cc..fcfa5cf5d 100644 --- a/tests/unit/javascript/position_caret/test_position_caret.py +++ b/tests/unit/javascript/position_caret/test_position_caret.py @@ -65,9 +65,9 @@ class CaretTester: @pytest.fixture -def caret_tester(js_tester): +def caret_tester(js_tester_webkit): """Helper fixture to test caret browsing positions.""" - caret_tester = CaretTester(js_tester) + caret_tester = CaretTester(js_tester_webkit) # Showing webview here is necessary for test_scrolled_down_img to # succeed in some cases, see #1988 caret_tester.js.webview.show() diff --git a/tests/unit/javascript/stylesheet/green.css b/tests/unit/javascript/stylesheet/green.css new file mode 100644 index 000000000..b2d035810 --- /dev/null +++ b/tests/unit/javascript/stylesheet/green.css @@ -0,0 +1 @@ +body, :root {background-color: rgb(0, 255, 0);} diff --git a/tests/unit/javascript/stylesheet/none.css b/tests/unit/javascript/stylesheet/none.css new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/javascript/stylesheet/simple.html b/tests/unit/javascript/stylesheet/simple.html new file mode 100644 index 000000000..4073672a4 --- /dev/null +++ b/tests/unit/javascript/stylesheet/simple.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} +
Hello World!
+{% endblock %} diff --git a/tests/unit/javascript/stylesheet/simple.xml b/tests/unit/javascript/stylesheet/simple.xml new file mode 100644 index 000000000..f9073de69 --- /dev/null +++ b/tests/unit/javascript/stylesheet/simple.xml @@ -0,0 +1,5 @@ + + +Hello World!
+{% endblock %} diff --git a/tests/unit/javascript/stylesheet/test_appendchild.js b/tests/unit/javascript/stylesheet/test_appendchild.js new file mode 100644 index 000000000..d1deadba6 --- /dev/null +++ b/tests/unit/javascript/stylesheet/test_appendchild.js @@ -0,0 +1,47 @@ +// Taken from acid3 bucket 5 +// https://github.com/w3c/web-platform-tests/blob/37cf5607a39357a0f213ab5df2e6b30499b0226f/acid/acid3/test.html#L2320 + +// test 65: bring in a couple of SVG files and some HTML files dynamically - preparation for later tests in this bucket +// NOTE FROM 2011 UPDATE: The svg.xml file still contains the SVG font, but it is no longer used +kungFuDeathGrip = document.createElement('p'); +kungFuDeathGrip.className = 'removed'; +var iframe, object; +// svg iframe +iframe = document.createElement('iframe'); +iframe.onload = function () { kungFuDeathGrip.title += '1' }; +iframe.src = "svg.xml"; +kungFuDeathGrip.appendChild(iframe); +// object iframe +object = document.createElement('object'); +object.onload = function () { kungFuDeathGrip.title += '2' }; +object.data = "svg.xml"; +kungFuDeathGrip.appendChild(object); +// xml iframe +iframe = document.createElement('iframe'); +iframe.onload = function () { kungFuDeathGrip.title += '3' }; +iframe.src = "empty.xml"; +kungFuDeathGrip.appendChild(iframe); +// html iframe +iframe = document.createElement('iframe'); +iframe.onload = function () { kungFuDeathGrip.title += '4' }; +iframe.src = "empty.html"; +kungFuDeathGrip.appendChild(iframe); +// html iframe +iframe = document.createElement('iframe'); +iframe.onload = function () { kungFuDeathGrip.title += '5' }; +iframe.src = "xhtml.1"; +kungFuDeathGrip.appendChild(iframe); +// html iframe +iframe = document.createElement('iframe'); +iframe.onload = function () { kungFuDeathGrip.title += '6' }; +iframe.src = "xhtml.2"; +kungFuDeathGrip.appendChild(iframe); +// html iframe +iframe = document.createElement('iframe'); +iframe.onload = function () { kungFuDeathGrip.title += '7' }; +iframe.src = "xhtml.3"; +kungFuDeathGrip.appendChild(iframe); +// add the lot to the document + +// Modified as we don't have a 'map' +document.getElementsByTagName('head')[0].appendChild(kungFuDeathGrip); diff --git a/tests/unit/javascript/stylesheet/test_stylesheet.py b/tests/unit/javascript/stylesheet/test_stylesheet.py new file mode 100644 index 000000000..47a181d77 --- /dev/null +++ b/tests/unit/javascript/stylesheet/test_stylesheet.py @@ -0,0 +1,138 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Jay Kamat +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see