diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 087834e53..0eb9b6493 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -395,9 +395,11 @@ class AbstractCaret(QObject): Signals: selection_toggled: Emitted when the selection was toggled. arg: Whether the selection is now active. + follow_selected_done: Emitted when a follow_selection action is done. """ selection_toggled = pyqtSignal(bool) + follow_selected_done = pyqtSignal() def __init__(self, tab, mode_manager, parent=None): super().__init__(parent) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index cfb809097..36ac2a99a 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -21,12 +21,11 @@ import math import functools -import sys import re import html as html_utils from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, - QUrl, QTimer, QObject, qVersion) + QUrl, QTimer, QObject) from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication @@ -234,6 +233,15 @@ class WebEngineCaret(browsertab.AbstractCaret): """QtWebEngine implementations related to moving the cursor/selection.""" + def _flags(self): + """Get flags to pass to JS.""" + flags = set() + if qtutils.version_check('5.7.1', compiled=False): + flags.add('filter-prefix') + if utils.is_windows: + flags.add('windows') + return list(flags) + @pyqtSlot(usertypes.KeyMode) def _on_mode_entered(self, mode): if mode != usertypes.KeyMode.caret: @@ -246,9 +254,9 @@ class WebEngineCaret(browsertab.AbstractCaret): self._tab.search.clear() self._tab.run_js_async( - javascript.assemble('caret', - 'setPlatform', sys.platform, qVersion())) - self._js_call('setInitialCursor', self._selection_cb) + javascript.assemble('caret', 'setFlags', self._flags())) + + self._js_call('setInitialCursor', callback=self._selection_cb) def _selection_cb(self, enabled): """Emit selection_toggled based on setInitialCursor.""" @@ -266,32 +274,25 @@ class WebEngineCaret(browsertab.AbstractCaret): self._js_call('disableCaret') def move_to_next_line(self, count=1): - for _ in range(count): - self._js_call('moveDown') + self._js_call('moveDown', count) def move_to_prev_line(self, count=1): - for _ in range(count): - self._js_call('moveUp') + self._js_call('moveUp', count) def move_to_next_char(self, count=1): - for _ in range(count): - self._js_call('moveRight') + self._js_call('moveRight', count) def move_to_prev_char(self, count=1): - for _ in range(count): - self._js_call('moveLeft') + self._js_call('moveLeft', count) def move_to_end_of_word(self, count=1): - for _ in range(count): - self._js_call('moveToEndOfWord') + self._js_call('moveToEndOfWord', count) def move_to_next_word(self, count=1): - for _ in range(count): - self._js_call('moveToNextWord') + self._js_call('moveToNextWord', count) def move_to_prev_word(self, count=1): - for _ in range(count): - self._js_call('moveToPreviousWord') + self._js_call('moveToPreviousWord', count) def move_to_start_of_line(self): self._js_call('moveToStartOfLine') @@ -300,20 +301,16 @@ class WebEngineCaret(browsertab.AbstractCaret): self._js_call('moveToEndOfLine') def move_to_start_of_next_block(self, count=1): - for _ in range(count): - self._js_call('moveToStartOfNextBlock') + self._js_call('moveToStartOfNextBlock', count) def move_to_start_of_prev_block(self, count=1): - for _ in range(count): - self._js_call('moveToStartOfPrevBlock') + self._js_call('moveToStartOfPrevBlock', count) def move_to_end_of_next_block(self, count=1): - for _ in range(count): - self._js_call('moveToEndOfNextBlock') + self._js_call('moveToEndOfNextBlock', count) def move_to_end_of_prev_block(self, count=1): - for _ in range(count): - self._js_call('moveToEndOfPrevBlock') + self._js_call('moveToEndOfPrevBlock', count) def move_to_start_of_document(self): self._js_call('moveToStartOfDocument') @@ -322,7 +319,7 @@ class WebEngineCaret(browsertab.AbstractCaret): self._js_call('moveToEndOfDocument') def toggle_selection(self): - self._js_call('toggleSelection', self.selection_toggled.emit) + self._js_call('toggleSelection', callback=self.selection_toggled.emit) def drop_selection(self): self._js_call('dropSelection') @@ -335,7 +332,13 @@ class WebEngineCaret(browsertab.AbstractCaret): self._tab.run_js_async(javascript.assemble('caret', 'getSelection'), callback) - def _follow_selected_cb(self, js_elem, tab=False): + def _follow_selected_cb_wrapped(self, js_elem, tab): + try: + self._follow_selected_cb(js_elem, tab) + finally: + self.follow_selected_done.emit() + + def _follow_selected_cb(self, js_elem, tab): """Callback for javascript which clicks the selected element. Args: @@ -344,6 +347,7 @@ class WebEngineCaret(browsertab.AbstractCaret): """ if js_elem is None: return + if js_elem == "focused": # we had a focused element, not a selected one. Just send self._follow_enter(tab) @@ -364,7 +368,6 @@ class WebEngineCaret(browsertab.AbstractCaret): elem.click(click_type) except webelem.Error as e: message.error(str(e)) - return def follow_selected(self, *, tab=False): if self._tab.search.search_displayed: @@ -380,11 +383,13 @@ class WebEngineCaret(browsertab.AbstractCaret): # click an existing blue selection js_code = javascript.assemble('webelem', 'find_selected_focused_link') - self._tab.run_js_async(js_code, lambda jsret: - self._follow_selected_cb(jsret, tab)) + self._tab.run_js_async( + js_code, + lambda jsret: self._follow_selected_cb_wrapped(jsret, tab)) - def _js_call(self, command, callback=None): - self._tab.run_js_async(javascript.assemble('caret', command), callback) + def _js_call(self, command, *args, callback=None): + code = javascript.assemble('caret', command, *args) + self._tab.run_js_async(code, callback) class WebEngineScroller(browsertab.AbstractScroller): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 7b7ad0c7d..7f0740b65 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -348,7 +348,7 @@ class WebKitCaret(browsertab.AbstractCaret): def selection(self, callback): callback(self._widget.selectedText()) - def follow_selected(self, *, tab=False): + def _follow_selected(self, *, tab=False): if QWebSettings.globalSettings().testAttribute( QWebSettings.JavascriptEnabled): if tab: @@ -389,6 +389,12 @@ class WebKitCaret(browsertab.AbstractCaret): else: self._tab.openurl(url) + def follow_selected(self, *, tab=False): + try: + self._follow_selected(tab=tab) + finally: + self.follow_selected_done.emit() + class WebKitZoom(browsertab.AbstractZoom): diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index 3f858f3fb..8f3ec8d72 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -755,31 +755,6 @@ window._qutebrowser.caret = (function() { */ CaretBrowsing.isSelectionCollapsed = false; - /** - * The id returned by window.setInterval for our blink function, so - * we can cancel it when caret browsing is disabled. - * @type {number?} - */ - CaretBrowsing.blinkFunctionId = null; - - /** - * The desired x-coordinate to match when moving the caret up and down. - * To match the behavior as documented in Mozilla's caret browsing spec - * (http://www.mozilla.org/access/keyboard/proposal), we keep track of the - * initial x position when the user starts moving the caret up and down, - * so that the x position doesn't drift as you move throughout lines, but - * stays as close as possible to the initial position. This is reset when - * moving left or right or clicking. - * @type {number?} - */ - CaretBrowsing.targetX = null; - - /** - * A flag that flips on or off as the caret blinks. - * @type {boolean} - */ - CaretBrowsing.blinkFlag = true; - /** * Whether we're running on Windows. * @type {boolean} @@ -788,9 +763,17 @@ window._qutebrowser.caret = (function() { /** * Whether we're running on on old Qt 5.7.1. + * There, we need to use -webkit-filter. * @type {boolean} */ - CaretBrowsing.isOldQt = null; + CaretBrowsing.needsFilterPrefix = null; + + /** + * The id returned by window.setInterval for our stopAnimation function, so + * we can cancel it when we call stopAnimation again. + * @type {number?} + */ + CaretBrowsing.animationFunctionId = null; /** * Check if a node is a control that normally allows the user to interact @@ -868,7 +851,7 @@ window._qutebrowser.caret = (function() { }; CaretBrowsing.injectCaretStyles = function() { - const prefix = CaretBrowsing.isOldQt ? "-webkit-" : ""; + const prefix = CaretBrowsing.needsFilterPrefix ? "-webkit-" : ""; const style = ` .CaretBrowsing_Caret { position: absolute; @@ -987,7 +970,6 @@ window._qutebrowser.caret = (function() { */ CaretBrowsing.recreateCaretElement = function() { if (CaretBrowsing.caretElement) { - window.clearInterval(CaretBrowsing.blinkFunctionId); CaretBrowsing.caretElement.parentElement.removeChild( CaretBrowsing.caretElement); CaretBrowsing.caretElement = null; @@ -1163,47 +1145,47 @@ window._qutebrowser.caret = (function() { } }; - CaretBrowsing.move = function(direction, granularity) { + CaretBrowsing.move = function(direction, granularity, count = 1) { let action = "move"; if (CaretBrowsing.selectionEnabled) { action = "extend"; } - window. - getSelection(). - modify(action, direction, granularity); + + for (let i = 0; i < count; i++) { + window. + getSelection(). + modify(action, direction, granularity); + } if (CaretBrowsing.isWindows && (direction === "forward" || direction === "right") && granularity === "word") { CaretBrowsing.move("left", "character"); - } else { - window.setTimeout(() => { - CaretBrowsing.updateCaretOrSelection(true); - }, 0); } + }; + CaretBrowsing.finishMove = function() { + window.setTimeout(() => { + CaretBrowsing.updateCaretOrSelection(true); + }, 0); CaretBrowsing.stopAnimation(); }; - CaretBrowsing.moveToBlock = function(paragraph, boundary) { + CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) { let action = "move"; if (CaretBrowsing.selectionEnabled) { action = "extend"; } - window. - getSelection(). - modify(action, paragraph, "paragraph"); + for (let i = 0; i < count; i++) { + window. + getSelection(). + modify(action, paragraph, "paragraph"); - window. - getSelection(). - modify(action, boundary, "paragraphboundary"); - - window.setTimeout(() => { - CaretBrowsing.updateCaretOrSelection(true); - }, 0); - - CaretBrowsing.stopAnimation(); + window. + getSelection(). + modify(action, boundary, "paragraphboundary"); + } }; CaretBrowsing.toggle = function(value) { @@ -1232,7 +1214,6 @@ window._qutebrowser.caret = (function() { return true; } window.setTimeout(() => { - CaretBrowsing.targetX = null; CaretBrowsing.updateCaretOrSelection(false); }, 0); return true; @@ -1250,7 +1231,6 @@ window._qutebrowser.caret = (function() { CaretBrowsing.updateCaretOrSelection(true); } else if (!CaretBrowsing.isCaretVisible && CaretBrowsing.caretElement) { - window.clearInterval(CaretBrowsing.blinkFunctionId); if (CaretBrowsing.caretElement) { CaretBrowsing.isSelectionCollapsed = false; CaretBrowsing.caretElement.parentElement.removeChild( @@ -1271,14 +1251,20 @@ window._qutebrowser.caret = (function() { }; CaretBrowsing.startAnimation = function() { - CaretBrowsing.caretElement.style.animationIterationCount = "infinite"; + if (CaretBrowsing.caretElement) { + CaretBrowsing.caretElement.style.animationIterationCount = "infinite"; + } }; CaretBrowsing.stopAnimation = function() { - CaretBrowsing.caretElement.style.animationIterationCount = 0; - window.setTimeout(() => { - CaretBrowsing.startAnimation(); - }, 1000); + if (CaretBrowsing.caretElement) { + CaretBrowsing.caretElement.style.animationIterationCount = 0; + window.clearTimeout(CaretBrowsing.animationFunctionId); + + CaretBrowsing.animationFunctionId = window.setTimeout(() => { + CaretBrowsing.startAnimation(); + }, 1000); + } }; CaretBrowsing.init = function() { @@ -1318,9 +1304,9 @@ window._qutebrowser.caret = (function() { return CaretBrowsing.selectionEnabled; }; - funcs.setPlatform = (platform, qtVersion) => { - CaretBrowsing.isWindows = platform.startsWith("win"); - CaretBrowsing.isOldQt = qtVersion === "5.7.1"; + funcs.setFlags = (flags) => { + CaretBrowsing.isWindows = flags.includes("windows"); + CaretBrowsing.needsFilterPrefix = flags.includes("filter-prefix"); }; funcs.disableCaret = () => { @@ -1331,67 +1317,80 @@ window._qutebrowser.caret = (function() { CaretBrowsing.toggle(); }; - funcs.moveRight = () => { + funcs.moveRight = (count = 1) => { + CaretBrowsing.move("right", "character", count); + CaretBrowsing.finishMove(); + }; + + funcs.moveLeft = (count = 1) => { + CaretBrowsing.move("left", "character", count); + CaretBrowsing.finishMove(); + }; + + funcs.moveDown = (count = 1) => { + CaretBrowsing.move("forward", "line", count); + CaretBrowsing.finishMove(); + }; + + funcs.moveUp = (count = 1) => { + CaretBrowsing.move("backward", "line", count); + CaretBrowsing.finishMove(); + }; + + funcs.moveToEndOfWord = (count = 1) => { + CaretBrowsing.move("forward", "word", count); + CaretBrowsing.finishMove(); + }; + + funcs.moveToNextWord = (count = 1) => { + CaretBrowsing.move("forward", "word", count); CaretBrowsing.move("right", "character"); + CaretBrowsing.finishMove(); }; - funcs.moveLeft = () => { - CaretBrowsing.move("left", "character"); - }; - - funcs.moveDown = () => { - CaretBrowsing.move("forward", "line"); - }; - - funcs.moveUp = () => { - CaretBrowsing.move("backward", "line"); - }; - - funcs.moveToEndOfWord = () => { - funcs.moveToNextWord(); - funcs.moveLeft(); - }; - - funcs.moveToNextWord = () => { - CaretBrowsing.move("forward", "word"); - funcs.moveRight(); - }; - - funcs.moveToPreviousWord = () => { - CaretBrowsing.move("backward", "word"); + funcs.moveToPreviousWord = (count = 1) => { + CaretBrowsing.move("backward", "word", count); + CaretBrowsing.finishMove(); }; funcs.moveToStartOfLine = () => { CaretBrowsing.move("left", "lineboundary"); + CaretBrowsing.finishMove(); }; funcs.moveToEndOfLine = () => { CaretBrowsing.move("right", "lineboundary"); + CaretBrowsing.finishMove(); }; - funcs.moveToStartOfNextBlock = () => { - CaretBrowsing.moveToBlock("forward", "backward"); + funcs.moveToStartOfNextBlock = (count = 1) => { + CaretBrowsing.moveToBlock("forward", "backward", count); + CaretBrowsing.finishMove(); }; - funcs.moveToStartOfPrevBlock = () => { - CaretBrowsing.moveToBlock("backward", "backward"); + funcs.moveToStartOfPrevBlock = (count = 1) => { + CaretBrowsing.moveToBlock("backward", "backward", count); + CaretBrowsing.finishMove(); }; - funcs.moveToEndOfNextBlock = () => { - CaretBrowsing.moveToBlock("forward", "forward"); + funcs.moveToEndOfNextBlock = (count = 1) => { + CaretBrowsing.moveToBlock("forward", "forward", count); + CaretBrowsing.finishMove(); }; - funcs.moveToEndOfPrevBlock = () => { - CaretBrowsing.moveToBlock("backward", "forward"); + funcs.moveToEndOfPrevBlock = (count = 1) => { + CaretBrowsing.moveToBlock("backward", "forward", count); + CaretBrowsing.finishMove(); }; funcs.moveToStartOfDocument = () => { CaretBrowsing.move("backward", "documentboundary"); + CaretBrowsing.finishMove(); }; funcs.moveToEndOfDocument = () => { CaretBrowsing.move("forward", "documentboundary"); - funcs.moveLeft(); + CaretBrowsing.finishMove(); }; funcs.dropSelection = () => { diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py index 93df8e70f..21b373dd1 100644 --- a/qutebrowser/utils/javascript.py +++ b/qutebrowser/utils/javascript.py @@ -59,6 +59,8 @@ def _convert_js_arg(arg): return str(arg).lower() elif isinstance(arg, (int, float)): return str(arg) + elif isinstance(arg, list): + return '[{}]'.format(', '.join(_convert_js_arg(e) for e in arg)) else: raise TypeError("Don't know how to handle {!r} of type {}!".format( arg, type(arg).__name__)) diff --git a/scripts/dev/ci/travis_run.sh b/scripts/dev/ci/travis_run.sh index 55ca7c11e..a287e844e 100644 --- a/scripts/dev/ci/travis_run.sh +++ b/scripts/dev/ci/travis_run.sh @@ -28,5 +28,9 @@ else args=() [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb' 'tests/unit') + # WORKAROUND for unknown crash inside swrast_dri.so + # See https://github.com/qutebrowser/qutebrowser/pull/4218#issuecomment-421931770 + [[ $TESTENV == py36-pyqt59 ]] && export QT_QUICK_BACKEND=software + tox -e "$TESTENV" -- "${args[@]}" fi diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index 7adf45f76..d8523a4b4 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -52,6 +52,7 @@ def main(): # pytest fixtures 'redefined-outer-name', 'unused-argument', + 'too-many-arguments', # things which are okay in tests 'missing-docstring', 'protected-access', @@ -65,9 +66,12 @@ def main(): toxinidir, ] - args = (['--disable={}'.format(','.join(disabled)), - '--ignored-modules=helpers,pytest,PyQt5'] + - sys.argv[2:] + files) + args = [ + '--disable={}'.format(','.join(disabled)), + '--ignored-modules=helpers,pytest,PyQt5', + r'--ignore-long-lines=( And I run :follow-selected --tab Then data/hello.txt should be loaded - - # Search + caret mode - - # https://bugreports.qt.io/browse/QTBUG-60673 - @qtbug60673 - Scenario: yanking a searched line - When I run :leave-mode - And I run :search fiv - And I wait for "search found fiv" in the log - And I run :enter-mode caret - And I run :move-to-end-of-line - And I run :yank selection - Then the clipboard should contain "five six" - - @qtbug60673 - Scenario: yanking a searched line with multiple matches - When I run :leave-mode - And I run :search w - And I wait for "search found w" in the log - And I run :search-next - And I wait for "next_result found w" in the log - And I run :enter-mode caret - And I run :move-to-end-of-line - And I run :yank selection - Then the clipboard should contain "wei drei" diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 5ce081e3b..5332d1433 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -31,6 +31,8 @@ import itertools import textwrap import unittest.mock import types +import mimetypes +import os.path import attr import pytest @@ -44,12 +46,15 @@ import helpers.utils from qutebrowser.config import (config, configdata, configtypes, configexc, configfiles, configcache) from qutebrowser.utils import objreg, standarddir, utils, usertypes -from qutebrowser.browser import greasemonkey, history +from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser.webkit import cookies from qutebrowser.misc import savemanager, sql, objects from qutebrowser.keyinput import modeman +_qute_scheme_handler = None + + class WinRegistryHelper: """Helper class for win_registry.""" @@ -152,29 +157,86 @@ def greasemonkey_manager(data_tmpdir): objreg.delete('greasemonkey') +@pytest.fixture(scope='session') +def testdata_scheme(qapp): + try: + global _qute_scheme_handler + from qutebrowser.browser.webengine import webenginequtescheme + from PyQt5.QtWebEngineWidgets import QWebEngineProfile + _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler( + parent=qapp) + _qute_scheme_handler.install(QWebEngineProfile.defaultProfile()) + except ImportError: + pass + + @qutescheme.add_handler('testdata') + def handler(url): # pylint: disable=unused-variable + file_abs = os.path.abspath(os.path.dirname(__file__)) + filename = os.path.join(file_abs, os.pardir, 'end2end', + url.path().lstrip('/')) + with open(filename, 'rb') as f: + data = f.read() + + mimetype, _encoding = mimetypes.guess_type(filename) + return mimetype, data + + @pytest.fixture -def webkit_tab(qtbot, tab_registry, cookiejar_and_cache, mode_manager, - session_manager_stub, greasemonkey_manager, fake_args): +def web_tab_setup(qtbot, tab_registry, session_manager_stub, + greasemonkey_manager, fake_args, host_blocker_stub, + config_stub, testdata_scheme): + """Shared setup for webkit_tab/webengine_tab.""" + # Make sure error logging via JS fails tests + config_stub.val.content.javascript.log = { + 'info': 'info', + 'error': 'error', + 'unknown': 'error', + 'warning': 'error', + } + + +@pytest.fixture +def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager): webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab') + + container = QWidget() + qtbot.add_widget(container) + + vbox = QVBoxLayout(container) tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager, private=False) - qtbot.add_widget(tab) + vbox.addWidget(tab) + # to make sure container isn't GCed + tab.container = container + + with qtbot.waitExposed(container): + container.show() + return tab @pytest.fixture -def webengine_tab(qtbot, tab_registry, fake_args, mode_manager, - session_manager_stub, greasemonkey_manager, - redirect_webengine_data, tabbed_browser_stubs): +def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data, + tabbed_browser_stubs, mode_manager): tabwidget = tabbed_browser_stubs[0].widget tabwidget.current_index = 0 tabwidget.index_of = 0 + container = QWidget() + qtbot.add_widget(container) + + vbox = QVBoxLayout(container) webenginetab = pytest.importorskip( 'qutebrowser.browser.webengine.webenginetab') tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager, private=False) - qtbot.add_widget(tab) + vbox.addWidget(tab) + # to make sure container isn't GCed + tab.container = container + + with qtbot.waitExposed(container): + container.show() + return tab @@ -455,9 +517,8 @@ def fake_args(request): @pytest.fixture -def mode_manager(win_registry, config_stub, qapp): - mm = modeman.ModeManager(0) - objreg.register('mode-manager', mm, scope='window', window=0) +def mode_manager(win_registry, config_stub, key_config_stub, qapp): + mm = modeman.init(0, parent=qapp) yield mm objreg.delete('mode-manager', scope='window', window=0) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index f10522a02..9af7a6fcc 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -475,6 +475,9 @@ class HostBlockerStub: def __init__(self): self.blocked_hosts = set() + def is_blocked(self, url): + return url in self.blocked_hosts + class SessionManagerStub: diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 06cf54467..66b3f8133 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -203,6 +203,7 @@ class CallbackChecker(QObject): def check(self, expected): """Wait until the JS result arrived and compare it.""" + __tracebackhide__ = True if self._result is self.UNSET: with self._qtbot.waitSignal(self.got_result, timeout=2000): pass diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py new file mode 100644 index 000000000..c93ff2afc --- /dev/null +++ b/tests/unit/browser/test_caret.py @@ -0,0 +1,359 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +"""Tests for caret browsing mode.""" + +import textwrap + +import pytest +from PyQt5.QtCore import QUrl + +from qutebrowser.utils import usertypes +from helpers import utils + + +@pytest.fixture +def caret(web_tab, qtbot, mode_manager): + with qtbot.wait_signal(web_tab.load_finished): + web_tab.openurl(QUrl('qute://testdata/data/caret.html')) + + mode_manager.enter(usertypes.KeyMode.caret) + + return web_tab.caret + + +class Selection: + + """Helper to interact with the caret selection.""" + + def __init__(self, qtbot, caret): + self._qtbot = qtbot + self._caret = caret + self._callback_checker = utils.CallbackChecker(qtbot) + + def check(self, expected, *, strip=False): + """Check whether we got the expected selection. + + Since (especially on Windows) the selection is empty if we're checking + too quickly, we try to read it multiple times. + """ + for _ in range(10): + with self._qtbot.wait_signal( + self._callback_checker.got_result) as blocker: + self._caret.selection(self._callback_checker.callback) + + selection = blocker.args[0] + if selection: + if strip: + selection = selection.strip() + assert selection == expected + return + + self._qtbot.wait(50) + + def check_multiline(self, expected, *, strip=False): + self.check(textwrap.dedent(expected).strip(), strip=strip) + + def toggle(self): + with self._qtbot.wait_signal(self._caret.selection_toggled): + self._caret.toggle_selection() + + +@pytest.fixture +def selection(qtbot, caret, callback_checker): + return Selection(qtbot, caret) + + +class TestDocument: + + def test_selecting_entire_document(self, caret, selection): + selection.toggle() + caret.move_to_end_of_document() + selection.check_multiline(""" + one two three + eins zwei drei + + four five six + vier fünf sechs + """, strip=True) + + def test_moving_to_end_and_start(self, caret, selection): + caret.move_to_end_of_document() + caret.move_to_start_of_document() + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + def test_moving_to_end_and_start_with_selection(self, caret, selection): + caret.move_to_end_of_document() + selection.toggle() + caret.move_to_start_of_document() + selection.check_multiline(""" + one two three + eins zwei drei + + four five six + vier fünf sechs + """, strip=True) + + +class TestBlock: + + def test_selecting_block(self, caret, selection): + selection.toggle() + caret.move_to_end_of_next_block() + selection.check_multiline(""" + one two three + eins zwei drei + """) + + def test_moving_back_to_the_end_of_prev_block_with_sel(self, caret, selection): + caret.move_to_end_of_next_block(2) + selection.toggle() + caret.move_to_end_of_prev_block() + caret.move_to_prev_word() + selection.check_multiline(""" + drei + + four five six + """) + + def test_moving_back_to_the_end_of_prev_block(self, caret, selection): + caret.move_to_end_of_next_block(2) + caret.move_to_end_of_prev_block() + selection.toggle() + caret.move_to_prev_word() + selection.check("drei") + + def test_moving_back_to_the_start_of_prev_block_with_sel(self, caret, selection): + caret.move_to_end_of_next_block(2) + selection.toggle() + caret.move_to_start_of_prev_block() + selection.check_multiline(""" + eins zwei drei + + four five six + """) + + def test_moving_back_to_the_start_of_prev_block(self, caret, selection): + caret.move_to_end_of_next_block(2) + caret.move_to_start_of_prev_block() + selection.toggle() + caret.move_to_next_word() + selection.check("eins ") + + def test_moving_to_the_start_of_next_block_with_sel(self, caret, selection): + selection.toggle() + caret.move_to_start_of_next_block() + selection.check("one two three\n") + + def test_moving_to_the_start_of_next_block(self, caret, selection): + caret.move_to_start_of_next_block() + selection.toggle() + caret.move_to_end_of_word() + selection.check("eins") + + +class TestLine: + + def test_selecting_a_line(self, caret, selection): + selection.toggle() + caret.move_to_end_of_line() + selection.check("one two three") + + def test_moving_and_selecting_a_line(self, caret, selection): + caret.move_to_next_line() + selection.toggle() + caret.move_to_end_of_line() + selection.check("eins zwei drei") + + def test_selecting_next_line(self, caret, selection): + selection.toggle() + caret.move_to_next_line() + selection.check("one two three\n") + + def test_moving_to_end_and_to_start_of_line(self, caret, selection): + caret.move_to_end_of_line() + caret.move_to_start_of_line() + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + def test_selecting_a_line_backwards(self, caret, selection): + caret.move_to_end_of_line() + selection.toggle() + caret.move_to_start_of_line() + selection.check("one two three") + + def test_selecting_previous_line(self, caret, selection): + caret.move_to_next_line() + selection.toggle() + caret.move_to_prev_line() + selection.check("one two three\n") + + def test_moving_to_previous_line(self, caret, selection): + caret.move_to_next_line() + caret.move_to_prev_line() + selection.toggle() + caret.move_to_next_line() + selection.check("one two three\n") + + +class TestWord: + + def test_selecting_a_word(self, caret, selection): + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + def test_moving_to_end_and_selecting_a_word(self, caret, selection): + caret.move_to_end_of_word() + selection.toggle() + caret.move_to_end_of_word() + selection.check(" two") + + def test_moving_to_next_word_and_selecting_a_word(self, caret, selection): + caret.move_to_next_word() + selection.toggle() + caret.move_to_end_of_word() + selection.check("two") + + def test_moving_to_next_word_and_selecting_until_next_word(self, caret, selection): + caret.move_to_next_word() + selection.toggle() + caret.move_to_next_word() + selection.check("two ") + + def test_moving_to_previous_word_and_selecting_a_word(self, caret, selection): + caret.move_to_end_of_word() + selection.toggle() + caret.move_to_prev_word() + selection.check("one") + + def test_moving_to_previous_word(self, caret, selection): + caret.move_to_end_of_word() + caret.move_to_prev_word() + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + +class TestChar: + + def test_selecting_a_char(self, caret, selection): + selection.toggle() + caret.move_to_next_char() + selection.check("o") + + def test_moving_and_selecting_a_char(self, caret, selection): + caret.move_to_next_char() + selection.toggle() + caret.move_to_next_char() + selection.check("n") + + def test_selecting_previous_char(self, caret, selection): + caret.move_to_end_of_word() + selection.toggle() + caret.move_to_prev_char() + selection.check("e") + + def test_moving_to_previous_char(self, caret, selection): + caret.move_to_end_of_word() + caret.move_to_prev_char() + selection.toggle() + caret.move_to_end_of_word() + selection.check("e") + + +def test_drop_selection(caret, selection): + selection.toggle() + caret.move_to_end_of_word() + caret.drop_selection() + selection.check("") + + +class TestSearch: + + # https://bugreports.qt.io/browse/QTBUG-60673 + + @pytest.mark.qtbug60673 + @pytest.mark.no_xvfb + def test_yanking_a_searched_line(self, caret, selection, mode_manager, callback_checker, web_tab, qtbot): + web_tab.show() + mode_manager.leave(usertypes.KeyMode.caret) + + web_tab.search.search('fiv', result_cb=callback_checker.callback) + callback_checker.check(True) + + mode_manager.enter(usertypes.KeyMode.caret) + caret.move_to_end_of_line() + selection.check('five six') + + @pytest.mark.qtbug60673 + @pytest.mark.no_xvfb + def test_yanking_a_searched_line_with_multiple_matches(self, caret, selection, mode_manager, callback_checker, web_tab, qtbot): + web_tab.show() + mode_manager.leave(usertypes.KeyMode.caret) + + web_tab.search.search('w', result_cb=callback_checker.callback) + callback_checker.check(True) + + web_tab.search.next_result(result_cb=callback_checker.callback) + callback_checker.check(True) + + mode_manager.enter(usertypes.KeyMode.caret) + + caret.move_to_end_of_line() + selection.check('wei drei') + + +class TestFollowSelected: + + LOAD_STARTED_DELAY = 50 + + @pytest.fixture(params=[True, False], autouse=True) + def toggle_js(self, request, config_stub): + config_stub.val.content.javascript.enabled = request.param + + def test_follow_selected_without_a_selection(self, qtbot, caret, selection, web_tab, + mode_manager): + caret.move_to_next_word() # Move cursor away from the link + mode_manager.leave(usertypes.KeyMode.caret) + with qtbot.wait_signal(caret.follow_selected_done): + with qtbot.assert_not_emitted(web_tab.load_started): + caret.follow_selected() + qtbot.wait(self.LOAD_STARTED_DELAY) + + def test_follow_selected_with_text(self, qtbot, caret, selection, web_tab): + caret.move_to_next_word() + selection.toggle() + caret.move_to_end_of_word() + with qtbot.wait_signal(caret.follow_selected_done): + with qtbot.assert_not_emitted(web_tab.load_started): + caret.follow_selected() + qtbot.wait(self.LOAD_STARTED_DELAY) + + def test_follow_selected_with_link(self, caret, selection, config_stub, + qtbot, web_tab): + selection.toggle() + caret.move_to_end_of_word() + with qtbot.wait_signal(web_tab.load_finished): + with qtbot.wait_signal(caret.follow_selected_done): + caret.follow_selected() + assert web_tab.url().path() == '/data/hello.txt' diff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py index 29e090fd0..0a196cfa1 100644 --- a/tests/unit/utils/test_javascript.py +++ b/tests/unit/utils/test_javascript.py @@ -84,6 +84,7 @@ class TestStringEscape: (None, 'undefined'), (object(), TypeError), (True, 'true'), + ([23, True, 'x'], '[23, true, "x"]'), ]) def test_convert_js_arg(arg, expected): if expected is TypeError: diff --git a/tox.ini b/tox.ini index 02f3ae729..37451918c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ setenv = pyqt{,56,571,59,510,511}: LINK_PYQT_SKIP=true pyqt{,56,571,59,510,511}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= -passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER +passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER QT_QUICK_BACKEND basepython = py35: {env:PYTHON:python3.5} py36: {env:PYTHON:python3.6}