Merge remote-tracking branch 'origin/pr/3262'

This commit is contained in:
Florian Bruhin 2017-11-19 14:24:48 +01:00
commit 5e20aa668a
11 changed files with 391 additions and 50 deletions

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1 @@
body, :root {background-color: rgb(0, 255, 0);}

View File

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block content %}
<p>Hello World!</p>
{% endblock %}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 suve <veg@svgames.pl> -->
<component type="desktop">
<id>org.qutebrowser.qutebrowser</id>
</component>

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block style %}
body {
background-color: rgb(255, 0, 0);
}
{% endblock %}
{% block content %}
<p>Hello World!</p>
{% endblock %}

View File

@ -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);

View File

@ -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 <http://www.gnu.org/licenses/>.
"""Tests for stylesheet.js."""
import os
import pytest
QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets")
QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile
from qutebrowser.utils import javascript
try:
from qutebrowser.browser.webengine import webenginesettings
except ImportError:
webenginesettings = None
DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)"
GREEN_BODY_BG = "rgb(0, 255, 0)"
CSS_BODY_GREEN = "body {background-color: rgb(0, 255, 0);}"
CSS_BODY_RED = "body {background-color: rgb(255, 0, 0);}"
class StylesheetTester:
"""Helper class (for the stylesheet_tester fixture) for asserts.
Attributes:
js: The js_tester fixture.
config_stub: The config stub object.
"""
def __init__(self, js_tester, config_stub):
self.js = js_tester
self.config_stub = config_stub
def init_stylesheet(self, css_file="green.css"):
"""Initialize the stylesheet with a provided css file."""
css_path = os.path.join(os.path.dirname(__file__), css_file)
self.config_stub.val.content.user_stylesheets = css_path
p = QWebEngineProfile.defaultProfile()
webenginesettings._init_stylesheet(p)
def set_css(self, css):
"""Set document style to `css` via stylesheet.js."""
code = javascript.assemble('stylesheet', 'set_css', css)
self.js.run(code, None)
def check_set(self, value, css_style="background-color",
document_element="document.body"):
"""Check whether the css in ELEMENT is set to VALUE."""
self.js.run("window.getComputedStyle({}, null)"
".getPropertyValue('{}');"
.format(document_element,
javascript.string_escape(css_style)), value)
def check_eq(self, one, two, true=True):
"""Check if one and two are equal."""
self.js.run("{} === {};".format(one, two), true)
@pytest.fixture
def stylesheet_tester(js_tester_webengine, config_stub):
"""Helper fixture to test stylesheets."""
ss_tester = StylesheetTester(js_tester_webengine, config_stub)
ss_tester.js.webview.show()
return ss_tester
@pytest.mark.parametrize('page', ['stylesheet/simple.html',
'stylesheet/simple_bg_set_red.html'])
def test_set_delayed(stylesheet_tester, page):
"""Test a delayed invocation of set_css."""
stylesheet_tester.init_stylesheet("none.css")
stylesheet_tester.js.load(page)
stylesheet_tester.set_css("body {background-color: rgb(0, 255, 0);}")
stylesheet_tester.check_set("rgb(0, 255, 0)")
@pytest.mark.parametrize('page', ['stylesheet/simple.html',
'stylesheet/simple_bg_set_red.html'])
def test_set_clear_bg(stylesheet_tester, page):
"""Test setting and clearing the stylesheet."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load('stylesheet/simple.html')
stylesheet_tester.check_set(GREEN_BODY_BG)
stylesheet_tester.set_css("")
stylesheet_tester.check_set(DEFAULT_BODY_BG)
def test_set_xml(stylesheet_tester):
"""Test stylesheet is applied without altering xml files."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load_file('stylesheet/simple.xml')
stylesheet_tester.check_set(GREEN_BODY_BG)
stylesheet_tester.check_eq('"html"', "document.documentElement.nodeName")
def test_set_svg(stylesheet_tester):
"""Test stylesheet is applied for svg files."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load_file('../../../misc/cheatsheet.svg')
stylesheet_tester.check_set(GREEN_BODY_BG,
document_element="document.documentElement")
stylesheet_tester.check_eq('"svg"', "document.documentElement.nodeName")
def test_set_error(stylesheet_tester):
"""Test stylesheet modifies file not found error pages."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load_file('non-existent.html', force=True)
stylesheet_tester.check_set(GREEN_BODY_BG)
def test_appendchild(stylesheet_tester):
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load('stylesheet/simple.html')
js_test_file_path = \
'../../tests/unit/javascript/stylesheet/test_appendchild.js'
stylesheet_tester.js.run_file(js_test_file_path, {})