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

This commit is contained in:
Florian Bruhin 2018-09-26 11:20:45 +02:00
commit a292664ca0
15 changed files with 591 additions and 420 deletions

View File

@ -395,9 +395,11 @@ class AbstractCaret(QObject):
Signals: Signals:
selection_toggled: Emitted when the selection was toggled. selection_toggled: Emitted when the selection was toggled.
arg: Whether the selection is now active. arg: Whether the selection is now active.
follow_selected_done: Emitted when a follow_selection action is done.
""" """
selection_toggled = pyqtSignal(bool) selection_toggled = pyqtSignal(bool)
follow_selected_done = pyqtSignal()
def __init__(self, tab, mode_manager, parent=None): def __init__(self, tab, mode_manager, parent=None):
super().__init__(parent) super().__init__(parent)

View File

@ -21,12 +21,11 @@
import math import math
import functools import functools
import sys
import re import re
import html as html_utils import html as html_utils
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, 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.QtGui import QKeyEvent, QIcon
from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -234,6 +233,15 @@ class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection.""" """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) @pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode): def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret: if mode != usertypes.KeyMode.caret:
@ -246,9 +254,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.search.clear() self._tab.search.clear()
self._tab.run_js_async( self._tab.run_js_async(
javascript.assemble('caret', javascript.assemble('caret', 'setFlags', self._flags()))
'setPlatform', sys.platform, qVersion()))
self._js_call('setInitialCursor', self._selection_cb) self._js_call('setInitialCursor', callback=self._selection_cb)
def _selection_cb(self, enabled): def _selection_cb(self, enabled):
"""Emit selection_toggled based on setInitialCursor.""" """Emit selection_toggled based on setInitialCursor."""
@ -266,32 +274,25 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('disableCaret') self._js_call('disableCaret')
def move_to_next_line(self, count=1): def move_to_next_line(self, count=1):
for _ in range(count): self._js_call('moveDown', count)
self._js_call('moveDown')
def move_to_prev_line(self, count=1): def move_to_prev_line(self, count=1):
for _ in range(count): self._js_call('moveUp', count)
self._js_call('moveUp')
def move_to_next_char(self, count=1): def move_to_next_char(self, count=1):
for _ in range(count): self._js_call('moveRight', count)
self._js_call('moveRight')
def move_to_prev_char(self, count=1): def move_to_prev_char(self, count=1):
for _ in range(count): self._js_call('moveLeft', count)
self._js_call('moveLeft')
def move_to_end_of_word(self, count=1): def move_to_end_of_word(self, count=1):
for _ in range(count): self._js_call('moveToEndOfWord', count)
self._js_call('moveToEndOfWord')
def move_to_next_word(self, count=1): def move_to_next_word(self, count=1):
for _ in range(count): self._js_call('moveToNextWord', count)
self._js_call('moveToNextWord')
def move_to_prev_word(self, count=1): def move_to_prev_word(self, count=1):
for _ in range(count): self._js_call('moveToPreviousWord', count)
self._js_call('moveToPreviousWord')
def move_to_start_of_line(self): def move_to_start_of_line(self):
self._js_call('moveToStartOfLine') self._js_call('moveToStartOfLine')
@ -300,20 +301,16 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfLine') self._js_call('moveToEndOfLine')
def move_to_start_of_next_block(self, count=1): def move_to_start_of_next_block(self, count=1):
for _ in range(count): self._js_call('moveToStartOfNextBlock', count)
self._js_call('moveToStartOfNextBlock')
def move_to_start_of_prev_block(self, count=1): def move_to_start_of_prev_block(self, count=1):
for _ in range(count): self._js_call('moveToStartOfPrevBlock', count)
self._js_call('moveToStartOfPrevBlock')
def move_to_end_of_next_block(self, count=1): def move_to_end_of_next_block(self, count=1):
for _ in range(count): self._js_call('moveToEndOfNextBlock', count)
self._js_call('moveToEndOfNextBlock')
def move_to_end_of_prev_block(self, count=1): def move_to_end_of_prev_block(self, count=1):
for _ in range(count): self._js_call('moveToEndOfPrevBlock', count)
self._js_call('moveToEndOfPrevBlock')
def move_to_start_of_document(self): def move_to_start_of_document(self):
self._js_call('moveToStartOfDocument') self._js_call('moveToStartOfDocument')
@ -322,7 +319,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfDocument') self._js_call('moveToEndOfDocument')
def toggle_selection(self): 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): def drop_selection(self):
self._js_call('dropSelection') self._js_call('dropSelection')
@ -335,7 +332,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async(javascript.assemble('caret', 'getSelection'), self._tab.run_js_async(javascript.assemble('caret', 'getSelection'),
callback) 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. """Callback for javascript which clicks the selected element.
Args: Args:
@ -344,6 +347,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
""" """
if js_elem is None: if js_elem is None:
return return
if js_elem == "focused": if js_elem == "focused":
# we had a focused element, not a selected one. Just send <enter> # we had a focused element, not a selected one. Just send <enter>
self._follow_enter(tab) self._follow_enter(tab)
@ -364,7 +368,6 @@ class WebEngineCaret(browsertab.AbstractCaret):
elem.click(click_type) elem.click(click_type)
except webelem.Error as e: except webelem.Error as e:
message.error(str(e)) message.error(str(e))
return
def follow_selected(self, *, tab=False): def follow_selected(self, *, tab=False):
if self._tab.search.search_displayed: if self._tab.search.search_displayed:
@ -380,11 +383,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
# click an existing blue selection # click an existing blue selection
js_code = javascript.assemble('webelem', js_code = javascript.assemble('webelem',
'find_selected_focused_link') 'find_selected_focused_link')
self._tab.run_js_async(js_code, lambda jsret: self._tab.run_js_async(
self._follow_selected_cb(jsret, tab)) js_code,
lambda jsret: self._follow_selected_cb_wrapped(jsret, tab))
def _js_call(self, command, callback=None): def _js_call(self, command, *args, callback=None):
self._tab.run_js_async(javascript.assemble('caret', command), callback) code = javascript.assemble('caret', command, *args)
self._tab.run_js_async(code, callback)
class WebEngineScroller(browsertab.AbstractScroller): class WebEngineScroller(browsertab.AbstractScroller):

View File

@ -348,7 +348,7 @@ class WebKitCaret(browsertab.AbstractCaret):
def selection(self, callback): def selection(self, callback):
callback(self._widget.selectedText()) callback(self._widget.selectedText())
def follow_selected(self, *, tab=False): def _follow_selected(self, *, tab=False):
if QWebSettings.globalSettings().testAttribute( if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled): QWebSettings.JavascriptEnabled):
if tab: if tab:
@ -389,6 +389,12 @@ class WebKitCaret(browsertab.AbstractCaret):
else: else:
self._tab.openurl(url) 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): class WebKitZoom(browsertab.AbstractZoom):

View File

@ -755,31 +755,6 @@ window._qutebrowser.caret = (function() {
*/ */
CaretBrowsing.isSelectionCollapsed = false; 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. * Whether we're running on Windows.
* @type {boolean} * @type {boolean}
@ -788,9 +763,17 @@ window._qutebrowser.caret = (function() {
/** /**
* Whether we're running on on old Qt 5.7.1. * Whether we're running on on old Qt 5.7.1.
* There, we need to use -webkit-filter.
* @type {boolean} * @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 * 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() { CaretBrowsing.injectCaretStyles = function() {
const prefix = CaretBrowsing.isOldQt ? "-webkit-" : ""; const prefix = CaretBrowsing.needsFilterPrefix ? "-webkit-" : "";
const style = ` const style = `
.CaretBrowsing_Caret { .CaretBrowsing_Caret {
position: absolute; position: absolute;
@ -987,7 +970,6 @@ window._qutebrowser.caret = (function() {
*/ */
CaretBrowsing.recreateCaretElement = function() { CaretBrowsing.recreateCaretElement = function() {
if (CaretBrowsing.caretElement) { if (CaretBrowsing.caretElement) {
window.clearInterval(CaretBrowsing.blinkFunctionId);
CaretBrowsing.caretElement.parentElement.removeChild( CaretBrowsing.caretElement.parentElement.removeChild(
CaretBrowsing.caretElement); CaretBrowsing.caretElement);
CaretBrowsing.caretElement = null; CaretBrowsing.caretElement = null;
@ -1163,34 +1145,39 @@ window._qutebrowser.caret = (function() {
} }
}; };
CaretBrowsing.move = function(direction, granularity) { CaretBrowsing.move = function(direction, granularity, count = 1) {
let action = "move"; let action = "move";
if (CaretBrowsing.selectionEnabled) { if (CaretBrowsing.selectionEnabled) {
action = "extend"; action = "extend";
} }
for (let i = 0; i < count; i++) {
window. window.
getSelection(). getSelection().
modify(action, direction, granularity); modify(action, direction, granularity);
}
if (CaretBrowsing.isWindows && if (CaretBrowsing.isWindows &&
(direction === "forward" || (direction === "forward" ||
direction === "right") && direction === "right") &&
granularity === "word") { granularity === "word") {
CaretBrowsing.move("left", "character"); CaretBrowsing.move("left", "character");
} else { }
};
CaretBrowsing.finishMove = function() {
window.setTimeout(() => { window.setTimeout(() => {
CaretBrowsing.updateCaretOrSelection(true); CaretBrowsing.updateCaretOrSelection(true);
}, 0); }, 0);
}
CaretBrowsing.stopAnimation(); CaretBrowsing.stopAnimation();
}; };
CaretBrowsing.moveToBlock = function(paragraph, boundary) { CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) {
let action = "move"; let action = "move";
if (CaretBrowsing.selectionEnabled) { if (CaretBrowsing.selectionEnabled) {
action = "extend"; action = "extend";
} }
for (let i = 0; i < count; i++) {
window. window.
getSelection(). getSelection().
modify(action, paragraph, "paragraph"); modify(action, paragraph, "paragraph");
@ -1198,12 +1185,7 @@ window._qutebrowser.caret = (function() {
window. window.
getSelection(). getSelection().
modify(action, boundary, "paragraphboundary"); modify(action, boundary, "paragraphboundary");
}
window.setTimeout(() => {
CaretBrowsing.updateCaretOrSelection(true);
}, 0);
CaretBrowsing.stopAnimation();
}; };
CaretBrowsing.toggle = function(value) { CaretBrowsing.toggle = function(value) {
@ -1232,7 +1214,6 @@ window._qutebrowser.caret = (function() {
return true; return true;
} }
window.setTimeout(() => { window.setTimeout(() => {
CaretBrowsing.targetX = null;
CaretBrowsing.updateCaretOrSelection(false); CaretBrowsing.updateCaretOrSelection(false);
}, 0); }, 0);
return true; return true;
@ -1250,7 +1231,6 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.updateCaretOrSelection(true); CaretBrowsing.updateCaretOrSelection(true);
} else if (!CaretBrowsing.isCaretVisible && } else if (!CaretBrowsing.isCaretVisible &&
CaretBrowsing.caretElement) { CaretBrowsing.caretElement) {
window.clearInterval(CaretBrowsing.blinkFunctionId);
if (CaretBrowsing.caretElement) { if (CaretBrowsing.caretElement) {
CaretBrowsing.isSelectionCollapsed = false; CaretBrowsing.isSelectionCollapsed = false;
CaretBrowsing.caretElement.parentElement.removeChild( CaretBrowsing.caretElement.parentElement.removeChild(
@ -1271,14 +1251,20 @@ window._qutebrowser.caret = (function() {
}; };
CaretBrowsing.startAnimation = function() { CaretBrowsing.startAnimation = function() {
if (CaretBrowsing.caretElement) {
CaretBrowsing.caretElement.style.animationIterationCount = "infinite"; CaretBrowsing.caretElement.style.animationIterationCount = "infinite";
}
}; };
CaretBrowsing.stopAnimation = function() { CaretBrowsing.stopAnimation = function() {
if (CaretBrowsing.caretElement) {
CaretBrowsing.caretElement.style.animationIterationCount = 0; CaretBrowsing.caretElement.style.animationIterationCount = 0;
window.setTimeout(() => { window.clearTimeout(CaretBrowsing.animationFunctionId);
CaretBrowsing.animationFunctionId = window.setTimeout(() => {
CaretBrowsing.startAnimation(); CaretBrowsing.startAnimation();
}, 1000); }, 1000);
}
}; };
CaretBrowsing.init = function() { CaretBrowsing.init = function() {
@ -1318,9 +1304,9 @@ window._qutebrowser.caret = (function() {
return CaretBrowsing.selectionEnabled; return CaretBrowsing.selectionEnabled;
}; };
funcs.setPlatform = (platform, qtVersion) => { funcs.setFlags = (flags) => {
CaretBrowsing.isWindows = platform.startsWith("win"); CaretBrowsing.isWindows = flags.includes("windows");
CaretBrowsing.isOldQt = qtVersion === "5.7.1"; CaretBrowsing.needsFilterPrefix = flags.includes("filter-prefix");
}; };
funcs.disableCaret = () => { funcs.disableCaret = () => {
@ -1331,67 +1317,80 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.toggle(); 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.move("right", "character");
CaretBrowsing.finishMove();
}; };
funcs.moveLeft = () => { funcs.moveToPreviousWord = (count = 1) => {
CaretBrowsing.move("left", "character"); CaretBrowsing.move("backward", "word", count);
}; CaretBrowsing.finishMove();
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.moveToStartOfLine = () => { funcs.moveToStartOfLine = () => {
CaretBrowsing.move("left", "lineboundary"); CaretBrowsing.move("left", "lineboundary");
CaretBrowsing.finishMove();
}; };
funcs.moveToEndOfLine = () => { funcs.moveToEndOfLine = () => {
CaretBrowsing.move("right", "lineboundary"); CaretBrowsing.move("right", "lineboundary");
CaretBrowsing.finishMove();
}; };
funcs.moveToStartOfNextBlock = () => { funcs.moveToStartOfNextBlock = (count = 1) => {
CaretBrowsing.moveToBlock("forward", "backward"); CaretBrowsing.moveToBlock("forward", "backward", count);
CaretBrowsing.finishMove();
}; };
funcs.moveToStartOfPrevBlock = () => { funcs.moveToStartOfPrevBlock = (count = 1) => {
CaretBrowsing.moveToBlock("backward", "backward"); CaretBrowsing.moveToBlock("backward", "backward", count);
CaretBrowsing.finishMove();
}; };
funcs.moveToEndOfNextBlock = () => { funcs.moveToEndOfNextBlock = (count = 1) => {
CaretBrowsing.moveToBlock("forward", "forward"); CaretBrowsing.moveToBlock("forward", "forward", count);
CaretBrowsing.finishMove();
}; };
funcs.moveToEndOfPrevBlock = () => { funcs.moveToEndOfPrevBlock = (count = 1) => {
CaretBrowsing.moveToBlock("backward", "forward"); CaretBrowsing.moveToBlock("backward", "forward", count);
CaretBrowsing.finishMove();
}; };
funcs.moveToStartOfDocument = () => { funcs.moveToStartOfDocument = () => {
CaretBrowsing.move("backward", "documentboundary"); CaretBrowsing.move("backward", "documentboundary");
CaretBrowsing.finishMove();
}; };
funcs.moveToEndOfDocument = () => { funcs.moveToEndOfDocument = () => {
CaretBrowsing.move("forward", "documentboundary"); CaretBrowsing.move("forward", "documentboundary");
funcs.moveLeft(); CaretBrowsing.finishMove();
}; };
funcs.dropSelection = () => { funcs.dropSelection = () => {

View File

@ -59,6 +59,8 @@ def _convert_js_arg(arg):
return str(arg).lower() return str(arg).lower()
elif isinstance(arg, (int, float)): elif isinstance(arg, (int, float)):
return str(arg) return str(arg)
elif isinstance(arg, list):
return '[{}]'.format(', '.join(_convert_js_arg(e) for e in arg))
else: else:
raise TypeError("Don't know how to handle {!r} of type {}!".format( raise TypeError("Don't know how to handle {!r} of type {}!".format(
arg, type(arg).__name__)) arg, type(arg).__name__))

View File

@ -28,5 +28,9 @@ else
args=() args=()
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb' 'tests/unit') [[ $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[@]}" tox -e "$TESTENV" -- "${args[@]}"
fi fi

View File

@ -52,6 +52,7 @@ def main():
# pytest fixtures # pytest fixtures
'redefined-outer-name', 'redefined-outer-name',
'unused-argument', 'unused-argument',
'too-many-arguments',
# things which are okay in tests # things which are okay in tests
'missing-docstring', 'missing-docstring',
'protected-access', 'protected-access',
@ -65,9 +66,12 @@ def main():
toxinidir, toxinidir,
] ]
args = (['--disable={}'.format(','.join(disabled)), args = [
'--ignored-modules=helpers,pytest,PyQt5'] + '--disable={}'.format(','.join(disabled)),
sys.argv[2:] + files) '--ignored-modules=helpers,pytest,PyQt5',
r'--ignore-long-lines=(<?https?://|^# Copyright 201\d)|^ *def [a-z]',
r'--method-rgx=[a-z_][A-Za-z0-9_]{1,100}$',
] + sys.argv[2:] + files
env = os.environ.copy() env = os.environ.copy()
env['PYTHONPATH'] = os.pathsep.join(pythonpath) env['PYTHONPATH'] = os.pathsep.join(pythonpath)

View File

@ -43,6 +43,7 @@ import qutebrowser.app # To register commands
ON_CI = 'CI' in os.environ ON_CI = 'CI' in os.environ
_qute_scheme_handler = None
# Set hypothesis settings # Set hypothesis settings

View File

@ -7,224 +7,6 @@ Feature: Caret mode
Given I open data/caret.html Given I open data/caret.html
And I run :tab-only ;; enter-mode caret And I run :tab-only ;; enter-mode caret
# document
Scenario: Selecting the entire document
When I run :toggle-selection
And I run :move-to-end-of-document
And I run :yank selection
Then the clipboard should contain:
one two three
eins zwei drei
four five six
vier fünf sechs
Scenario: Moving to end and to start of document
When I run :move-to-end-of-document
And I run :move-to-start-of-document
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Moving to end and to start of document (with selection)
When I run :move-to-end-of-document
And I run :toggle-selection
And I run :move-to-start-of-document
And I run :yank selection
Then the clipboard should contain:
one two three
eins zwei drei
four five six
vier fünf sechs
# block
Scenario: Selecting a block
When I run :toggle-selection
And I run :move-to-end-of-next-block
And I run :yank selection
Then the clipboard should contain:
one two three
eins zwei drei
Scenario: Moving back to the end of previous block (with selection)
When I run :move-to-end-of-next-block with count 2
And I run :toggle-selection
And I run :move-to-end-of-prev-block
And I run :move-to-prev-word
And I run :yank selection
Then the clipboard should contain:
drei
four five six
Scenario: Moving back to the end of previous block
When I run :move-to-end-of-next-block with count 2
And I run :move-to-end-of-prev-block
And I run :toggle-selection
And I run :move-to-prev-word
And I run :yank selection
Then the clipboard should contain "drei"
Scenario: Moving back to the start of previous block (with selection)
When I run :move-to-end-of-next-block with count 2
And I run :toggle-selection
And I run :move-to-start-of-prev-block
And I run :yank selection
Then the clipboard should contain:
eins zwei drei
four five six
Scenario: Moving back to the start of previous block
When I run :move-to-end-of-next-block with count 2
And I run :move-to-start-of-prev-block
And I run :toggle-selection
And I run :move-to-next-word
And I run :yank selection
Then the clipboard should contain "eins "
Scenario: Moving to the start of next block (with selection)
When I run :toggle-selection
And I run :move-to-start-of-next-block
And I run :yank selection
Then the clipboard should contain "one two three\n"
Scenario: Moving to the start of next block
When I run :move-to-start-of-next-block
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "eins"
# line
Scenario: Selecting a line
When I run :toggle-selection
And I run :move-to-end-of-line
And I run :yank selection
Then the clipboard should contain "one two three"
Scenario: Moving and selecting a line
When I run :move-to-next-line
And I run :toggle-selection
And I run :move-to-end-of-line
And I run :yank selection
Then the clipboard should contain "eins zwei drei"
Scenario: Selecting next line
When I run :toggle-selection
And I run :move-to-next-line
And I run :yank selection
Then the clipboard should contain "one two three\n"
Scenario: Moving to end and to start of line
When I run :move-to-end-of-line
And I run :move-to-start-of-line
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Selecting a line (backwards)
When I run :move-to-end-of-line
And I run :toggle-selection
When I run :move-to-start-of-line
And I run :yank selection
Then the clipboard should contain "one two three"
Scenario: Selecting previous line
When I run :move-to-next-line
And I run :toggle-selection
When I run :move-to-prev-line
And I run :yank selection
Then the clipboard should contain "one two three\n"
Scenario: Moving to previous line
When I run :move-to-next-line
When I run :move-to-prev-line
And I run :toggle-selection
When I run :move-to-next-line
And I run :yank selection
Then the clipboard should contain "one two three\n"
# word
Scenario: Selecting a word
When I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Moving to end and selecting a word
When I run :move-to-end-of-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain " two"
Scenario: Moving to next word and selecting a word
When I run :move-to-next-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "two"
Scenario: Moving to next word and selecting until next word
When I run :move-to-next-word
And I run :toggle-selection
And I run :move-to-next-word
And I run :yank selection
Then the clipboard should contain "two "
Scenario: Moving to previous word and selecting a word
When I run :move-to-end-of-word
And I run :toggle-selection
And I run :move-to-prev-word
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Moving to previous word
When I run :move-to-end-of-word
And I run :move-to-prev-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "one"
# char
Scenario: Selecting a char
When I run :toggle-selection
And I run :move-to-next-char
And I run :yank selection
Then the clipboard should contain "o"
Scenario: Moving and selecting a char
When I run :move-to-next-char
And I run :toggle-selection
And I run :move-to-next-char
And I run :yank selection
Then the clipboard should contain "n"
Scenario: Selecting previous char
When I run :move-to-end-of-word
And I run :toggle-selection
And I run :move-to-prev-char
And I run :yank selection
Then the clipboard should contain "e"
Scenario: Moving to previous char
When I run :move-to-end-of-word
And I run :move-to-prev-char
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank selection
Then the clipboard should contain "e"
# :yank selection # :yank selection
Scenario: :yank selection without selection Scenario: :yank selection without selection
@ -261,42 +43,8 @@ Feature: Caret mode
And the message "7 chars yanked to clipboard" should be shown. And the message "7 chars yanked to clipboard" should be shown.
And the clipboard should contain "one two" And the clipboard should contain "one two"
# :drop-selection
Scenario: :drop-selection
When I run :toggle-selection
And I run :move-to-end-of-word
And I run :drop-selection
And I run :yank selection
Then the message "Nothing to yank" should be shown.
# :follow-selected # :follow-selected
Scenario: :follow-selected without a selection
When I run :follow-selected
Then no crash should happen
Scenario: :follow-selected with text
When I run :move-to-next-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :follow-selected
Then no crash should happen
Scenario: :follow-selected with link (with JS)
When I set content.javascript.enabled to true
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :follow-selected
Then data/hello.txt should be loaded
Scenario: :follow-selected with link (without JS)
When I set content.javascript.enabled to false
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :follow-selected
Then data/hello.txt should be loaded
Scenario: :follow-selected with --tab (with JS) Scenario: :follow-selected with --tab (with JS)
When I set content.javascript.enabled to true When I set content.javascript.enabled to true
And I run :tab-only And I run :tab-only
@ -356,28 +104,3 @@ Feature: Caret mode
And I run :fake-key <tab> And I run :fake-key <tab>
And I run :follow-selected --tab And I run :follow-selected --tab
Then data/hello.txt should be loaded 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"

View File

@ -31,6 +31,8 @@ import itertools
import textwrap import textwrap
import unittest.mock import unittest.mock
import types import types
import mimetypes
import os.path
import attr import attr
import pytest import pytest
@ -44,12 +46,15 @@ import helpers.utils
from qutebrowser.config import (config, configdata, configtypes, configexc, from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles, configcache) configfiles, configcache)
from qutebrowser.utils import objreg, standarddir, utils, usertypes 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.browser.webkit import cookies
from qutebrowser.misc import savemanager, sql, objects from qutebrowser.misc import savemanager, sql, objects
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
_qute_scheme_handler = None
class WinRegistryHelper: class WinRegistryHelper:
"""Helper class for win_registry.""" """Helper class for win_registry."""
@ -152,29 +157,86 @@ def greasemonkey_manager(data_tmpdir):
objreg.delete('greasemonkey') 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 @pytest.fixture
def webkit_tab(qtbot, tab_registry, cookiejar_and_cache, mode_manager, def web_tab_setup(qtbot, tab_registry, session_manager_stub,
session_manager_stub, greasemonkey_manager, fake_args): 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') 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, tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
private=False) 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 return tab
@pytest.fixture @pytest.fixture
def webengine_tab(qtbot, tab_registry, fake_args, mode_manager, def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
session_manager_stub, greasemonkey_manager, tabbed_browser_stubs, mode_manager):
redirect_webengine_data, tabbed_browser_stubs):
tabwidget = tabbed_browser_stubs[0].widget tabwidget = tabbed_browser_stubs[0].widget
tabwidget.current_index = 0 tabwidget.current_index = 0
tabwidget.index_of = 0 tabwidget.index_of = 0
container = QWidget()
qtbot.add_widget(container)
vbox = QVBoxLayout(container)
webenginetab = pytest.importorskip( webenginetab = pytest.importorskip(
'qutebrowser.browser.webengine.webenginetab') 'qutebrowser.browser.webengine.webenginetab')
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager, tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
private=False) 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 return tab
@ -455,9 +517,8 @@ def fake_args(request):
@pytest.fixture @pytest.fixture
def mode_manager(win_registry, config_stub, qapp): def mode_manager(win_registry, config_stub, key_config_stub, qapp):
mm = modeman.ModeManager(0) mm = modeman.init(0, parent=qapp)
objreg.register('mode-manager', mm, scope='window', window=0)
yield mm yield mm
objreg.delete('mode-manager', scope='window', window=0) objreg.delete('mode-manager', scope='window', window=0)

View File

@ -475,6 +475,9 @@ class HostBlockerStub:
def __init__(self): def __init__(self):
self.blocked_hosts = set() self.blocked_hosts = set()
def is_blocked(self, url):
return url in self.blocked_hosts
class SessionManagerStub: class SessionManagerStub:

View File

@ -203,6 +203,7 @@ class CallbackChecker(QObject):
def check(self, expected): def check(self, expected):
"""Wait until the JS result arrived and compare it.""" """Wait until the JS result arrived and compare it."""
__tracebackhide__ = True
if self._result is self.UNSET: if self._result is self.UNSET:
with self._qtbot.waitSignal(self.got_result, timeout=2000): with self._qtbot.waitSignal(self.got_result, timeout=2000):
pass pass

View File

@ -0,0 +1,359 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 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'

View File

@ -84,6 +84,7 @@ class TestStringEscape:
(None, 'undefined'), (None, 'undefined'),
(object(), TypeError), (object(), TypeError),
(True, 'true'), (True, 'true'),
([23, True, 'x'], '[23, true, "x"]'),
]) ])
def test_convert_js_arg(arg, expected): def test_convert_js_arg(arg, expected):
if expected is TypeError: if expected is TypeError:

View File

@ -16,7 +16,7 @@ setenv =
pyqt{,56,571,59,510,511}: LINK_PYQT_SKIP=true pyqt{,56,571,59,510,511}: LINK_PYQT_SKIP=true
pyqt{,56,571,59,510,511}: QUTE_BDD_WEBENGINE=true pyqt{,56,571,59,510,511}: QUTE_BDD_WEBENGINE=true
cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= 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 = basepython =
py35: {env:PYTHON:python3.5} py35: {env:PYTHON:python3.5}
py36: {env:PYTHON:python3.6} py36: {env:PYTHON:python3.6}