Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad5005dee | ||
|
|
361e4e93ed | ||
|
|
d22125249c | ||
|
|
a9871df971 | ||
|
|
89eb5224b0 | ||
|
|
1f850b8de9 | ||
|
|
40d3679073 | ||
|
|
4d5dfb5ee0 | ||
|
|
2619d9b83d | ||
|
|
2725538155 | ||
|
|
b1f3c19fc6 | ||
|
|
88f2f9dfb5 | ||
|
|
57c424accd | ||
|
|
2ab6fdab47 | ||
|
|
10c64a9e52 | ||
|
|
63954af9a7 | ||
|
|
6b423e15ae | ||
|
|
50c5a425c0 | ||
|
|
3de4a942e1 | ||
|
|
ec969e2da2 | ||
|
|
a5c3c49e8c | ||
|
|
ac1095405c | ||
|
|
18e008d9c0 | ||
|
|
68775a8cf6 | ||
|
|
ca62acb219 | ||
|
|
dbdb48ff71 | ||
|
|
b2088e711c | ||
|
|
4730c6fd6b | ||
|
|
bc19f138fb | ||
|
|
7502d10fd9 | ||
|
|
0d5489395e |
@@ -38,7 +38,8 @@ disable=no-self-use,
|
||||
suppressed-message,
|
||||
too-many-return-statements,
|
||||
duplicate-code,
|
||||
wrong-import-position
|
||||
wrong-import-position,
|
||||
too-many-boolean-expressions
|
||||
|
||||
[BASIC]
|
||||
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
@@ -14,6 +14,27 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.10.1
|
||||
-------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- `--qt-arg` and `--qt-flag` can now also be used to pass arguments to Chromium when using QtWebEngine.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- URLs are now redacted properly (username/password, and path/query for HTTPS) when using Proxy Autoconfig with QtWebKit
|
||||
- Crash when updating adblock lists with invalid UTF8-chars in them
|
||||
- Fixed the web inspector with QtWebEngine
|
||||
- Version checks when starting qutebrowser now also take the Qt version PyQt was compiled against into account
|
||||
- Hinting a input now doesn't select existing text anymore with QtWebKit
|
||||
- The cursor now moves to the end when input elements are selected with QtWebEngine
|
||||
- Download suffixes like (1) are now correctly stripped with QtWebEngine
|
||||
- Crash when trying to print a tab which was closed in the meantime
|
||||
- Crash when trying to open a file twice on Windows
|
||||
|
||||
v0.10.0
|
||||
-------
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
appdirs==1.4.2
|
||||
appdirs==1.4.3
|
||||
packaging==16.8
|
||||
pyparsing==2.1.10
|
||||
setuptools==34.3.0
|
||||
pyparsing==2.2.0
|
||||
setuptools==34.3.1
|
||||
six==1.10.0
|
||||
wheel==0.29.0
|
||||
|
||||
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 10, 0)
|
||||
__version_info__ = (0, 10, 1)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
||||
@@ -142,9 +142,6 @@ def init(args, crash_handler):
|
||||
pre_text="Error while initializing")
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
QTimer.singleShot(0, functools.partial(_process_args, args))
|
||||
QTimer.singleShot(10, functools.partial(_init_late_modules, args))
|
||||
|
||||
log.init.debug("Initializing eventfilter...")
|
||||
event_filter = EventFilter(qApp)
|
||||
qApp.installEventFilter(event_filter)
|
||||
@@ -155,11 +152,13 @@ def init(args, crash_handler):
|
||||
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
|
||||
qApp.focusChanged.connect(on_focus_changed)
|
||||
|
||||
_process_args(args)
|
||||
|
||||
QDesktopServices.setUrlHandler('http', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('https', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
|
||||
|
||||
macros.init()
|
||||
QTimer.singleShot(10, functools.partial(_init_late_modules, args))
|
||||
|
||||
log.init.debug("Init done!")
|
||||
crash_handler.raise_crashdlg()
|
||||
@@ -448,6 +447,7 @@ def _init_modules(args, crash_handler):
|
||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||
else:
|
||||
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
|
||||
macros.init()
|
||||
# Init backend-specific stuff
|
||||
browsertab.init()
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_fileobj(byte_io):
|
||||
byte_io = zf.open(filename, mode='r')
|
||||
else:
|
||||
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
||||
return io.TextIOWrapper(byte_io, encoding='utf-8')
|
||||
return byte_io
|
||||
|
||||
|
||||
def is_whitelisted_host(host):
|
||||
@@ -147,7 +147,7 @@ class HostBlocker:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
target.add(line.strip())
|
||||
except OSError:
|
||||
except (OSError, UnicodeDecodeError):
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
|
||||
return True
|
||||
@@ -205,6 +205,54 @@ class HostBlocker:
|
||||
download.finished.connect(
|
||||
functools.partial(self.on_download_finished, download))
|
||||
|
||||
def _parse_line(self, line):
|
||||
"""Parse a line from a host file.
|
||||
|
||||
Args:
|
||||
line: The bytes object to parse.
|
||||
|
||||
Returns:
|
||||
True if parsing succeeded, False otherwise.
|
||||
"""
|
||||
if line.startswith(b'#'):
|
||||
# Ignoring comments early so we don't have to care about
|
||||
# encoding errors in them.
|
||||
return True
|
||||
|
||||
try:
|
||||
line = line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.misc.error("Failed to decode: {!r}".format(line))
|
||||
return False
|
||||
|
||||
# Remove comments
|
||||
try:
|
||||
hash_idx = line.index('#')
|
||||
line = line[:hash_idx]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
line = line.strip()
|
||||
# Skip empty lines
|
||||
if not line:
|
||||
return True
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) == 1:
|
||||
# "one host per line" format
|
||||
host = parts[0]
|
||||
elif len(parts) == 2:
|
||||
# /etc/hosts format
|
||||
host = parts[1]
|
||||
else:
|
||||
log.misc.error("Failed to parse: {!r}".format(line))
|
||||
return False
|
||||
|
||||
if host not in self.WHITELISTED:
|
||||
self._blocked_hosts.add(host)
|
||||
|
||||
return True
|
||||
|
||||
def _merge_file(self, byte_io):
|
||||
"""Read and merge host files.
|
||||
|
||||
@@ -218,35 +266,18 @@ class HostBlocker:
|
||||
line_count = 0
|
||||
try:
|
||||
f = get_fileobj(byte_io)
|
||||
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
|
||||
zipfile.LargeZipFile, LookupError) as e:
|
||||
except (OSError, zipfile.BadZipFile, zipfile.LargeZipFile,
|
||||
LookupError) as e:
|
||||
message.error("adblock: Error while reading {}: {} - {}".format(
|
||||
byte_io.name, e.__class__.__name__, e))
|
||||
return
|
||||
|
||||
for line in f:
|
||||
line_count += 1
|
||||
# Remove comments
|
||||
try:
|
||||
hash_idx = line.index('#')
|
||||
line = line[:hash_idx]
|
||||
except ValueError:
|
||||
pass
|
||||
line = line.strip()
|
||||
# Skip empty lines
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) == 1:
|
||||
# "one host per line" format
|
||||
host = parts[0]
|
||||
elif len(parts) == 2:
|
||||
# /etc/hosts format
|
||||
host = parts[1]
|
||||
else:
|
||||
ok = self._parse_line(line)
|
||||
if not ok:
|
||||
error_count += 1
|
||||
continue
|
||||
if host not in self.WHITELISTED:
|
||||
self._blocked_hosts.add(host)
|
||||
|
||||
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
if error_count > 0:
|
||||
message.error("adblock: {} read errors for {}".format(
|
||||
|
||||
@@ -350,7 +350,7 @@ class CommandDispatcher:
|
||||
message.error("Printing failed!")
|
||||
|
||||
tab.printing.check_preview_support()
|
||||
diag = QPrintPreviewDialog()
|
||||
diag = QPrintPreviewDialog(tab)
|
||||
diag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint |
|
||||
Qt.WindowMinimizeButtonHint)
|
||||
@@ -376,7 +376,7 @@ class CommandDispatcher:
|
||||
message.error("Printing failed!")
|
||||
diag.deleteLater()
|
||||
|
||||
diag = QPrintDialog()
|
||||
diag = QPrintDialog(tab)
|
||||
diag.open(lambda: tab.printing.to_printer(diag.printer(),
|
||||
print_callback))
|
||||
|
||||
|
||||
@@ -529,7 +529,11 @@ class AbstractDownloadItem(QObject):
|
||||
if filename is None: # pragma: no cover
|
||||
log.downloads.error("No filename to open the download!")
|
||||
return
|
||||
utils.open_file(filename, cmdline)
|
||||
# By using a singleshot timer, we ensure that we return fast. This
|
||||
# is important on systems where process creation takes long, as
|
||||
# otherwise the prompt might hang around and cause bugs
|
||||
# (see issue #2296)
|
||||
QTimer.singleShot(0, lambda: utils.open_file(filename, cmdline))
|
||||
|
||||
def _ensure_can_set_filename(self, filename):
|
||||
"""Make sure we can still set a filename."""
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import sys
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import (QObject, pyqtSignal, pyqtSlot)
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
|
||||
from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
|
||||
QNetworkReply, QNetworkAccessManager,
|
||||
QHostAddress)
|
||||
@@ -199,16 +199,24 @@ class PACResolver:
|
||||
err = "Cannot resolve FindProxyForURL function, got '{}' instead"
|
||||
raise EvalProxyError(err.format(self._resolver.toString()))
|
||||
|
||||
def resolve(self, query):
|
||||
def resolve(self, query, from_file=False):
|
||||
"""Resolve a proxy via PAC.
|
||||
|
||||
Args:
|
||||
query: QNetworkProxyQuery.
|
||||
from_file: Whether the proxy info is coming from a file.
|
||||
|
||||
Return:
|
||||
A list of QNetworkProxy objects in order of preference.
|
||||
"""
|
||||
result = self._resolver.call([query.url().toString(),
|
||||
if from_file:
|
||||
string_flags = QUrl.PrettyDecoded
|
||||
else:
|
||||
string_flags = QUrl.RemoveUserInfo
|
||||
if query.url().scheme() == 'https':
|
||||
string_flags |= QUrl.RemovePath | QUrl.RemoveQuery
|
||||
|
||||
result = self._resolver.call([query.url().toString(string_flags),
|
||||
query.peerHostName()])
|
||||
result_str = result.toString()
|
||||
if not result.isString():
|
||||
@@ -236,6 +244,7 @@ class PACFetcher(QObject):
|
||||
assert url.scheme().startswith(pac_prefix)
|
||||
url.setScheme(url.scheme()[len(pac_prefix):])
|
||||
|
||||
self._pac_url = url
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
|
||||
self._reply = self._manager.get(QNetworkRequest(url))
|
||||
@@ -292,8 +301,9 @@ class PACFetcher(QObject):
|
||||
Return a list of QNetworkProxy objects in order of preference.
|
||||
"""
|
||||
self._wait()
|
||||
from_file = self._pac_url.scheme() == 'file'
|
||||
try:
|
||||
return self._pac.resolve(query)
|
||||
return self._pac.resolve(query, from_file=from_file)
|
||||
except (EvalProxyError, ParseProxyError) as e:
|
||||
log.network.exception("Error in PAC resolution: {}.".format(e))
|
||||
# .invalid is guaranteed to be inaccessible in RFC 6761.
|
||||
|
||||
@@ -326,6 +326,10 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
raise Error("Element position is out of view!")
|
||||
return pos
|
||||
|
||||
def _move_text_cursor(self):
|
||||
"""Move cursor to end after clicking."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_fake_event(self, click_target):
|
||||
"""Send a fake click event to the element."""
|
||||
pos = self._mouse_pos()
|
||||
@@ -356,11 +360,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
for evt in events:
|
||||
self._tab.send_event(evt)
|
||||
|
||||
def after_click():
|
||||
"""Move cursor to end after clicking."""
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
QTimer.singleShot(0, after_click)
|
||||
QTimer.singleShot(0, self._move_text_cursor)
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
"""Fake a click on an editable input field."""
|
||||
|
||||
@@ -144,7 +144,7 @@ def _get_suggested_filename(path):
|
||||
See https://bugreports.qt.io/browse/QTBUG-56978
|
||||
"""
|
||||
filename = os.path.basename(path)
|
||||
filename = re.sub(r'\([0-9]+\)$', '', filename)
|
||||
filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename)
|
||||
if not qtutils.version_check('5.8.1'):
|
||||
# https://bugreports.qt.io/browse/QTBUG-58155
|
||||
filename = urllib.parse.unquote(filename)
|
||||
|
||||
@@ -157,6 +157,12 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self._id)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def _move_text_cursor(self):
|
||||
if self.is_text_input() and self.is_editable():
|
||||
js_code = javascript.assemble('webelem', 'move_cursor_to_end',
|
||||
self._id)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
|
||||
# pylint doesn't know about Qt.MouseEventSynthesizedBySystem
|
||||
@@ -171,6 +177,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
# This actually "clicks" the element by calling focus() on it in JS.
|
||||
js_code = javascript.assemble('webelem', 'focus', self._id)
|
||||
self._tab.run_js_async(js_code)
|
||||
self._move_text_cursor()
|
||||
|
||||
def _click_js(self, _click_target):
|
||||
settings = QWebEngineSettings.globalSettings()
|
||||
|
||||
@@ -300,9 +300,18 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
break
|
||||
elem = elem._parent() # pylint: disable=protected-access
|
||||
|
||||
def _move_text_cursor(self):
|
||||
if self is None:
|
||||
# old PyQt versions call the slot after the element is deleted.
|
||||
return
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
ok = self._elem.evaluateJavaScript('this.focus(); true;')
|
||||
if not ok:
|
||||
if ok:
|
||||
self._move_text_cursor()
|
||||
else:
|
||||
log.webelem.debug("Failed to focus via JS, falling back to event")
|
||||
self._click_fake_event(click_target)
|
||||
|
||||
|
||||
@@ -694,30 +694,17 @@ class CssColor(BaseType):
|
||||
|
||||
class QssColor(CssColor):
|
||||
|
||||
"""Base class for a color value.
|
||||
|
||||
Class attributes:
|
||||
color_func_regexes: Valid function regexes.
|
||||
"""
|
||||
|
||||
num = r'[0-9]{1,3}%?'
|
||||
|
||||
color_func_regexes = [
|
||||
r'rgb\({num},\s*{num},\s*{num}\)'.format(num=num),
|
||||
r'rgba\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num),
|
||||
r'hsv\({num},\s*{num},\s*{num}\)'.format(num=num),
|
||||
r'hsva\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num),
|
||||
r'qlineargradient\(.*\)',
|
||||
r'qradialgradient\(.*\)',
|
||||
r'qconicalgradient\(.*\)',
|
||||
]
|
||||
"""Color used in a Qt stylesheet."""
|
||||
|
||||
def validate(self, value):
|
||||
functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient',
|
||||
'qradialgradient', 'qconicalgradient']
|
||||
self._basic_validation(value)
|
||||
if not value:
|
||||
return
|
||||
elif any(re.match(r, value) for r in self.color_func_regexes):
|
||||
# QColor doesn't handle these, so we do the best we can easily
|
||||
elif (any(value.startswith(func + '(') for func in functions) and
|
||||
value.endswith(')')):
|
||||
# QColor doesn't handle these
|
||||
pass
|
||||
elif QColor.isValidColor(value):
|
||||
pass
|
||||
|
||||
@@ -203,5 +203,11 @@ window._qutebrowser.webelem = (function() {
|
||||
elem.focus();
|
||||
};
|
||||
|
||||
funcs.move_cursor_to_end = function(id) {
|
||||
var elem = elements[id];
|
||||
elem.selectionStart = elem.value.length;
|
||||
elem.selectionEnd = elem.value.length;
|
||||
};
|
||||
|
||||
return funcs;
|
||||
})();
|
||||
|
||||
@@ -262,7 +262,7 @@ def get_backend(args):
|
||||
|
||||
def check_qt_version(backend):
|
||||
"""Check if the Qt version is recent enough."""
|
||||
from PyQt5.QtCore import qVersion, PYQT_VERSION, PYQT_VERSION_STR
|
||||
from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR
|
||||
from qutebrowser.utils import qtutils, version
|
||||
if (qtutils.version_check('5.2.0', operator.lt, strict=True) or
|
||||
PYQT_VERSION < 0x050200):
|
||||
|
||||
@@ -27,7 +27,7 @@ import getpass
|
||||
import binascii
|
||||
import hashlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QTimer
|
||||
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||
|
||||
import qutebrowser
|
||||
@@ -281,7 +281,11 @@ class IPCServer(QObject):
|
||||
if self._socket is None:
|
||||
log.ipc.debug("In on_disconnected with None socket!")
|
||||
else:
|
||||
self._socket.deleteLater()
|
||||
# For some reason Qt can still get delayed canReadNotifications
|
||||
# internally, so if we call deleteLater() right away and then call
|
||||
# QApplication::processEvents() somewhere in the code, we can get a
|
||||
# segfault.
|
||||
QTimer.singleShot(500, self._socket.deleteLater)
|
||||
self._socket = None
|
||||
# Maybe another connection is waiting.
|
||||
self.handle_connection()
|
||||
|
||||
@@ -19,13 +19,15 @@
|
||||
|
||||
"""Misc. widgets used at different places."""
|
||||
|
||||
import operator
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QTimer
|
||||
from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
|
||||
QStyleOption, QStyle, QLayout, QApplication)
|
||||
from PyQt5.QtGui import QValidator, QPainter
|
||||
|
||||
from qutebrowser.utils import utils, objreg
|
||||
from qutebrowser.misc import cmdhistory
|
||||
from qutebrowser.utils import utils, objreg, qtutils, log, usertypes
|
||||
from qutebrowser.misc import cmdhistory, objects
|
||||
|
||||
|
||||
class MinimalLineEditMixin:
|
||||
@@ -260,6 +262,16 @@ class WrapperLayout(QLayout):
|
||||
self._widget = widget
|
||||
container.setFocusProxy(widget)
|
||||
widget.setParent(container)
|
||||
if (qtutils.version_check('5.8.0', op=operator.eq) and
|
||||
objects.backend == usertypes.Backend.QtWebEngine and
|
||||
container.window() and
|
||||
container.window().windowHandle() and
|
||||
not container.window().windowHandle().isActive()):
|
||||
log.misc.debug("Calling QApplication::sync...")
|
||||
# WORKAROUND for:
|
||||
# https://bugreports.qt.io/browse/QTBUG-56652
|
||||
# https://codereview.qt-project.org/#/c/176113/5//ALL,unified
|
||||
QApplication.sync()
|
||||
|
||||
def unwrap(self):
|
||||
self._widget.setParent(None)
|
||||
|
||||
@@ -88,9 +88,12 @@ def version_check(version, op=operator.ge, strict=False):
|
||||
op: The operator to use for the check.
|
||||
strict: If given, also check the compiled Qt version.
|
||||
"""
|
||||
if strict:
|
||||
assert op in [operator.ge, operator.lt], op
|
||||
parsed = pkg_resources.parse_version(version)
|
||||
result = op(pkg_resources.parse_version(qVersion()), parsed)
|
||||
if result and strict:
|
||||
if ((strict and op == operator.ge and result) or
|
||||
(strict and op == operator.lt and not result)):
|
||||
result = op(pkg_resources.parse_version(QT_VERSION_STR), parsed)
|
||||
return result
|
||||
|
||||
|
||||
@@ -4,10 +4,21 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simple input</title>
|
||||
<script type="text/javascript">
|
||||
function setup_event_listener() {
|
||||
var elem = document.getElementById('qute-input-existing');
|
||||
console.log(elem);
|
||||
elem.addEventListener('input', function() {
|
||||
console.log("contents: " + elem.value);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<body onload="setup_event_listener()">
|
||||
<form><input id="qute-input"></input></form>
|
||||
With padding:
|
||||
<form><input type="text" style="padding-left: 20px;"></input></form>
|
||||
With existing text (logs to JS)::
|
||||
<form><input id="qute-input-existing" value="existing"></input></form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -188,6 +188,14 @@ Feature: Using hints
|
||||
And I run :hint
|
||||
Then the error "No elements found." should be shown
|
||||
|
||||
Scenario: Clicking input with existing text
|
||||
When I set general -> log-javascript-console to info
|
||||
And I open data/hints/input.html
|
||||
And I run :click-element id qute-input-existing
|
||||
And I wait for "Entering mode KeyMode.insert *" in the log
|
||||
And I run :fake-key new
|
||||
Then the javascript message "contents: existingnew" should be logged
|
||||
|
||||
### iframes
|
||||
|
||||
@qtwebengine_todo: Hinting in iframes is not implemented yet
|
||||
|
||||
@@ -70,7 +70,7 @@ Feature: Setting positional marks
|
||||
And I run :jump-mark b
|
||||
Then the error "Mark b is not set" should be shown
|
||||
|
||||
@qtwebengine_todo: Does not emit loaded signal for fragments?
|
||||
@qtwebengine_todo: Does not emit loaded signal for fragments? @flaky
|
||||
Scenario: Jumping to a local mark after changing fragments
|
||||
When I open data/marks.html#top
|
||||
And I run :scroll 'top'
|
||||
|
||||
@@ -263,8 +263,10 @@ Feature: Saving and loading sessions
|
||||
Then the error "No session loaded currently!" should be shown
|
||||
|
||||
Scenario: Saving current session after one is loaded
|
||||
When I open data/numbers/1.txt
|
||||
When I run :session-save current_session
|
||||
And I run :session-load current_session
|
||||
And I wait until data/numbers/1.txt is loaded
|
||||
And I run :session-save --current
|
||||
Then the message "Saved session current_session." should be shown
|
||||
|
||||
@@ -288,6 +290,7 @@ Feature: Saving and loading sessions
|
||||
And I run :window-only
|
||||
And I run :tab-only
|
||||
And I run :session-load window_session_name
|
||||
And I wait until data/numbers/5.txt is loaded
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
|
||||
@@ -209,15 +209,22 @@ class QuteProc(testprocess.Process):
|
||||
"load status for <qutebrowser.browser.* tab_id=0 "
|
||||
"url='about:blank'>: LoadStatus.success")
|
||||
start_okay_messages_focus = [
|
||||
# QtWebKit
|
||||
## QtWebKit
|
||||
"Focus object changed: "
|
||||
"<qutebrowser.browser.* tab_id=0 url='about:blank'>",
|
||||
# QtWebEngine
|
||||
# when calling QApplication::sync
|
||||
"Focus object changed: "
|
||||
"<qutebrowser.browser.webkit.webview.WebView tab_id=0 url=''>",
|
||||
|
||||
## QtWebEngine
|
||||
"Focus object changed: "
|
||||
"<PyQt5.QtWidgets.QOpenGLWidget object at *>",
|
||||
# QtWebEngine with Qt >= 5.8
|
||||
# with Qt >= 5.8
|
||||
"Focus object changed: "
|
||||
"<PyQt5.QtGui.QWindow object at *>",
|
||||
# when calling QApplication::sync
|
||||
"Focus object changed: "
|
||||
"<PyQt5.QtWidgets.QWidget object at *>",
|
||||
]
|
||||
|
||||
if (log_line.category == 'ipc' and
|
||||
|
||||
@@ -48,12 +48,6 @@ def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, zoom,
|
||||
if source == 'keypress':
|
||||
quteproc.press_keys(input_text)
|
||||
elif source == 'clipboard':
|
||||
if request.config.webengine:
|
||||
pytest.xfail(reason="QtWebEngine TODO: caret mode is not "
|
||||
"implemented")
|
||||
# Note we actually run the keypress tests with QtWebEngine, as for
|
||||
# some reason it selects all the text when clicking the field the
|
||||
# second time.
|
||||
quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(input_text))
|
||||
quteproc.send_cmd(':insert-text {clipboard}')
|
||||
else:
|
||||
|
||||
@@ -219,3 +219,45 @@ def test_webengine_inspector(request, quteproc_new):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(('127.0.0.1', port))
|
||||
s.close()
|
||||
|
||||
|
||||
@pytest.mark.linux
|
||||
def test_webengine_download_suffix(request, quteproc_new, tmpdir):
|
||||
"""Make sure QtWebEngine does not add a suffix to downloads."""
|
||||
if not request.config.webengine:
|
||||
pytest.skip()
|
||||
|
||||
download_dir = tmpdir / 'downloads'
|
||||
download_dir.ensure(dir=True)
|
||||
|
||||
(tmpdir / 'user-dirs.dirs').write(
|
||||
'XDG_DOWNLOAD_DIR={}'.format(download_dir))
|
||||
env = {'XDG_CONFIG_HOME': str(tmpdir)}
|
||||
args = (['--temp-basedir'] + _base_args(request.config))
|
||||
quteproc_new.start(args, env=env)
|
||||
|
||||
quteproc_new.set_setting('storage', 'prompt-download-directory', 'false')
|
||||
quteproc_new.set_setting('storage', 'download-directory',
|
||||
str(download_dir))
|
||||
quteproc_new.open_path('data/downloads/download.bin', wait=False)
|
||||
quteproc_new.wait_for(category='downloads', message='Download * finished')
|
||||
quteproc_new.open_path('data/downloads/download.bin', wait=False)
|
||||
quteproc_new.wait_for(message='Entering mode KeyMode.yesno *')
|
||||
quteproc_new.send_cmd(':prompt-accept yes')
|
||||
quteproc_new.wait_for(category='downloads', message='Download * finished')
|
||||
|
||||
files = download_dir.listdir()
|
||||
assert len(files) == 1
|
||||
assert files[0].basename == 'download.bin'
|
||||
|
||||
|
||||
def test_command_on_start(request, quteproc_new):
|
||||
"""Make sure passing a command on start works.
|
||||
|
||||
See https://github.com/qutebrowser/qutebrowser/issues/2408
|
||||
"""
|
||||
args = (['--temp-basedir'] + _base_args(request.config) +
|
||||
[':quickmark-add https://www.example.com/ example'])
|
||||
quteproc_new.start(args)
|
||||
quteproc_new.send_cmd(':quit')
|
||||
quteproc_new.wait_for_quit()
|
||||
|
||||
@@ -312,6 +312,68 @@ def test_failed_dl_update(config_stub, basedir, download_stub,
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('location', ['content', 'comment'])
|
||||
def test_invalid_utf8(config_stub, download_stub, tmpdir, caplog, location):
|
||||
"""Make sure invalid UTF-8 is handled correctly.
|
||||
|
||||
See https://github.com/qutebrowser/qutebrowser/issues/2301
|
||||
"""
|
||||
blocklist = tmpdir / 'blocklist'
|
||||
if location == 'comment':
|
||||
blocklist.write_binary(b'# nbsp: \xa0\n')
|
||||
else:
|
||||
assert location == 'content'
|
||||
blocklist.write_binary(b'https://www.example.org/\xa0')
|
||||
for url in BLOCKLIST_HOSTS:
|
||||
blocklist.write(url + '\n', mode='a')
|
||||
|
||||
config_stub.data = {
|
||||
'content': {
|
||||
'host-block-lists': [QUrl(str(blocklist))],
|
||||
'host-blocking-enabled': True,
|
||||
'host-blocking-whitelist': None,
|
||||
}
|
||||
}
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker.adblock_update()
|
||||
finished_signal = host_blocker._in_progress[0].finished
|
||||
|
||||
if location == 'content':
|
||||
with caplog.at_level(logging.ERROR):
|
||||
finished_signal.emit()
|
||||
expected = (r"Failed to decode: "
|
||||
r"b'https://www.example.org/\xa0localhost\n'")
|
||||
assert caplog.records[-2].message == expected
|
||||
else:
|
||||
finished_signal.emit()
|
||||
|
||||
host_blocker.read_hosts()
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
def test_invalid_utf8_compiled(config_stub, tmpdir, monkeypatch, caplog):
|
||||
"""Make sure invalid UTF-8 in the compiled file is handled."""
|
||||
data_dir = tmpdir / 'data'
|
||||
config_dir = tmpdir / 'config'
|
||||
monkeypatch.setattr(adblock.standarddir, 'data', lambda: str(data_dir))
|
||||
monkeypatch.setattr(adblock.standarddir, 'config', lambda: str(config_dir))
|
||||
|
||||
config_stub.data = {
|
||||
'content': {
|
||||
'host-block-lists': [],
|
||||
}
|
||||
}
|
||||
|
||||
(config_dir / 'blocked-hosts').write_binary(
|
||||
b'https://www.example.org/\xa0')
|
||||
(data_dir / 'blocked-hosts').ensure()
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
with caplog.at_level(logging.ERROR):
|
||||
host_blocker.read_hosts()
|
||||
assert caplog.records[-1].message == "Failed to read host blocklist!"
|
||||
|
||||
|
||||
def test_blocking_with_whitelist(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir):
|
||||
"""Ensure hosts in host-blocking-whitelist are never blocked."""
|
||||
|
||||
@@ -171,6 +171,41 @@ def test_fail_return():
|
||||
res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test")))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('url, has_secret', [
|
||||
('http://example.com/secret', True), # path passed with HTTP
|
||||
('http://example.com?secret=yes', True), # query passed with HTTP
|
||||
('http://secret@example.com', False), # user stripped with HTTP
|
||||
('http://user:secret@example.com', False), # password stripped with HTTP
|
||||
|
||||
('https://example.com/secret', False), # path stripped with HTTPS
|
||||
('https://example.com?secret=yes', False), # query stripped with HTTPS
|
||||
('https://secret@example.com', False), # user stripped with HTTPS
|
||||
('https://user:secret@example.com', False), # password stripped with HTTPS
|
||||
])
|
||||
@pytest.mark.parametrize('from_file', [True, False])
|
||||
def test_secret_url(url, has_secret, from_file):
|
||||
"""Make sure secret parts in an URL are stripped correctly.
|
||||
|
||||
The following parts are considered secret:
|
||||
- If the PAC info is loaded from a local file, nothing.
|
||||
- If the URL to resolve is a HTTP URL, the username/password.
|
||||
- If the URL to resolve is a HTTPS URL, the username/password, query
|
||||
and path.
|
||||
"""
|
||||
test_str = """
|
||||
function FindProxyForURL(domain, host) {{
|
||||
has_secret = domain.indexOf("secret") !== -1;
|
||||
expected_secret = {};
|
||||
if (has_secret !== expected_secret) {{
|
||||
throw new Error("Expected secret: " + expected_secret + ", found: " + has_secret + " in " + domain);
|
||||
}}
|
||||
return "DIRECT";
|
||||
}}
|
||||
""".format('true' if (has_secret or from_file) else 'false')
|
||||
res = pac.PACResolver(test_str)
|
||||
res.resolve(QNetworkProxyQuery(QUrl(url)), from_file=from_file)
|
||||
|
||||
|
||||
# See https://github.com/qutebrowser/qutebrowser/pull/1891#issuecomment-259222615
|
||||
|
||||
try:
|
||||
|
||||
@@ -408,7 +408,7 @@ class TestDefaultConfig:
|
||||
If it did change, place a new qutebrowser-vx.y.z.conf in old_configs
|
||||
and then increment the version.
|
||||
"""
|
||||
assert qutebrowser.__version__ == '0.10.0'
|
||||
assert qutebrowser.__version__ == '0.10.1'
|
||||
|
||||
@pytest.mark.parametrize('filename',
|
||||
os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')),
|
||||
|
||||
@@ -880,19 +880,16 @@ class ColorTests:
|
||||
('#12', []),
|
||||
('foobar', []),
|
||||
('42', []),
|
||||
('rgb(1, 2, 3, 4)', []),
|
||||
('foo(1, 2, 3)', []),
|
||||
('rgb(1, 2, 3', []),
|
||||
|
||||
('rgb(0, 0, 0)', [configtypes.QssColor]),
|
||||
('rgb(0,0,0)', [configtypes.QssColor]),
|
||||
('-foobar(42)', [configtypes.CssColor]),
|
||||
|
||||
('rgba(255, 255, 255, 255)', [configtypes.QssColor]),
|
||||
('rgba(255,255,255,255)', [configtypes.QssColor]),
|
||||
('hsv(359, 255, 255)', [configtypes.QssColor]),
|
||||
('hsva(359, 255, 255, 255)', [configtypes.QssColor]),
|
||||
('hsv(10%, 10%, 10%)', [configtypes.QssColor]),
|
||||
('rgba(255, 255, 255, 1.0)', [configtypes.QssColor]),
|
||||
('hsv(10%,10%,10%)', [configtypes.QssColor]),
|
||||
|
||||
('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, '
|
||||
'stop: 0.4 gray, stop:1 green)', [configtypes.QssColor]),
|
||||
('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, '
|
||||
|
||||
@@ -205,8 +205,7 @@ def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream):
|
||||
print("test", file=sys.{})
|
||||
sys.exit(1)
|
||||
""".format(stream)))
|
||||
assert len(caplog.records) == 2
|
||||
assert caplog.records[1].msg == 'Process {}:\ntest'.format(stream)
|
||||
assert caplog.records[-1].msg == 'Process {}:\ntest'.format(stream)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stream', ['stdout', 'stderr'])
|
||||
|
||||
@@ -101,7 +101,6 @@ class FakeSocket(QObject):
|
||||
_error_val: The value returned for error().
|
||||
_state_val: The value returned for state().
|
||||
_connect_successful: The value returned for waitForConnected().
|
||||
deleted: Set to True if deleteLater() was called.
|
||||
"""
|
||||
|
||||
readyRead = pyqtSignal()
|
||||
@@ -115,7 +114,6 @@ class FakeSocket(QObject):
|
||||
self._data = data
|
||||
self._connect_successful = connect_successful
|
||||
self.error = stubs.FakeSignal('error', func=self._error)
|
||||
self.deleted = False
|
||||
|
||||
def _error(self):
|
||||
return self._error_val
|
||||
@@ -131,9 +129,6 @@ class FakeSocket(QObject):
|
||||
self._data = rest
|
||||
return firstline + mid
|
||||
|
||||
def deleteLater(self):
|
||||
self.deleted = True
|
||||
|
||||
def errorString(self):
|
||||
return "Error string"
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ import overflow_test_cases
|
||||
# strict=True
|
||||
('5.4.0', '5.3.0', '5.4.0', operator.ge, False),
|
||||
('5.4.0', '5.4.0', '5.4.0', operator.ge, True),
|
||||
|
||||
('5.4.0', '5.3.0', '5.4.0', operator.lt, True),
|
||||
('5.4.0', '5.4.0', '5.4.0', operator.lt, False),
|
||||
])
|
||||
def test_version_check(monkeypatch, qversion, compiled, version, op, expected):
|
||||
"""Test for version_check().
|
||||
@@ -66,7 +69,7 @@ def test_version_check(monkeypatch, qversion, compiled, version, op, expected):
|
||||
"""
|
||||
monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion)
|
||||
if compiled is not None:
|
||||
monkeypatch.setattr('qutebrowser.utils.qtutils.QT_VERSION_STR', compiled)
|
||||
monkeypatch.setattr(qtutils, 'QT_VERSION_STR', compiled)
|
||||
strict = True
|
||||
else:
|
||||
strict = False
|
||||
|
||||
Reference in New Issue
Block a user